-
-
Notifications
You must be signed in to change notification settings - Fork 113
Clone remote repository. #1548
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Clone remote repository. #1548
Changes from all commits
Commits
Show all changes
67 commits
Select commit
Hold shift + click to select a range
e1d2e5c
Add clone button to sidebar
jeremypw ebb8ed1
Sketch out functionality
jeremypw 5200d9d
Fix action, Tweak regex
jeremypw 01d124f
Various corrections
jeremypw e39eac8
Clone repository
jeremypw 7e3da04
Reformat, local repo name, make active option
jeremypw 3b4ec49
Merge branch 'master' into jeremypw/implement-clone
jeremypw 328c75b
Merge branch 'master' into jeremypw/implement-clone
jeremypw a267a98
Fix lint
jeremypw 7881aee
Merge branch 'master' into jeremypw/implement-clone
jeremypw 85b2bb3
Merge branch 'master' into jeremypw/implement-clone
jeremypw 2251850
Allow narrower sidebar
jeremypw 15c6fc5
Merge branch 'master' into jeremypw/implement-clone
jeremypw ea1a356
Update src/Dialogs/CloneRepositoryDialog.vala
jeremypw 576ca43
Update src/Dialogs/CloneRepositoryDialog.vala
jeremypw 9c38d00
Replace local folder path entry with button linked to filechooser
jeremypw 5287f59
Set mnemonic widget for CloneEntry label
jeremypw 9f8bf46
Fix construction as advised, add comments
jeremypw 497bac7
Better variable names
jeremypw b4c79ed
Remove activates_default = false;
jeremypw 55490d2
Rewording
jeremypw 7a4bf5d
Use "emblem-downloads" icon
jeremypw a59c64b
No space before ellipsis
jeremypw 6138cfa
Do not use Gtk.Button.from_icon_name
jeremypw 0582c77
More explicit labels
jeremypw 50293b5
Merge branch 'master' into jeremypw/implement-clone
danirabbit ec4b2d0
Simplify
jeremypw 4dd6f6d
Merge branch 'master' into jeremypw/implement-clone
jeremypw ffbb883
Show all only on content_box
jeremypw 636d871
Use box padding not margin
jeremypw 7cadd28
CloneRepositoryDialog: design tweaks (#1584)
danirabbit 2c315a2
Move buttons from actionbar to projectchooser (#1583)
danirabbit 1bfa1aa
Check URL is in correct form using function not regex
jeremypw d428416
Merge branch 'master' into jeremypw/implement-clone
jeremypw 3273fe5
Prefill local name; improve NAME_REGEX
jeremypw 67be8d9
Restrict project folder names
jeremypw 273113c
Merge branch 'master' into jeremypw/implement-clone
jeremypw 731a05a
Fix extraneous comment
jeremypw e3a22b9
Remove set as active checkbox for now
jeremypw 25bbd0e
Use SYNC_CREATE
jeremypw 4d66c36
Fix unneeded try-catch
jeremypw 379ef30
Move cloning into response callback
jeremypw c2fe76f
Close dialog before cloning starts
jeremypw 22e57b7
Add TODO comment
jeremypw 7d60208
Fix undefined variable
jeremypw 2595107
Handle clone failure with dialog - option to retry
jeremypw 1454a5b
Handle no current active project
jeremypw 1b3232d
Persist projects folder and remote entries
jeremypw 0ae3fb9
Fix lint
jeremypw e949241
Merge branch 'master' into jeremypw/implement-clone
jeremypw 5536cc4
Merge branch 'master' into jeremypw/implement-clone
leonardo-lemos 784460d
Merge branch 'master' into jeremypw/implement-clone
zeebok 6915fce
Merge branch 'master' into jeremypw/implement-clone
jeremypw 01346c9
Add on accelerator to open a project
jeremypw 4119847
Merge branch 'master' into jeremypw/implement-clone
danirabbit db0e6f5
Merge branch 'master' into jeremypw/implement-clone
jeremypw af36e42
Set parent to error dialog, fix spelling
jeremypw e463c9f
Make clone function async
jeremypw 9d95150
Include success messagedialog
jeremypw d049990
Only construct parameters in Object ()
jeremypw 1a09d91
Use a thread for cloning, show spinner
jeremypw fc536e0
Remove unneeded throws Error
jeremypw 49e95a9
Fix some coding issues
jeremypw e4cf12b
Fix typo
jeremypw 4469d6c
Show spinner on separate page; hide buttons
jeremypw 1afe983
Cleanup comments
jeremypw 6779e66
Reorganise, reinstate default action
jeremypw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,258 @@ | ||
/* | ||
* SPDX-License-Identifier: GPL-2.0-or-later | ||
* SPDX-FileCopyrightText: 2025 elementary, Inc. <https://elementary.io> | ||
* | ||
* Authored by: Jeremy Wootten <[email protected]> | ||
*/ | ||
|
||
public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog { | ||
// Git project name rules according to GitLab | ||
// - Must start and end with a letter ( a-zA-Z ) or digit ( 0-9 ). | ||
// - Can contain only letters ( a-zA-Z ), digits ( 0-9 ), underscores ( _ ), dots ( . ), or dashes ( - ). | ||
// - Must not contain consecutive special characters. | ||
// - Cannot end in . git or . atom . | ||
private const string NAME_REGEX = """^[0-9a-zA-Z].([-_.]?[0-9a-zA-Z])*$"""; //TODO additional validation required | ||
|
||
private Regex name_regex; | ||
private Gtk.Label projects_folder_label; | ||
private Granite.ValidatedEntry remote_repository_uri_entry; | ||
private Granite.ValidatedEntry local_project_name_entry; | ||
private Gtk.Button clone_button; | ||
private Gtk.Stack stack; | ||
private Gtk.Spinner spinner; | ||
private Gtk.Revealer revealer; | ||
|
||
public bool can_clone { get; private set; default = false; } | ||
public string suggested_local_folder { get; construct; } | ||
public string suggested_remote { get; construct; } | ||
|
||
public bool cloning_in_progress { | ||
set { | ||
if (value) { | ||
stack.visible_child_name = "cloning"; | ||
spinner.start (); | ||
|
||
} else { | ||
stack.visible_child_name = "entries"; | ||
spinner.stop (); | ||
} | ||
} | ||
} | ||
|
||
public CloneRepositoryDialog (string _suggested_local_folder, string _suggested_remote) { | ||
Object ( | ||
suggested_local_folder: _suggested_local_folder, | ||
suggested_remote: _suggested_remote | ||
); | ||
} | ||
|
||
construct { | ||
transient_for = ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (); | ||
image_icon = new ThemedIcon ("git"); | ||
badge_icon = new ThemedIcon ("emblem-downloads"); | ||
modal = true; | ||
|
||
///TRANSLATORS "Git" is a proper name and must not be translated | ||
primary_text = _("Create a local clone of a Git repository"); | ||
secondary_text = _("The source repository and local folder must exist and have the required read and write permissions"); | ||
|
||
var cancel_button = add_button (_("Cancel"), Gtk.ResponseType.CANCEL); | ||
clone_button = (Gtk.Button)add_button (_("Clone Repository"), Gtk.ResponseType.APPLY); | ||
set_default (clone_button); | ||
|
||
try { | ||
name_regex = new Regex (NAME_REGEX, OPTIMIZE, ANCHORED | NOTEMPTY); | ||
} catch (RegexError e) { | ||
warning ("%s\n", e.message); | ||
} | ||
|
||
remote_repository_uri_entry = new Granite.ValidatedEntry () { | ||
placeholder_text = _("https://example.com/username/projectname.git"), | ||
input_purpose = URL, | ||
activates_default = true | ||
}; | ||
remote_repository_uri_entry.changed.connect (on_remote_uri_changed); | ||
remote_repository_uri_entry.text = suggested_remote; | ||
|
||
// The suggested folder is assumed to be valid as it is generated internally | ||
projects_folder_label = new Gtk.Label (suggested_local_folder) { | ||
hexpand = true, | ||
halign = START | ||
}; | ||
|
||
var folder_chooser_button_child = new Gtk.Box (HORIZONTAL, 6); | ||
folder_chooser_button_child.add (projects_folder_label); | ||
folder_chooser_button_child.add ( | ||
new Gtk.Image.from_icon_name ("folder-open-symbolic", BUTTON) | ||
); | ||
|
||
var folder_chooser_button = new Gtk.Button () { | ||
child = folder_chooser_button_child | ||
}; | ||
folder_chooser_button.clicked.connect (() => { | ||
var chooser = new Gtk.FileChooserNative ( | ||
_("Select folder where the cloned repository will be created"), | ||
this.transient_for, | ||
SELECT_FOLDER, | ||
_("Select"), | ||
_("Cancel") | ||
); | ||
chooser.set_current_folder (projects_folder_label.label); | ||
chooser.response.connect ((res) => { | ||
if (res == Gtk.ResponseType.ACCEPT) { | ||
projects_folder_label.label = chooser.get_filename (); | ||
update_can_clone (); | ||
} | ||
|
||
chooser.destroy (); | ||
}); | ||
chooser.show (); | ||
|
||
}); | ||
|
||
local_project_name_entry = new Granite.ValidatedEntry () { | ||
activates_default = true | ||
}; | ||
local_project_name_entry.changed.connect (validate_local_name); | ||
|
||
var content_box = new Gtk.Grid (); | ||
content_box.attach (new CloneEntry (_("Repository URL"), remote_repository_uri_entry), 0, 0); | ||
content_box.attach (new CloneEntry (_("Location"), folder_chooser_button), 0, 1); | ||
content_box.attach (new CloneEntry (_("Name of Clone"), local_project_name_entry), 0, 2); | ||
content_box.attach (revealer, 1, 2); | ||
|
||
var cloning_box = new Gtk.Box (HORIZONTAL, 12) { | ||
valign = CENTER, | ||
halign = CENTER | ||
}; | ||
var cloning_label = new Granite.HeaderLabel (_("Cloning in progress")); | ||
spinner = new Gtk.Spinner (); | ||
cloning_box.add (cloning_label); | ||
cloning_box.add (spinner); | ||
|
||
stack = new Gtk.Stack (); | ||
stack.add_named (content_box, "entries"); | ||
stack.add_named (cloning_box, "cloning"); | ||
stack.visible_child_name = "entries"; | ||
|
||
custom_bin.add (stack); | ||
custom_bin.show_all (); | ||
|
||
bind_property ("can-clone", clone_button, "sensitive", DEFAULT | SYNC_CREATE); | ||
spinner.bind_property ("active", clone_button, "visible", INVERT_BOOLEAN); | ||
spinner.bind_property ("active", cancel_button, "visible", INVERT_BOOLEAN); | ||
can_clone = false; | ||
|
||
// Focus cancel button so that entry placeholder text shows | ||
cancel_button.grab_focus (); | ||
} | ||
|
||
public string get_projects_folder () { | ||
return projects_folder_label.label; | ||
} | ||
|
||
public string get_remote () { | ||
if (remote_repository_uri_entry.is_valid) { | ||
var uri = remote_repository_uri_entry.text; | ||
var last_separator = uri.last_index_of (Path.DIR_SEPARATOR_S); | ||
return uri.slice (0, last_separator + 1); | ||
} else { | ||
return suggested_remote; | ||
} | ||
} | ||
|
||
public string get_valid_source_repository_uri () requires (can_clone) { | ||
//TODO Further validation here? | ||
return remote_repository_uri_entry.text; | ||
} | ||
|
||
public string get_valid_target () requires (can_clone) { | ||
return Path.build_filename (Path.DIR_SEPARATOR_S, projects_folder_label.label, local_project_name_entry.text); | ||
} | ||
|
||
private void update_can_clone () { | ||
can_clone = remote_repository_uri_entry.is_valid && | ||
local_project_name_entry.is_valid && | ||
projects_folder_label.label != ""; | ||
|
||
// Checking whether the target folder already exists and is not empty occurs after pressing apply | ||
} | ||
|
||
private void on_remote_uri_changed (Gtk.Editable source) { | ||
var entry = (Granite.ValidatedEntry)source; | ||
if (entry.is_valid) { //entry is a URL | ||
//Only accept HTTPS url atm but may also accept ssh address in future | ||
entry.is_valid = validate_https_address (entry.text); | ||
} | ||
|
||
update_can_clone (); | ||
} | ||
|
||
private bool validate_https_address (string address) { | ||
var valid = false; | ||
string? scheme, userinfo, host, path, query, fragment; | ||
int port; | ||
try { | ||
Uri.split ( | ||
address, | ||
UriFlags.NONE, | ||
out scheme, | ||
out userinfo, | ||
out host, | ||
out port, | ||
out path, | ||
out query, | ||
out fragment | ||
); | ||
|
||
if (query == null && | ||
fragment == null && | ||
scheme == "https" && | ||
host != null && //e.g. github.com | ||
userinfo == null && //User is first part of pat | ||
(port < 0 || port == 443)) { //TODO Allow non-standard port to be selected | ||
|
||
if (path.has_prefix (Path.DIR_SEPARATOR_S)) { | ||
path = path.substring (1, -1); | ||
} | ||
|
||
var parts = path.split (Path.DIR_SEPARATOR_S); | ||
valid = parts.length == 2 && parts[1].has_suffix (".git"); | ||
if (valid) { | ||
local_project_name_entry.text = parts[1].slice (0, -4); | ||
} | ||
} | ||
} catch (UriError e) { | ||
warning ("Uri split error %s", e.message); | ||
} | ||
|
||
return valid; | ||
} | ||
|
||
private void validate_local_name () { | ||
unowned var name = local_project_name_entry.text; | ||
MatchInfo? match_info; | ||
bool valid = false; | ||
if (name_regex.match (name, ANCHORED | NOTEMPTY, out match_info) && match_info.matches ()) { | ||
valid = !name.has_suffix (".git") && !name.has_suffix (".atom"); | ||
} | ||
|
||
local_project_name_entry.is_valid = valid; | ||
update_can_clone (); | ||
} | ||
|
||
private class CloneEntry : Gtk.Box { | ||
public CloneEntry (string label_text, Gtk.Widget entry) { | ||
var label = new Granite.HeaderLabel (label_text) { | ||
mnemonic_widget = entry | ||
}; | ||
|
||
add (label); | ||
add (entry); | ||
} | ||
|
||
construct { | ||
orientation = VERTICAL; | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.