-
-
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
Clone remote repository. #1548
Changes from 55 commits
e1d2e5c
ebb8ed1
5200d9d
01d124f
e39eac8
7e3da04
3b4ec49
328c75b
a267a98
7881aee
85b2bb3
2251850
15c6fc5
ea1a356
576ca43
9c38d00
5287f59
9f8bf46
497bac7
b4c79ed
55490d2
7a4bf5d
a59c64b
6138cfa
0582c77
50293b5
ec4b2d0
4dd6f6d
ffbb883
636d871
7cadd28
2c315a2
1bfa1aa
d428416
3273fe5
67be8d9
273113c
731a05a
e3a22b9
25bbd0e
4d66c36
379ef30
c2fe76f
22e57b7
7d60208
2595107
1454a5b
1b3232d
0ae3fb9
e949241
5536cc4
784460d
6915fce
01346c9
4119847
db0e6f5
af36e42
e463c9f
9d95150
d049990
1a09d91
fc536e0
49e95a9
e4cf12b
4469d6c
1afe983
6779e66
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,228 @@ | ||
| /* | ||
| * 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 { | ||
| public bool can_clone { get; private set; default = false; } | ||
|
|
||
|
|
||
| // 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; | ||
|
|
||
| public string suggested_local_folder { get; construct; } | ||
| public string suggested_remote { get; construct; } | ||
|
|
||
| public CloneRepositoryDialog (string _suggested_local_folder, string _suggested_remote) { | ||
| Object ( | ||
| transient_for: ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (), | ||
| image_icon: new ThemedIcon ("git"), | ||
| modal: true, | ||
| suggested_local_folder: _suggested_local_folder, | ||
| suggested_remote: _suggested_remote | ||
| ); | ||
| } | ||
|
|
||
| construct { | ||
| try { | ||
| name_regex = new Regex (NAME_REGEX, OPTIMIZE, ANCHORED | NOTEMPTY); | ||
| } catch (RegexError e) { | ||
| warning ("%s\n", e.message); | ||
| } | ||
|
|
||
| var cancel_button = add_button (_("Cancel"), Gtk.ResponseType.CANCEL); | ||
|
|
||
| ///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"); | ||
| badge_icon = new ThemedIcon ("emblem-downloads"); | ||
|
|
||
| remote_repository_uri_entry = new Granite.ValidatedEntry () { | ||
| placeholder_text = _("https://example.com/username/projectname.git"), | ||
| input_purpose = URL | ||
| }; | ||
| 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 (); | ||
| local_project_name_entry.changed.connect (validate_local_name); | ||
|
|
||
| var content_box = new Gtk.Box (VERTICAL, 12); | ||
| content_box.add (new CloneEntry (_("Repository URL"), remote_repository_uri_entry)); | ||
| content_box.add (new CloneEntry (_("Location"), folder_chooser_button)); | ||
| content_box.add (new CloneEntry (_("Name of Clone"), local_project_name_entry)); | ||
| content_box.show_all (); | ||
|
|
||
| custom_bin.add (content_box); | ||
|
|
||
| var clone_button = (Gtk.Button) add_button (_("Clone Repository"), Gtk.ResponseType.APPLY); | ||
| clone_button.can_default = true; | ||
| clone_button.has_default = true; | ||
| clone_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); | ||
|
|
||
| bind_property ("can-clone", clone_button, "sensitive", DEFAULT | SYNC_CREATE); | ||
|
|
||
| //Do not want to connect to "is-valid" property notification as this gets changed to "true" every time the entry | ||
| //text changed. So call explicitly after we validate the text. | ||
| 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 != ""; | ||
|
|
||
| //TODO Check whether the target folder already exists and is not empty? | ||
| } | ||
|
|
||
| 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"); | ||
jeremypw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -112,5 +112,38 @@ namespace Scratch.Services { | |
|
|
||
| return build_path; | ||
| } | ||
|
|
||
| //TODO Make this a real async function that does not block the main loop. | ||
|
||
| public async bool clone_repository ( | ||
| string uri, | ||
| string local_folder, | ||
| out File? repo_workdir | ||
| ) throws Error { | ||
|
|
||
| repo_workdir = null; | ||
| var folder_file = File.new_for_path (local_folder); | ||
|
|
||
| var fetch_options = new Ggit.FetchOptions (); | ||
| fetch_options.set_download_tags (Ggit.RemoteDownloadTagsType.UNSPECIFIED); | ||
| //TODO Set callbacks for authentification and progress | ||
| fetch_options.set_remote_callbacks (null); | ||
|
|
||
| var clone_options = new Ggit.CloneOptions (); | ||
| clone_options.set_local (Ggit.CloneLocal.AUTO); | ||
| clone_options.set_is_bare (false); | ||
| clone_options.set_fetch_options (fetch_options); | ||
|
|
||
| var new_repo = Ggit.Repository.clone ( | ||
| uri, | ||
| folder_file, | ||
| clone_options | ||
| ); | ||
|
|
||
| if (new_repo != null) { | ||
| repo_workdir = new_repo.get_workdir (); | ||
| } | ||
|
|
||
| return new_repo != null; | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.