328 lines
13 KiB
EmacsLisp
328 lines
13 KiB
EmacsLisp
|
;;; magit-clone.el --- Clone a repository -*- lexical-binding:t -*-
|
||
|
|
||
|
;; Copyright (C) 2008-2022 The Magit Project Contributors
|
||
|
|
||
|
;; Author: Jonas Bernoulli <jonas@bernoul.li>
|
||
|
;; Maintainer: Jonas Bernoulli <jonas@bernoul.li>
|
||
|
|
||
|
;; SPDX-License-Identifier: GPL-3.0-or-later
|
||
|
|
||
|
;; Magit 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 3 of the License, or
|
||
|
;; (at your option) any later version.
|
||
|
;;
|
||
|
;; Magit 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 Magit. If not, see <https://www.gnu.org/licenses/>.
|
||
|
|
||
|
;;; Commentary:
|
||
|
|
||
|
;; This library implements clone commands.
|
||
|
|
||
|
;;; Code:
|
||
|
|
||
|
(require 'magit)
|
||
|
|
||
|
;;; Options
|
||
|
|
||
|
(defcustom magit-clone-set-remote-head nil
|
||
|
"Whether cloning creates the symbolic-ref `<remote>/HEAD'."
|
||
|
:package-version '(magit . "2.4.2")
|
||
|
:group 'magit-commands
|
||
|
:type 'boolean)
|
||
|
|
||
|
(defcustom magit-clone-set-remote.pushDefault 'ask
|
||
|
"Whether to set the value of `remote.pushDefault' after cloning.
|
||
|
|
||
|
If t, then set without asking. If nil, then don't set. If
|
||
|
`ask', then ask."
|
||
|
:package-version '(magit . "2.4.0")
|
||
|
:group 'magit-commands
|
||
|
:type '(choice (const :tag "set" t)
|
||
|
(const :tag "ask" ask)
|
||
|
(const :tag "don't set" nil)))
|
||
|
|
||
|
(defcustom magit-clone-default-directory nil
|
||
|
"Default directory to use when `magit-clone' reads destination.
|
||
|
If nil (the default), then use the value of `default-directory'.
|
||
|
If a directory, then use that. If a function, then call that
|
||
|
with the remote url as only argument and use the returned value."
|
||
|
:package-version '(magit . "2.90.0")
|
||
|
:group 'magit-commands
|
||
|
:type '(choice (const :tag "value of default-directory")
|
||
|
(directory :tag "constant directory")
|
||
|
(function :tag "function's value")))
|
||
|
|
||
|
(defcustom magit-clone-always-transient nil
|
||
|
"Whether `magit-clone' always acts as a transient prefix command.
|
||
|
If nil, then a prefix argument has to be used to show the transient
|
||
|
popup instead of invoking the default suffix `magit-clone-regular'
|
||
|
directly."
|
||
|
:package-version '(magit . "3.0.0")
|
||
|
:group 'magit-commands
|
||
|
:type 'boolean)
|
||
|
|
||
|
(defcustom magit-clone-name-alist
|
||
|
'(("\\`\\(?:github:\\|gh:\\)?\\([^:]+\\)\\'" "github.com" "github.user")
|
||
|
("\\`\\(?:gitlab:\\|gl:\\)\\([^:]+\\)\\'" "gitlab.com" "gitlab.user"))
|
||
|
"Alist mapping repository names to repository urls.
|
||
|
|
||
|
Each element has the form (REGEXP HOSTNAME USER). When the user
|
||
|
enters a name when a cloning command asks for a name or url, then
|
||
|
that is looked up in this list. The first element whose REGEXP
|
||
|
matches is used.
|
||
|
|
||
|
The format specified by option `magit-clone-url-format' is used
|
||
|
to turn the name into an url, using HOSTNAME and the repository
|
||
|
name. If the provided name contains a slash, then that is used.
|
||
|
Otherwise if the name omits the owner of the repository, then the
|
||
|
default user specified in the matched entry is used.
|
||
|
|
||
|
If USER contains a dot, then it is treated as a Git variable and
|
||
|
the value of that is used as the username. Otherwise it is used
|
||
|
as the username itself."
|
||
|
:package-version '(magit . "3.0.0")
|
||
|
:group 'magit-commands
|
||
|
:type '(repeat (list regexp
|
||
|
(string :tag "hostname")
|
||
|
(string :tag "user name or git variable"))))
|
||
|
|
||
|
(defcustom magit-clone-url-format "git@%h:%n.git"
|
||
|
"Format used when turning repository names into urls.
|
||
|
%h is the hostname and %n is the repository name, including
|
||
|
the name of the owner. Also see `magit-clone-name-alist'."
|
||
|
:package-version '(magit . "3.0.0")
|
||
|
:group 'magit-commands
|
||
|
:type 'regexp)
|
||
|
|
||
|
;;; Commands
|
||
|
|
||
|
;;;###autoload (autoload 'magit-clone "magit-clone" nil t)
|
||
|
(transient-define-prefix magit-clone (&optional transient)
|
||
|
"Clone a repository."
|
||
|
:man-page "git-clone"
|
||
|
["Fetch arguments"
|
||
|
("-B" "Clone a single branch" "--single-branch")
|
||
|
("-n" "Do not clone tags" "--no-tags")
|
||
|
("-S" "Clones submodules" "--recurse-submodules" :level 6)
|
||
|
("-l" "Do not optimize" "--no-local" :level 7)]
|
||
|
["Setup arguments"
|
||
|
("-o" "Set name of remote" ("-o" "--origin="))
|
||
|
("-b" "Set HEAD branch" ("-b" "--branch="))
|
||
|
(magit-clone:--filter
|
||
|
:if (lambda () (magit-git-version>= "2.17.0"))
|
||
|
:level 7)
|
||
|
("-g" "Separate git directory" "--separate-git-dir="
|
||
|
transient-read-directory :level 7)
|
||
|
("-t" "Use template directory" "--template="
|
||
|
transient-read-existing-directory :level 6)]
|
||
|
["Local sharing arguments"
|
||
|
("-s" "Share objects" ("-s" "--shared" :level 7))
|
||
|
("-h" "Do not use hardlinks" "--no-hardlinks")]
|
||
|
["Clone"
|
||
|
("C" "regular" magit-clone-regular)
|
||
|
("s" "shallow" magit-clone-shallow)
|
||
|
("d" "shallow since date" magit-clone-shallow-since :level 7)
|
||
|
("e" "shallow excluding" magit-clone-shallow-exclude :level 7)
|
||
|
(">" "sparse checkout" magit-clone-sparse
|
||
|
:if (lambda () (magit-git-version>= "2.25.0"))
|
||
|
:level 6)
|
||
|
("b" "bare" magit-clone-bare)
|
||
|
("m" "mirror" magit-clone-mirror)]
|
||
|
(interactive (list (or magit-clone-always-transient current-prefix-arg)))
|
||
|
(if transient
|
||
|
(transient-setup 'magit-clone)
|
||
|
(call-interactively #'magit-clone-regular)))
|
||
|
|
||
|
(transient-define-argument magit-clone:--filter ()
|
||
|
:description "Filter some objects"
|
||
|
:class 'transient-option
|
||
|
:key "-f"
|
||
|
:argument "--filter="
|
||
|
:reader #'magit-clone-read-filter)
|
||
|
|
||
|
(defun magit-clone-read-filter (prompt initial-input history)
|
||
|
(magit-completing-read prompt
|
||
|
(list "blob:none" "tree:0")
|
||
|
nil nil initial-input history))
|
||
|
|
||
|
;;;###autoload
|
||
|
(defun magit-clone-regular (repository directory args)
|
||
|
"Create a clone of REPOSITORY in DIRECTORY.
|
||
|
Then show the status buffer for the new repository."
|
||
|
(interactive (magit-clone-read-args))
|
||
|
(magit-clone-internal repository directory args))
|
||
|
|
||
|
;;;###autoload
|
||
|
(defun magit-clone-shallow (repository directory args depth)
|
||
|
"Create a shallow clone of REPOSITORY in DIRECTORY.
|
||
|
Then show the status buffer for the new repository.
|
||
|
With a prefix argument read the DEPTH of the clone;
|
||
|
otherwise use 1."
|
||
|
(interactive (append (magit-clone-read-args)
|
||
|
(list (if current-prefix-arg
|
||
|
(read-number "Depth: " 1)
|
||
|
1))))
|
||
|
(magit-clone-internal repository directory
|
||
|
(cons (format "--depth=%s" depth) args)))
|
||
|
|
||
|
;;;###autoload
|
||
|
(defun magit-clone-shallow-since (repository directory args date)
|
||
|
"Create a shallow clone of REPOSITORY in DIRECTORY.
|
||
|
Then show the status buffer for the new repository.
|
||
|
Exclude commits before DATE, which is read from the
|
||
|
user."
|
||
|
(interactive (append (magit-clone-read-args)
|
||
|
(list (transient-read-date "Exclude commits before: "
|
||
|
nil nil))))
|
||
|
(magit-clone-internal repository directory
|
||
|
(cons (format "--shallow-since=%s" date) args)))
|
||
|
|
||
|
;;;###autoload
|
||
|
(defun magit-clone-shallow-exclude (repository directory args exclude)
|
||
|
"Create a shallow clone of REPOSITORY in DIRECTORY.
|
||
|
Then show the status buffer for the new repository.
|
||
|
Exclude commits reachable from EXCLUDE, which is a
|
||
|
branch or tag read from the user."
|
||
|
(interactive (append (magit-clone-read-args)
|
||
|
(list (read-string "Exclude commits reachable from: "))))
|
||
|
(magit-clone-internal repository directory
|
||
|
(cons (format "--shallow-exclude=%s" exclude) args)))
|
||
|
|
||
|
;;;###autoload
|
||
|
(defun magit-clone-bare (repository directory args)
|
||
|
"Create a bare clone of REPOSITORY in DIRECTORY.
|
||
|
Then show the status buffer for the new repository."
|
||
|
(interactive (magit-clone-read-args))
|
||
|
(magit-clone-internal repository directory (cons "--bare" args)))
|
||
|
|
||
|
;;;###autoload
|
||
|
(defun magit-clone-mirror (repository directory args)
|
||
|
"Create a mirror of REPOSITORY in DIRECTORY.
|
||
|
Then show the status buffer for the new repository."
|
||
|
(interactive (magit-clone-read-args))
|
||
|
(magit-clone-internal repository directory (cons "--mirror" args)))
|
||
|
|
||
|
;;;###autoload
|
||
|
(defun magit-clone-sparse (repository directory args)
|
||
|
"Clone REPOSITORY into DIRECTORY and create a sparse checkout."
|
||
|
(interactive (magit-clone-read-args))
|
||
|
(magit-clone-internal repository directory (cons "--no-checkout" args)
|
||
|
'sparse))
|
||
|
|
||
|
(defun magit-clone-internal (repository directory args &optional sparse)
|
||
|
(let* ((checkout (not (memq (car args) '("--bare" "--mirror"))))
|
||
|
(remote (or (transient-arg-value "--origin" args)
|
||
|
(magit-get "clone.defaultRemote")
|
||
|
"origin"))
|
||
|
(set-push-default
|
||
|
(and checkout
|
||
|
(or (eq magit-clone-set-remote.pushDefault t)
|
||
|
(and magit-clone-set-remote.pushDefault
|
||
|
(y-or-n-p (format "Set `remote.pushDefault' to %S? "
|
||
|
remote)))))))
|
||
|
(run-hooks 'magit-credential-hook)
|
||
|
(setq directory (file-name-as-directory (expand-file-name directory)))
|
||
|
(when (file-exists-p directory)
|
||
|
(if (file-directory-p directory)
|
||
|
(when (length> (directory-files directory) 2)
|
||
|
(let ((name (magit-clone--url-to-name repository)))
|
||
|
(unless (and name
|
||
|
(setq directory (file-name-as-directory
|
||
|
(expand-file-name name directory)))
|
||
|
(not (file-exists-p directory)))
|
||
|
(user-error "%s already exists" directory))))
|
||
|
(user-error "%s already exists and is not a directory" directory)))
|
||
|
(magit-run-git-async "clone" args "--" repository
|
||
|
(magit-convert-filename-for-git directory))
|
||
|
;; Don't refresh the buffer we're calling from.
|
||
|
(process-put magit-this-process 'inhibit-refresh t)
|
||
|
(set-process-sentinel
|
||
|
magit-this-process
|
||
|
(lambda (process event)
|
||
|
(when (memq (process-status process) '(exit signal))
|
||
|
(let ((magit-process-raise-error t))
|
||
|
(magit-process-sentinel process event)))
|
||
|
(when (and (eq (process-status process) 'exit)
|
||
|
(= (process-exit-status process) 0))
|
||
|
(when checkout
|
||
|
(let ((default-directory directory))
|
||
|
(when set-push-default
|
||
|
(setf (magit-get "remote.pushDefault") remote))
|
||
|
(unless magit-clone-set-remote-head
|
||
|
(magit-remote-unset-head remote))))
|
||
|
(when (and sparse checkout)
|
||
|
(when (magit-git-version< "2.25.0")
|
||
|
(user-error
|
||
|
"`git sparse-checkout' not available until Git v2.25"))
|
||
|
(let ((default-directory directory))
|
||
|
(magit-call-git "sparse-checkout" "init" "--cone")
|
||
|
(magit-call-git "checkout" (magit-get-current-branch))))
|
||
|
(with-current-buffer (process-get process 'command-buf)
|
||
|
(magit-status-setup-buffer directory)))))))
|
||
|
|
||
|
(defun magit-clone-read-args ()
|
||
|
(let ((repo (magit-clone-read-repository)))
|
||
|
(list repo
|
||
|
(read-directory-name
|
||
|
"Clone to: "
|
||
|
(if (functionp magit-clone-default-directory)
|
||
|
(funcall magit-clone-default-directory repo)
|
||
|
magit-clone-default-directory)
|
||
|
nil nil
|
||
|
(magit-clone--url-to-name repo))
|
||
|
(transient-args 'magit-clone))))
|
||
|
|
||
|
(defun magit-clone-read-repository ()
|
||
|
(magit-read-char-case "Clone from " nil
|
||
|
(?u "[u]rl or name"
|
||
|
(let ((str (magit-read-string-ns "Clone from url or name")))
|
||
|
(if (string-match-p "\\(://\\|@\\)" str)
|
||
|
str
|
||
|
(magit-clone--name-to-url str))))
|
||
|
(?p "[p]ath"
|
||
|
(magit-convert-filename-for-git
|
||
|
(read-directory-name "Clone repository: ")))
|
||
|
(?l "[l]ocal url"
|
||
|
(concat "file://"
|
||
|
(magit-convert-filename-for-git
|
||
|
(read-directory-name "Clone repository: file://"))))
|
||
|
(?b "or [b]undle"
|
||
|
(magit-convert-filename-for-git
|
||
|
(read-file-name "Clone from bundle: ")))))
|
||
|
|
||
|
(defun magit-clone--url-to-name (url)
|
||
|
(and (string-match "\\([^/:]+?\\)\\(/?\\.git\\)?$" url)
|
||
|
(match-string 1 url)))
|
||
|
|
||
|
(defun magit-clone--name-to-url (name)
|
||
|
(or (seq-some
|
||
|
(pcase-lambda (`(,re ,host ,user))
|
||
|
(and (string-match re name)
|
||
|
(let ((repo (match-string 1 name)))
|
||
|
(magit-clone--format-url host user repo))))
|
||
|
magit-clone-name-alist)
|
||
|
(user-error "Not an url and no matching entry in `%s'"
|
||
|
'magit-clone-name-alist)))
|
||
|
|
||
|
(defun magit-clone--format-url (host user repo)
|
||
|
(format-spec
|
||
|
magit-clone-url-format
|
||
|
`((?h . ,host)
|
||
|
(?n . ,(if (string-search "/" repo)
|
||
|
repo
|
||
|
(if (string-search "." user)
|
||
|
(if-let ((user (magit-get user)))
|
||
|
(concat user "/" repo)
|
||
|
(user-error "Set %S or specify owner explicitly" user))
|
||
|
(concat user "/" repo)))))))
|
||
|
|
||
|
;;; _
|
||
|
(provide 'magit-clone)
|
||
|
;;; magit-clone.el ends here
|