-
-
Notifications
You must be signed in to change notification settings - Fork 118
Expand file tree
/
Copy pathCloneRepositoryDialog.vala
More file actions
264 lines (222 loc) · 9.58 KB
/
CloneRepositoryDialog.vala
File metadata and controls
264 lines (222 loc) · 9.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
/*
* SPDX-License-Identifier: GPL-2.0-or-later
* SPDX-FileCopyrightText: 2025 elementary, Inc. <https://elementary.io>
*
* Authored by: Jeremy Wootten <jeremywootten@gmail.com>
*/
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;
public bool can_clone { get; private set; default = false; }
public bool update_submodules { get; set; default = true; }
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 update_submodules_checkbox = new Gtk.CheckButton.with_label (_("Update submodules")) {
margin_top = 12,
halign = START
};
this.bind_property ("update-submodules", update_submodules_checkbox, "active", BIDIRECTIONAL | SYNC_CREATE );
var content_box = new Gtk.Box (VERTICAL, 0);
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.add (update_submodules_checkbox);
var cloning_label = new Granite.HeaderLabel (_("Cloning in progress"));
spinner = new Gtk.Spinner ();
var cloning_box = new Gtk.Box (HORIZONTAL, 12) {
valign = CENTER,
halign = CENTER
};
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;
}
}
}