fordon_greeman / voidfiles

Branch: main
Switch branches/tags
main
theidioticdev add emacs 2079ec5 23h ago
367 lines · 14.8 KB Raw Blame History
1 ;;; magit-clone.el --- Clone a repository -*- lexical-binding:t -*-
2
3 ;; Copyright (C) 2008-2026 The Magit Project Contributors
4
5 ;; Author: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev>
6 ;; Maintainer: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev>
7
8 ;; SPDX-License-Identifier: GPL-3.0-or-later
9
10 ;; Magit is free software: you can redistribute it and/or modify it
11 ;; under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
14 ;;
15 ;; Magit is distributed in the hope that it will be useful, but WITHOUT
16 ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
17 ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
18 ;; License for more details.
19 ;;
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with Magit. If not, see <https://www.gnu.org/licenses/>.
22
23 ;;; Commentary:
24
25 ;; This library implements clone commands.
26
27 ;;; Code:
28
29 (require 'magit)
30
31 ;;; Options
32
33 (defcustom magit-clone-set-remote-head nil
34 "Whether cloning creates the symbolic-ref `<remote>/HEAD'."
35 :package-version '(magit . "2.4.2")
36 :group 'magit-commands
37 :type 'boolean)
38
39 (defcustom magit-clone-set-remote.pushDefault 'ask
40 "Whether to set the value of `remote.pushDefault' after cloning.
41
42 If t, then set without asking. If nil, then don't set. If
43 `ask', then ask."
44 :package-version '(magit . "2.4.0")
45 :group 'magit-commands
46 :type '(choice (const :tag "Set" t)
47 (const :tag "Ask" ask)
48 (const :tag "Don't set" nil)))
49
50 (defcustom magit-clone-default-directory nil
51 "Default directory to use when `magit-clone' reads destination.
52 If nil (the default), then use the value of `default-directory'.
53 If a directory, then use that. If a function, then call that
54 with the remote url as only argument and use the returned value."
55 :package-version '(magit . "2.90.0")
56 :group 'magit-commands
57 :type '(choice (const :tag "Value of default-directory")
58 (directory :tag "Constant directory")
59 (function :tag "Function's value")))
60
61 (defcustom magit-clone-always-transient nil
62 "Whether `magit-clone' always acts as a transient prefix command.
63 If nil, then a prefix argument has to be used to show the transient
64 popup instead of invoking the default suffix `magit-clone-regular'
65 directly."
66 :package-version '(magit . "3.0.0")
67 :group 'magit-commands
68 :type 'boolean)
69
70 (defcustom magit-clone-name-alist
71 '(("\\`\\(?:github:\\|gh:\\)?\\([^:]+\\)\\'" "github.com" "github.user")
72 ("\\`\\(?:gitlab:\\|gl:\\)\\([^:]+\\)\\'" "gitlab.com" "gitlab.user")
73 ("\\`\\(?:sourcehut:\\|sh:\\)\\([^:]+\\)\\'" "git.sr.ht" "sourcehut.user"))
74 "Alist mapping repository names to repository urls.
75
76 Each element has the form (REGEXP HOSTNAME USER). When the user
77 enters a name when a cloning command asks for a name or url, then
78 that is looked up in this list. The first element whose REGEXP
79 matches is used.
80
81 The format specified by option `magit-clone-url-format' is used
82 to turn the name into an url, using HOSTNAME and the repository
83 name. If the provided name contains a slash, then that is used.
84 Otherwise if the name omits the owner of the repository, then the
85 default user specified in the matched entry is used.
86
87 If USER contains a dot, then it is treated as a Git variable and
88 the value of that is used as the username. Otherwise it is used
89 as the username itself."
90 :package-version '(magit . "4.0.0")
91 :group 'magit-commands
92 :type '(repeat (list regexp
93 (string :tag "Hostname")
94 (string :tag "User name or git variable"))))
95
96 (defcustom magit-clone-url-format
97 '(("git.sr.ht" . "git@%h:%n")
98 (t . "git@%h:%n.git"))
99 "Format(s) used when turning repository names into urls.
100
101 In a format string, %h is the hostname and %n is the repository
102 name, including the name of the owner.
103
104 The value can be a string (representing a single static format)
105 or an alist with elements (HOSTNAME . FORMAT) mapping hostnames
106 to formats. When an alist is used, the t key represents the
107 default. Also see `magit-clone-name-alist'."
108 :package-version '(magit . "4.0.0")
109 :group 'magit-commands
110 :type '(choice (string :tag "Format")
111 (alist :key-type (choice (string :tag "Host")
112 (const :tag "Default" t))
113 :value-type (string :tag "Format"))))
114
115 (defcustom magit-post-clone-hook nil
116 "Hook run after the repository has been successfully cloned.
117
118 When the hook is called, `default-directory' is let-bound to the
119 directory where the repository has been cloned."
120 :package-version '(magit . "4.0.0")
121 :group 'magit-commands
122 :type 'hook)
123
124 ;;; Commands
125
126 ;;;###autoload(autoload 'magit-clone "magit-clone" nil t)
127 (transient-define-prefix magit-clone (&optional transient)
128 "Clone a repository."
129 :man-page "git-clone"
130 ["Fetch arguments"
131 ("-B" "Clone a single branch" "--single-branch")
132 ("-n" "Do not clone tags" "--no-tags")
133 ("-S" "Clones submodules" "--recurse-submodules" :level 6)
134 ("-l" "Do not optimize" "--no-local" :level 7)]
135 ["Setup arguments"
136 ("-o" "Set name of remote" ("-o" "--origin="))
137 ("-b" "Set HEAD branch" ("-b" "--branch="))
138 (magit-clone:--filter :level 7)
139 ("-g" "Separate git directory" "--separate-git-dir="
140 transient-read-directory :level 7)
141 ("-t" "Use template directory" "--template="
142 transient-read-existing-directory :level 6)]
143 ["Local sharing arguments"
144 ("-s" "Share objects" ("-s" "--shared" :level 7))
145 ("-h" "Do not use hardlinks" "--no-hardlinks")]
146 ["Clone"
147 ("C" "regular" magit-clone-regular)
148 ("s" "shallow" magit-clone-shallow)
149 ("d" "shallow since date" magit-clone-shallow-since :level 7)
150 ("e" "shallow excluding" magit-clone-shallow-exclude :level 7)
151 (">" "sparse checkout" magit-clone-sparse :level 6)
152 ("b" "bare" magit-clone-bare)
153 ("m" "mirror" magit-clone-mirror)]
154 (interactive (list (or magit-clone-always-transient current-prefix-arg)))
155 (if transient
156 (transient-setup 'magit-clone)
157 (call-interactively #'magit-clone-regular)))
158
159 (transient-define-argument magit-clone:--filter ()
160 :description "Filter some objects"
161 :class 'transient-option
162 :key "-f"
163 :argument "--filter="
164 :reader #'magit-clone-read-filter)
165
166 (defun magit-clone-read-filter (prompt initial-input history)
167 (magit-completing-read prompt
168 (list "blob:none" "tree:0")
169 nil nil initial-input history))
170
171 ;;;###autoload
172 (defun magit-clone-regular (repository directory args)
173 "Create a clone of REPOSITORY in DIRECTORY.
174 Then show the status buffer for the new repository."
175 (interactive (magit-clone-read-args))
176 (magit-clone-internal repository directory args))
177
178 ;;;###autoload
179 (defun magit-clone-shallow (repository directory args depth)
180 "Create a shallow clone of REPOSITORY in DIRECTORY.
181 Then show the status buffer for the new repository.
182 With a prefix argument read the DEPTH of the clone;
183 otherwise use 1."
184 (interactive (append (magit-clone-read-args)
185 (list (if current-prefix-arg
186 (read-number "Depth: " 1)
187 1))))
188 (magit-clone-internal repository directory
189 (cons (format "--depth=%s" depth) args)))
190
191 ;;;###autoload
192 (defun magit-clone-shallow-since (repository directory args date)
193 "Create a shallow clone of REPOSITORY in DIRECTORY.
194 Then show the status buffer for the new repository.
195 Exclude commits before DATE, which is read from the
196 user."
197 (interactive (append (magit-clone-read-args)
198 (list (transient-read-date "Exclude commits before: "
199 nil nil))))
200 (magit-clone-internal repository directory
201 (cons (format "--shallow-since=%s" date) args)))
202
203 ;;;###autoload
204 (defun magit-clone-shallow-exclude (repository directory args exclude)
205 "Create a shallow clone of REPOSITORY in DIRECTORY.
206 Then show the status buffer for the new repository.
207 Exclude commits reachable from EXCLUDE, which is a
208 branch or tag read from the user."
209 (interactive (append (magit-clone-read-args)
210 (list (read-string "Exclude commits reachable from: "))))
211 (magit-clone-internal repository directory
212 (cons (format "--shallow-exclude=%s" exclude) args)))
213
214 ;;;###autoload
215 (defun magit-clone-bare (repository directory args)
216 "Create a bare clone of REPOSITORY in DIRECTORY.
217 Then show the status buffer for the new repository."
218 (interactive (magit-clone-read-args))
219 (magit-clone-internal repository directory (cons "--bare" args)))
220
221 ;;;###autoload
222 (defun magit-clone-mirror (repository directory args)
223 "Create a mirror of REPOSITORY in DIRECTORY.
224 Then show the status buffer for the new repository."
225 (interactive (magit-clone-read-args))
226 (magit-clone-internal repository directory (cons "--mirror" args)))
227
228 ;;;###autoload
229 (defun magit-clone-sparse (repository directory args)
230 "Clone REPOSITORY into DIRECTORY and create a sparse checkout."
231 (interactive (magit-clone-read-args))
232 (magit-clone-internal repository directory (cons "--no-checkout" args)
233 'sparse))
234
235 (defun magit-clone-internal (repository directory args &optional sparse)
236 (let* ((checkout (not (member (car args) '("--bare" "--mirror"))))
237 (remote (or (transient-arg-value "--origin=" args)
238 (magit-get "clone.defaultRemote")
239 "origin"))
240 (set-push-default
241 (and checkout
242 (or (eq magit-clone-set-remote.pushDefault t)
243 (and magit-clone-set-remote.pushDefault
244 (y-or-n-p (format "Set `remote.pushDefault' to %S? "
245 remote)))))))
246 (run-hooks 'magit-credential-hook)
247 (setq directory (file-name-as-directory (expand-file-name directory)))
248 (when (file-exists-p directory)
249 (if (file-directory-p directory)
250 (when (length> (directory-files directory) 2)
251 (let ((name (magit-clone--url-to-name repository)))
252 (unless (and name
253 (setq directory (file-name-as-directory
254 (expand-file-name name directory)))
255 (not (file-exists-p directory)))
256 (user-error "%s already exists" directory))))
257 (user-error "%s already exists and is not a directory" directory)))
258 (magit-run-git-async "clone" args "--" repository
259 (magit-convert-filename-for-git directory))
260 ;; Don't refresh the buffer we're calling from.
261 (process-put magit-this-process 'inhibit-refresh t)
262 (set-process-sentinel
263 magit-this-process
264 (lambda (process event)
265 (when (memq (process-status process) '(exit signal))
266 (let ((magit-process-raise-error t))
267 (magit-process-sentinel process event)))
268 (when (and (eq (process-status process) 'exit)
269 (= (process-exit-status process) 0))
270 (when checkout
271 (let ((default-directory directory))
272 (when set-push-default
273 (setf (magit-get "remote.pushDefault") remote))
274 (unless magit-clone-set-remote-head
275 (magit-remote-unset-head remote))))
276 (when (and sparse checkout)
277 (let ((default-directory directory))
278 (magit-call-git "sparse-checkout" "init" "--cone")
279 (magit-call-git "checkout" (magit-get-current-branch))))
280 (let ((default-directory directory))
281 (run-hooks 'magit-post-clone-hook))
282 (with-current-buffer (process-get process 'command-buf)
283 (magit-status-setup-buffer directory)))))))
284
285 (defun magit-clone-read-args ()
286 (let ((repo (magit-clone-read-repository)))
287 (list repo
288 (read-directory-name
289 "Clone to: "
290 (if (functionp magit-clone-default-directory)
291 (funcall magit-clone-default-directory repo)
292 magit-clone-default-directory)
293 nil nil
294 (magit-clone--url-to-name repo))
295 (transient-args 'magit-clone))))
296
297 (defun magit-clone-read-repository ()
298 (magit-read-char-case "Clone from " nil
299 (?u "[u]rl or name"
300 (let ((str (magit-read-string-ns "Clone from url or name")))
301 (if (string-match-p "\\(://\\|@\\)" str)
302 str
303 (magit-clone--name-to-url str))))
304 (?p "[p]ath"
305 (magit-convert-filename-for-git
306 (read-directory-name "Clone repository: ")))
307 (?l "[l]ocal url"
308 (concat "file://"
309 (magit-convert-filename-for-git
310 (read-directory-name "Clone repository: file://"))))
311 (?b "[b]undle"
312 (magit-convert-filename-for-git
313 (read-file-name "Clone from bundle: ")))))
314
315 (defun magit-clone--url-to-name (url)
316 (and (string-match "\\([^/:]+?\\)\\(/?\\.git\\)?$" url)
317 (match-str 1 url)))
318
319 (defun magit-clone--name-to-url (name)
320 (or (seq-some
321 (pcase-lambda (`(,re ,host ,user))
322 (and (string-match re name)
323 (let ((repo (match-str 1 name)))
324 (magit-clone--format-url host user repo))))
325 magit-clone-name-alist)
326 (user-error "Not an url and no matching entry in `%s'"
327 'magit-clone-name-alist)))
328
329 (defun magit-clone--format-url (host user repo)
330 (if-let ((url-format
331 (cond ((listp magit-clone-url-format)
332 (cdr (or (assoc host magit-clone-url-format)
333 (assoc t magit-clone-url-format))))
334 ((stringp magit-clone-url-format)
335 magit-clone-url-format))))
336 (format-spec
337 url-format
338 `((?h . ,host)
339 (?n . ,(cond
340 ((string-search "/" repo) repo)
341 ((string-search "." user)
342 (if-let ((user (magit-get user)))
343 (concat user "/" repo)
344 (user-error "Set %S or specify owner explicitly" user)))
345 ((concat user "/" repo))))))
346 (user-error
347 "Bogus `magit-clone-url-format' (bad type or missing default)")))
348
349 ;;; _
350 (provide 'magit-clone)
351 ;; Local Variables:
352 ;; read-symbol-shorthands: (
353 ;; ("and$" . "cond-let--and$")
354 ;; ("thread$" . "cond-let--thread$")
355 ;; ("when$" . "cond-let--when$")
356 ;; ("and-let*" . "cond-let--and-let*")
357 ;; ("and-let" . "cond-let--and-let")
358 ;; ("if-let*" . "cond-let--if-let*")
359 ;; ("if-let" . "cond-let--if-let")
360 ;; ("when-let*" . "cond-let--when-let*")
361 ;; ("when-let" . "cond-let--when-let")
362 ;; ("while-let*" . "cond-let--while-let*")
363 ;; ("while-let" . "cond-let--while-let")
364 ;; ("match-string" . "match-string")
365 ;; ("match-str" . "match-string-no-properties"))
366 ;; End:
367 ;;; magit-clone.el ends here