;;; magit-remote.el --- Transfer Git commits -*- lexical-binding:t -*- ;; Copyright (C) 2008-2022 The Magit Project Contributors ;; Author: Jonas Bernoulli ;; Maintainer: Jonas Bernoulli ;; 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 . ;;; Commentary: ;; This library implements remote commands. ;;; Code: (require 'magit) ;;; Options (defcustom magit-remote-add-set-remote.pushDefault 'ask-if-unset "Whether to set the value of `remote.pushDefault' after adding a remote. If `ask', then always ask. If `ask-if-unset', then ask, but only if the variable isn't set already. If nil, then don't ever set. If the value is a string, then set without asking, provided that the name of the added remote is equal to that string and the variable isn't already set." :package-version '(magit . "2.4.0") :group 'magit-commands :type '(choice (const :tag "ask if unset" ask-if-unset) (const :tag "always ask" ask) (string :tag "set if named") (const :tag "don't set"))) (defcustom magit-remote-direct-configure t "Whether the command `magit-remote' shows Git variables. When set to nil, no variables are displayed by this transient command, instead the sub-transient `magit-remote-configure' has to be used to view and change remote related variables." :package-version '(magit . "2.12.0") :group 'magit-commands :type 'boolean) (defcustom magit-prefer-push-default nil "Whether to prefer `remote.pushDefault' over per-branch variables." :package-version '(magit . "3.0.0") :group 'magit-commands :type 'boolean) ;;; Commands ;;;###autoload (autoload 'magit-remote "magit-remote" nil t) (transient-define-prefix magit-remote (remote) "Add, configure or remove a remote." :man-page "git-remote" :value '("-f") ["Variables" :if (lambda () (and magit-remote-direct-configure (oref transient--prefix scope))) ("u" magit-remote..url) ("U" magit-remote..fetch) ("s" magit-remote..pushurl) ("S" magit-remote..push) ("O" magit-remote..tagopt)] ["Arguments for add" ("-f" "Fetch after add" "-f")] ["Actions" [("a" "Add" magit-remote-add) ("r" "Rename" magit-remote-rename) ("k" "Remove" magit-remote-remove)] [("C" "Configure..." magit-remote-configure) ("p" "Prune stale branches" magit-remote-prune) ("P" "Prune stale refspecs" magit-remote-prune-refspecs) (7 "z" "Unshallow remote" magit-remote-unshallow)]] (interactive (list (magit-get-current-remote))) (transient-setup 'magit-remote nil nil :scope remote)) (defun magit-read-url (prompt &optional initial-input) (let ((url (magit-read-string-ns prompt initial-input))) (if (string-prefix-p "~" url) (expand-file-name url) url))) ;;;###autoload (defun magit-remote-add (remote url &optional args) "Add a remote named REMOTE and fetch it." (interactive (let ((origin (magit-get "remote.origin.url")) (remote (magit-read-string-ns "Remote name"))) (list remote (magit-read-url "Remote url" (and origin (string-match "\\([^:/]+\\)/[^/]+\\(\\.git\\)?\\'" origin) (replace-match remote t t origin 1))) (transient-args 'magit-remote)))) (if (pcase (list magit-remote-add-set-remote.pushDefault (magit-get "remote.pushDefault")) (`(,(pred stringp) ,_) t) ((or `(ask ,_) '(ask-if-unset nil)) (y-or-n-p (format "Set `remote.pushDefault' to \"%s\"? " remote)))) (progn (magit-call-git "remote" "add" args remote url) (setf (magit-get "remote.pushDefault") remote) (magit-refresh)) (magit-run-git-async "remote" "add" args remote url))) ;;;###autoload (defun magit-remote-rename (old new) "Rename the remote named OLD to NEW." (interactive (let ((remote (magit-read-remote "Rename remote"))) (list remote (magit-read-string-ns (format "Rename %s to" remote))))) (unless (string= old new) (magit-call-git "remote" "rename" old new) (magit-remote--cleanup-push-variables old new) (magit-refresh))) ;;;###autoload (defun magit-remote-remove (remote) "Delete the remote named REMOTE." (interactive (list (magit-read-remote "Delete remote"))) (magit-call-git "remote" "rm" remote) (magit-remote--cleanup-push-variables remote) (magit-refresh)) (defun magit-remote--cleanup-push-variables (remote &optional new-name) (magit-with-toplevel (when (equal (magit-get "remote.pushDefault") remote) (magit-set new-name "remote.pushDefault")) (dolist (var (magit-git-lines "config" "--name-only" "--get-regexp" "^branch\.[^.]*\.pushRemote" (format "^%s$" remote))) (magit-call-git "config" (and (not new-name) "--unset") var new-name)))) (defconst magit--refspec-re "\\`\\(\\+\\)?\\([^:]+\\):\\(.*\\)\\'") ;;;###autoload (defun magit-remote-prune (remote) "Remove stale remote-tracking branches for REMOTE." (interactive (list (magit-read-remote "Prune stale branches of remote"))) (magit-run-git-async "remote" "prune" remote)) ;;;###autoload (defun magit-remote-prune-refspecs (remote) "Remove stale refspecs for REMOTE. A refspec is stale if there no longer exists at least one branch on the remote that would be fetched due to that refspec. A stale refspec is problematic because its existence causes Git to refuse to fetch according to the remaining non-stale refspecs. If only stale refspecs remain, then offer to either delete the remote or to replace the stale refspecs with the default refspec. Also remove the remote-tracking branches that were created due to the now stale refspecs. Other stale branches are not removed." (interactive (list (magit-read-remote "Prune refspecs of remote"))) (let* ((tracking-refs (magit-list-remote-branches remote)) (remote-refs (magit-remote-list-refs remote)) (variable (format "remote.%s.fetch" remote)) (refspecs (magit-get-all variable)) stale) (dolist (refspec refspecs) (when (string-match magit--refspec-re refspec) (let ((theirs (match-string 2 refspec)) (ours (match-string 3 refspec))) (unless (if (string-match "\\*" theirs) (let ((re (replace-match ".*" t t theirs))) (--some (string-match-p re it) remote-refs)) (member theirs remote-refs)) (push (cons refspec (if (string-match "\\*" ours) (let ((re (replace-match ".*" t t ours))) (--filter (string-match-p re it) tracking-refs)) (list (car (member ours tracking-refs))))) stale))))) (if (not stale) (message "No stale refspecs for remote %S" remote) (if (= (length stale) (length refspecs)) (magit-read-char-case (format "All of %s's refspecs are stale. " remote) nil (?s "replace with [d]efault refspec" (magit-set-all (list (format "+refs/heads/*:refs/remotes/%s/*" remote)) variable)) (?r "[r]emove remote" (magit-call-git "remote" "rm" remote)) (?a "or [a]abort" (user-error "Abort"))) (if (if (length= stale 1) (pcase-let ((`(,refspec . ,refs) (car stale))) (magit-confirm 'prune-stale-refspecs (format "Prune stale refspec %s and branch %%s" refspec) (format "Prune stale refspec %s and %%i branches" refspec) nil refs)) (magit-confirm 'prune-stale-refspecs nil (format "Prune %%i stale refspecs and %i branches" (length (cl-mapcan (lambda (s) (copy-sequence (cdr s))) stale))) nil (mapcar (pcase-lambda (`(,refspec . ,refs)) (concat refspec "\n" (mapconcat (lambda (b) (concat " " b)) refs "\n"))) stale))) (pcase-dolist (`(,refspec . ,refs) stale) (magit-call-git "config" "--unset" variable (regexp-quote refspec)) (magit--log-action (lambda (refs) (format "Deleting %i branches" (length refs))) (lambda (ref) (format "Deleting branch %s (was %s)" ref (magit-rev-parse "--short" ref))) refs) (dolist (ref refs) (magit-call-git "update-ref" "-d" ref))) (user-error "Abort"))) (magit-refresh)))) ;;;###autoload (defun magit-remote-set-head (remote &optional branch) "Set the local representation of REMOTE's default branch. Query REMOTE and set the symbolic-ref refs/remotes//HEAD accordingly. With a prefix argument query for the branch to be used, which allows you to select an incorrect value if you fancy doing that." (interactive (let ((remote (magit-read-remote "Set HEAD for remote"))) (list remote (and current-prefix-arg (magit-read-remote-branch (format "Set %s/HEAD to" remote) remote nil nil t))))) (magit-run-git "remote" "set-head" remote (or branch "--auto"))) ;;;###autoload (defun magit-remote-unset-head (remote) "Unset the local representation of REMOTE's default branch. Delete the symbolic-ref \"refs/remotes//HEAD\"." (interactive (list (magit-read-remote "Unset HEAD for remote"))) (magit-run-git "remote" "set-head" remote "--delete")) ;;;###autoload (defun magit-remote-unshallow (remote) "Convert a shallow remote into a full one. If only a single refspec is set and it does not contain a wildcard, then also offer to replace it with the standard refspec." (interactive (list (or (magit-get-current-remote) (magit-read-remote "Delete remote")))) (let ((refspecs (magit-get-all "remote" remote "fetch")) (standard (format "+refs/heads/*:refs/remotes/%s/*" remote))) (when (and (length= refspecs 1) (not (string-search "*" (car refspecs))) (yes-or-no-p (format "Also replace refspec %s with %s? " (car refspecs) standard))) (magit-set standard "remote" remote "fetch")) (magit-git-fetch "--unshallow" remote))) ;;; Configure ;;;###autoload (autoload 'magit-remote-configure "magit-remote" nil t) (transient-define-prefix magit-remote-configure (remote) "Configure a remote." :man-page "git-remote" [:description (lambda () (concat (propertize "Configure " 'face 'transient-heading) (propertize (oref transient--prefix scope) 'face 'magit-branch-remote))) ("u" magit-remote..url) ("U" magit-remote..fetch) ("s" magit-remote..pushurl) ("S" magit-remote..push) ("O" magit-remote..tagopt)] (interactive (list (or (and (not current-prefix-arg) (not (and magit-remote-direct-configure (eq transient-current-command 'magit-remote))) (magit-get-current-remote)) (magit--read-remote-scope)))) (transient-setup 'magit-remote-configure nil nil :scope remote)) (defun magit--read-remote-scope (&optional obj) (magit-read-remote (if obj (format "Set %s for remote" (format (oref obj variable) "")) "Configure remote"))) (transient-define-infix magit-remote..url () :class 'magit--git-variable:urls :scope #'magit--read-remote-scope :variable "remote.%s.url" :multi-value t :history-key 'magit-remote..*url) (transient-define-infix magit-remote..fetch () :class 'magit--git-variable :scope #'magit--read-remote-scope :variable "remote.%s.fetch" :multi-value t) (transient-define-infix magit-remote..pushurl () :class 'magit--git-variable:urls :scope #'magit--read-remote-scope :variable "remote.%s.pushurl" :multi-value t :history-key 'magit-remote..*url :seturl-arg "--push") (transient-define-infix magit-remote..push () :class 'magit--git-variable :scope #'magit--read-remote-scope :variable "remote.%s.push") (transient-define-infix magit-remote..tagopt () :class 'magit--git-variable:choices :scope #'magit--read-remote-scope :variable "remote.%s.tagOpt" :choices '("--no-tags" "--tags")) ;;; Transfer Utilities (defun magit--push-remote-variable (&optional branch short) (unless branch (setq branch (magit-get-current-branch))) (magit--propertize-face (if (or (not branch) magit-prefer-push-default) (if short "pushDefault" "remote.pushDefault") (if short "pushRemote" (format "branch.%s.pushRemote" branch))) 'bold)) (defun magit--select-push-remote (prompt-suffix) (let* ((branch (or (magit-get-current-branch) (user-error "No branch is checked out"))) (remote (magit-get-push-remote branch)) (changed nil)) (when (or current-prefix-arg (not remote) (not (member remote (magit-list-remotes)))) (setq changed t) (setq remote (magit-read-remote (format "Set %s and %s" (magit--push-remote-variable) prompt-suffix))) (setf (magit-get (magit--push-remote-variable branch)) remote)) (list branch remote changed))) ;;; _ (provide 'magit-remote) ;;; magit-remote.el ends here