Skip to content

Add functionality to 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

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions src/Dialogs/CloneRepositoryDialog.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
* Copyright 2025 elementary, Inc. (https://elementary.io)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program; if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
* Authored by: Jeremy Wootten <[email protected]>
*/

public class Scratch.Dialogs.CloneRepositoryDialog : Granite.MessageDialog {
public FolderManager.ProjectFolderItem? active_project { get; construct; }
public bool can_clone { get; private set; default = false; }


//Taken from "switchboard-plug-parental-controls/src/plug/Views/InternetView.vala"
private const string NAME_REGEX = "^[0-9a-zA-Z_-]+$";
private const string LOCAL_FOLDER_REGEX ="""^(/[^/ ]*)+/?$""";
private const string URL_REGEX = "([^/w.])[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{1,3}([^/])\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*\\b)";
private Regex name_regex;
private Regex local_folder_regex;
private Regex url_regex;
private Granite.ValidatedEntry repository_host_uri_entry;
private Granite.ValidatedEntry repository_user_entry;
private Granite.ValidatedEntry repository_name_entry;
private Granite.ValidatedEntry repository_local_folder_entry;
private Granite.ValidatedEntry repository_local_name_entry;
private Gtk.CheckButton set_as_active_check;

public CloneRepositoryDialog (string local_folder) {
Object (
transient_for: ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (),
image_icon: new ThemedIcon ("git")
);

repository_local_folder_entry.text = local_folder;
repository_host_uri_entry.text = "https://github.com";
repository_user_entry.text = "elementary";
repository_name_entry.text = "";
repository_local_name_entry.text = "";

on_is_valid_changed ();
}

construct {
try {
name_regex = new Regex (NAME_REGEX, RegexCompileFlags.OPTIMIZE);
local_folder_regex = new Regex (LOCAL_FOLDER_REGEX, RegexCompileFlags.OPTIMIZE);
url_regex = new Regex (URL_REGEX, RegexCompileFlags.OPTIMIZE);
} catch (RegexError e) {
warning ("%s\n", e.message);
}

add_button (_("Cancel"), Gtk.ResponseType.CANCEL);
primary_text = _("Create a local clone of a Git repository");
///TRANSLATORS "Git" is a proper name and must not be translated
secondary_text = _("The source repository and local folder must exist and be accessible");
badge_icon = new ThemedIcon ("download");

repository_host_uri_entry = new Granite.ValidatedEntry.from_regex (url_regex) {
activates_default = false
};
repository_user_entry = new Granite.ValidatedEntry.from_regex (name_regex) {
activates_default = false
};
repository_name_entry = new Granite.ValidatedEntry.from_regex (name_regex) {
activates_default = false
};
repository_local_folder_entry = new Granite.ValidatedEntry.from_regex (local_folder_regex) {
activates_default = false,
width_chars = 50
};
repository_local_name_entry = new Granite.ValidatedEntry.from_regex (name_regex) {
activates_default = false,
};

set_as_active_check = new Gtk.CheckButton.with_label (_("Set as Active Project")) {
margin_top = 12,
active = true
};

var content_box = new Gtk.Box (VERTICAL, 12);
content_box.add (new Granite.HeaderLabel (_("Source Repository")));
content_box.add (new CloneEntry (_("Host URI"), repository_host_uri_entry));
content_box.add (new CloneEntry (_("User"), repository_user_entry));
content_box.add (new CloneEntry (_("Name"), repository_name_entry));
content_box.add (new Granite.HeaderLabel (_("Clone")));
content_box.add (new CloneEntry (_("Target Folder"), repository_local_folder_entry));
content_box.add (new CloneEntry (_("Target Name"), repository_local_name_entry));
content_box.add (set_as_active_check);

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);

repository_host_uri_entry.notify["is-valid"].connect (on_is_valid_changed);
repository_user_entry.notify["is-valid"].connect (on_is_valid_changed);
repository_name_entry.notify["is-valid"].connect (on_is_valid_changed);
repository_local_folder_entry.notify["is-valid"].connect (on_is_valid_changed);

clone_button.sensitive = can_clone;
bind_property ("can-clone", clone_button, "sensitive");

show_all ();
}

public string get_source_repository_uri () requires (can_clone) {
//TODO Further validation here?
var repo_uri = Path.build_path (
Path.DIR_SEPARATOR_S,
repository_host_uri_entry.text,
repository_user_entry.text,
repository_name_entry.text
);

if (!repo_uri.has_suffix (".git")) {
repo_uri += ".git";
}

return repo_uri;
}

public string get_local_folder () requires (can_clone) {
//TODO Further validation here?
return repository_local_folder_entry.text;
}

public string get_local_name () requires (can_clone) {
var local_name = repository_local_name_entry.text;
if (local_name == "") {
local_name = repository_name_entry.text;
}
//TODO Further validation here?
return local_name;
}

private void on_is_valid_changed () {
can_clone = repository_host_uri_entry.is_valid &&
repository_user_entry.is_valid &&
repository_name_entry.is_valid &&
repository_local_folder_entry.is_valid;
}

private class CloneEntry : Gtk.Box {
public CloneEntry (string label_text, Gtk.Widget entry) {
var label = new Gtk.Label (label_text) {
halign = START
};
add (label);
add (entry);
}

construct {
orientation = VERTICAL;
spacing = 6;
hexpand = false;
margin_start = 12;
margin_end = 12;
margin_bottom = 12;
}
}
}
43 changes: 43 additions & 0 deletions src/MainWindow.vala
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ namespace Scratch {
public const string ACTION_GROUP = "win";
public const string ACTION_PREFIX = ACTION_GROUP + ".";
public const string ACTION_FIND = "action-find";
public const string ACTION_CLONE_REPO = "action-clone-repo";
public const string ACTION_FIND_NEXT = "action-find-next";
public const string ACTION_FIND_PREVIOUS = "action-find-previous";
public const string ACTION_FIND_GLOBAL = "action-find-global";
Expand Down Expand Up @@ -126,6 +127,7 @@ namespace Scratch {
private Services.GitManager git_manager;

private const ActionEntry[] ACTION_ENTRIES = {
{ ACTION_CLONE_REPO, action_clone_repo },
{ ACTION_FIND, action_find, "s"},
{ ACTION_FIND_NEXT, action_find_next },
{ ACTION_FIND_PREVIOUS, action_find_previous },
Expand Down Expand Up @@ -1023,6 +1025,47 @@ namespace Scratch {
}
}

private void action_clone_repo (SimpleAction action, Variant? param) {
var uri = "";
//By default, create clone in parent of the current project
var local_folder = Path.get_dirname (git_manager.active_project_path);
var local_name = "";
var clone_dialog = new Dialogs.CloneRepositoryDialog (local_folder);
clone_dialog.response.connect ((res) => {
if (res == Gtk.ResponseType.APPLY) {
uri = clone_dialog.get_source_repository_uri ();
local_folder = clone_dialog.get_local_folder ();
local_name = clone_dialog.get_local_name ();
}

clone_dialog.destroy ();
});

clone_dialog.run ();

if (clone_dialog.can_clone) {
//TODO Show progress while cloning
git_manager.clone_repository.begin (
uri,
Path.build_filename (Path.DIR_SEPARATOR_S, local_folder, local_name),
(obj, res) => {
try {
File? workdir = null;
if (git_manager.clone_repository.end (res, out workdir)) {
debug ("Repository cloned into %s", workdir.get_uri ());
open_folder (workdir);
//TODO Make active according to dialog checkbox
}
} catch (Error e) {
warning ("Unable to clone '%s'. %s", uri, e.message);
}
}
);
} else {
//TODO Give feedback
}
}

private void action_collapse_all_folders () {
folder_manager_view.collapse_all ();
}
Expand Down
31 changes: 31 additions & 0 deletions src/Services/GitManager.vala
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,36 @@ namespace Scratch.Services {

return build_path;
}

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);
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;
}
}
}
7 changes: 7 additions & 0 deletions src/Widgets/Sidebar.vala
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ public class Code.Sidebar : Gtk.Grid {
label = _("Open Folder…")
};

var clone_button = new Gtk.Button.from_icon_name ("folder-open-symbolic", Gtk.IconSize.SMALL_TOOLBAR) {
action_name = Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_CLONE_REPO,
always_show_image = true,
label = _("Clone Repository…")
};

var collapse_all_menu_item = new GLib.MenuItem (_("Collapse All"), Scratch.MainWindow.ACTION_PREFIX
+ Scratch.MainWindow.ACTION_COLLAPSE_ALL_FOLDERS);

Expand All @@ -81,6 +87,7 @@ public class Code.Sidebar : Gtk.Grid {
project_more_button.tooltip_text = _("Manage project folders");

actionbar.add (add_folder_button);
actionbar.add (clone_button);
actionbar.pack_end (project_more_button);

add (headerbar);
Expand Down
1 change: 1 addition & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ code_files = files(
'Dialogs/PreferencesDialog.vala',
'Dialogs/RestoreConfirmationDialog.vala',
'Dialogs/CloseProjectsConfirmationDialog.vala',
'Dialogs/CloneRepositoryDialog.vala',
'Dialogs/OverwriteUncommittedConfirmationDialog.vala',
'Dialogs/GlobalSearchDialog.vala',
'Dialogs/NewBranchDialog.vala',
Expand Down