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