diff --git a/lisp/vc/vc-git.el b/lisp/vc/vc-git.el index 5dcd0b998c5..67fe164bfd7 100644 --- a/lisp/vc/vc-git.el +++ b/lisp/vc/vc-git.el @@ -772,8 +772,9 @@ or an empty string if none." (vc-git--out-match '("symbolic-ref" "HEAD") "^\\(refs/heads/\\)?\\(.+\\)$" 2)) -(defun vc-git--branch-remotes () - "Return alist of configured remote branches for current branch. +(defun vc-git--branch-remotes (&optional branch) + "Return alist of configured remote branches for BRANCH. +BRANCH defaults to the current branch. If there is a configured upstream, return the remote-tracking branch with key `upstream'. If there is a distinct configured push remote, return the remote-tracking branch there with key `push'. @@ -783,7 +784,7 @@ ignored because that means we're not actually in a triangular workflow." ;; an unwanted dependency on the setting of push.default. (cl-flet ((get (key) (string-trim-right (vc-git--out-str "config" key)))) - (let* ((branch (vc-git-working-branch)) + (let* ((branch (or branch (vc-git-working-branch))) (pull (get (format "branch.%s.remote" branch))) (merge (string-remove-prefix "refs/heads/" (get (format "branch.%s.merge" @@ -799,12 +800,14 @@ ignored because that means we're not actually in a triangular workflow." alist (cl-acons 'push (format "%s/%s" push branch) alist))))) -(defun vc-git-trunk-or-topic-p () +(defun vc-git-trunk-or-topic-p (&optional branch) "Return `topic' if branch has distinct pull and push remotes, else nil. This is able to identify topic branches for certain forge workflows." - (let ((remotes (vc-git--branch-remotes))) + (let ((remotes (vc-git--branch-remotes branch))) (and (assq 'upstream remotes) (assq 'push remotes) 'topic))) +(declare-function vc-trunk-or-topic-p "vc") + (defun vc-git-topic-outgoing-base () "Return the outgoing base for the current branch as a string. This works by considering the current branch as a topic branch @@ -815,9 +818,10 @@ for outstanding changes is the tracking branch, and return that. Otherwise, fall back to the following algorithm, which requires that the corresponding trunk exists as a local branch. Find all merge bases -between the current branch and other local branches. Each of these is a -commit on the current branch. Use `git merge-base --independent' on -them all to find the topologically most recent. Take the branch for +between the current branch and either all local trunks, if there are any +positively identified trunks, or just all local branches. Each of these +is a commit on the current branch. Use `git merge-base --independent' +on them all to find the topologically most recent. Take the branch for which that commit is a merge base with the current branch to be the branch into which the current branch will eventually be merged. Find its upstream. (If there is more than one branch whose merge base with @@ -832,6 +836,13 @@ them one-by-one, accepting the first that has an upstream.)" ;; Fallback algorithm. ((bind* (branches (vc-git-branches)) (current (car branches)) + ;; If there are any branches we can positively identify as + ;; trunks, consider only those. + (trunks (cl-remove-if-not (lambda (b) + (eq (vc-trunk-or-topic-p b 'Git) + 'trunk)) + branches)) + (branches (or trunks branches)) raw-merge-bases) (with-temp-buffer (cl-labels @@ -839,7 +850,9 @@ them one-by-one, accepting the first that has an upstream.)" (buffer-substring (point) (pos-eol))) (git-merge-base (commit1 commit2) ;; Return all merge bases between COMMIT1 and COMMIT2. - ;; Memoized to reduce shelling out to Git. + ;; Memoized to reduce shelling out to Git + ;; (that doesn't actually help at present, but may be + ;; useful in the future). (let* ((sorted (sort (list commit1 commit2))) ;; Git branch names may not contain "..". (key (string-join sorted "..")) diff --git a/lisp/vc/vc-hg.el b/lisp/vc/vc-hg.el index 90e25ba43f4..296090d4334 100644 --- a/lisp/vc/vc-hg.el +++ b/lisp/vc/vc-hg.el @@ -1962,9 +1962,23 @@ The return value is always a string." (let ((alist (vc-hg--working-branch))) (cdr (or (assq 'bookmark alist) (assq 'branch alist))))) -(defun vc-hg-trunk-or-topic-p () - "Return `topic' if there is a currently active bookmark, else nil." - (and (assq 'bookmark (vc-hg--working-branch)) 'topic)) +(defun vc-hg--bookmarks () + "Return list of strings naming all bookmarks." + (let (res) + (with-temp-buffer + (vc-hg-command t nil nil "bookmark" "--list") + (goto-char (point-min)) + (while (re-search-forward "^ [ *] \\(\\S-+\\)" nil t) + (push (match-string 1) res))) + res)) + +(defun vc-hg-trunk-or-topic-p (&optional branch) + "Return `topic' or nil for BRANCH or the currently active bookmark. +If BRANCH names a bookmark, or BRANCH is nil but there is a currently +active bookmark, return `topic'. Otherwise return nil." + (if branch + (member branch (vc-hg--bookmarks)) + (and (assq 'bookmark (vc-hg--working-branch)) 'topic))) (defun vc-hg-topic-outgoing-base () "Return outgoing base for current commit considered as a topic branch. diff --git a/lisp/vc/vc.el b/lisp/vc/vc.el index 5809b5b15ae..85f059df573 100644 --- a/lisp/vc/vc.el +++ b/lisp/vc/vc.el @@ -615,17 +615,17 @@ ;; ;; Return the name of the current branch, if there is one, else nil. ;; -;; - trunk-or-topic-p () +;; - trunk-or-topic-p (&optional branch) ;; ;; For the current branch, or the closest equivalent for a VCS without -;; named branches, return `trunk' if it is definitely a longer-lived -;; trunk branch, `topic' if it is definitely a shorter-lived topic -;; branch, or nil if no general determination can be made. +;; named branches, or the branch named BRANCH if non-nil, return +;; `trunk' if it is definitely a longer-lived trunk branch, `topic' if +;; it is definitely a shorter-lived topic branch, or nil if no general +;; determination can be made. ;; ;; What counts as a longer-lived or shorter-lived branch for VC is ;; explained in Info node `(emacs)Outstanding Changes' and in the -;; docstrings for the `vc-trunk-branch-regexps' and -;; `vc-topic-branch-regexps' user options. +;; docstring for `vc-trunk-or-topic-p'. ;; ;; - topic-outgoing-base () ;; @@ -3178,20 +3178,11 @@ There value can be of one of the following forms: whether a branch is a trunk. Emacs will ask the backend whether it thinks the current branch is a trunk. -In VC, trunk branches are those where you've finished sharing the work -on the branch with your collaborators just as soon as you've checked it -in, and in the case of a decentralized VCS, pushed it. In addition, -typically you never delete trunk branches. - -The specific VCS workflow you are using may only acknowledge a single -trunk, and give other names to kinds of branches which VC would consider -to be just further trunks. - If trunk branches in your project can be identified by name, include regexps matching their names in the value of this variable. This is more reliable than letting Emacs ask the backend. -See also `vc-topic-branch-regexps'." +See also `vc-trunk-or-topic-p' and `vc-topic-branch-regexps'." :type '(choice (repeat :tag "Regexps" string) (cons :tag "Negated regexps" (const not) (repeat :tag "Regexps" string)) @@ -3221,21 +3212,11 @@ There value can be of one of the following forms: whether a branch is a topic branch. Emacs will ask the backend whether it thinks the current branch is a topic branch. -In VC, topic branches are those where checking in work, and pushing it -in the case of a decentralized VCS, is not enough to complete the -process of sharing the changes with your collaborators. In addition, -it's required that you merge the topic branch into another branch. -After this is done, typically you delete the topic branch. - -Topic branches are sometimes called \"feature branches\", though it is -also common for that term to be reserved for only a certain kind of -topic branch. - If topic branches in your project can be identified by name, include regexps matching their names in the value of this variable. This is more reliable than letting Emacs ask the backend. -See also `vc-trunk-branch-regexps'." +See also `vc-trunk-or-topic-p' and `vc-trunk-branch-regexps'." :type '(choice (repeat :tag "Regexps" string) (cons :tag "Negated regexps" (const not) (repeat :tag "Regexps" string)) @@ -3287,6 +3268,51 @@ defcustoms don't decide the matter of which kind of branch this is." (trunk 'trunk) (topic 'topic))))) +(defun vc-trunk-or-topic-p (&optional branch backend) + "Return whether BRANCH, or the current branch, is a trunk or a topic. +Returns `trunk' if BRANCH is definitely a trunk, `topic' if BRANCH is +definitely a topic, or nil if no general determination can be made. + +In VC, trunk branches are those where you've finished sharing the work +on the branch with your collaborators just as soon as you've checked it +in, and in the case of a decentralized VCS, pushed it. In addition, +typically you never delete trunk branches. The specific VCS workflow +you are using may only acknowledge a single trunk, and give other names +to kinds of branches which VC would consider to be just further trunks. + +Topic branches are those where checking in work, and pushing it in the +case of a decentralized VCS, is not enough to complete the process of +sharing the changes with your collaborators. In addition, it's required +that you merge the topic branch into another branch. After this is +done, typically you delete the topic branch. Topic branches are +sometimes called \"feature branches\", though it is also common for that +term to be reserved for only a certain kind of topic branch. + +Determining whether BRANCH is a trunk or a topic proceeds in two stages: +1. If BRANCH is non-nil, compare that name against + `vc-trunk-branch-regexps' and `vc-topic-branch-regexps', which see. + If BRANCH is nil, ask the backend for the name of the current branch. + If the backend returns a name, compare it against + `vc-trunk-branch-regexps' and `vc-topic-branch-regexps'. +2. If that doesn't settle it, either because the backend reports that + the current branch has no name or because comparing the name against + the two regexp defcustoms yields no decisive answer, call the backend's + `trunk-or-topic-p' VC API function. + +If trunk and/or topic branches in your project can be identified by +name, include regexps matching those names in `vc-trunk-branch-regexps' +and/or `vc-topic-branch-regexps' as appropriate. This is more reliable +than letting Emacs ask the backend. + +BACKEND is the VC backend." + (let* ((backend (or backend (vc-deduce-backend))) + (branch (or branch (vc-call-backend backend 'working-branch)))) + (or (and branch (vc--match-branch-name-regexps branch)) + ;; It's okay to pass nil BRANCH here, which is what happens in + ;; case of a VCS without named branches or where the current + ;; branch has no name. + (vc-call-backend backend 'trunk-or-topic-p branch)))) + (defun vc--outgoing-base (backend) "Return an outgoing base for the current branch under VC backend BACKEND. The outgoing base is the upstream location for which outstanding changes @@ -3294,30 +3320,18 @@ on this branch are destined once they are no longer outstanding. There are two stages to determining the outgoing base. First we decide whether we think this is a shorter-lived or a -longer-lived (\"trunk\") branch (see `vc-trunk-branch-regexps' and -`vc-topic-branch-regexps' regarding this distinction), as follows: -1. Ask the backend for the name of the current branch. - If it returns non-nil, compare that name against - `vc-trunk-branch-regexps' and `vc-topic-branch-regexps'. -2. If that doesn't settle it, either because the backend returns nil for - the name of the current branch, or because comparing the name against - the two regexp defcustoms yields no decisive answer, call BACKEND's - `trunk-or-topic-p' VC API function. -3. If that doesn't settle it either, assume this is a shorter-lived - branch. This is based on how it's commands primarily intended for - working with shorter-lived branches that call this function. +longer-lived (\"trunk\") branch by calling `vc-trunk-or-topic-p'. +If that function returns nil, assume this is a shorter-lived branch. +This is based on how it's commands primarily intended for working with +shorter-lived branches that call this function. Second, if we have determined that this is a trunk, return nil, meaning that the outgoing base is the place to which `vc-push' would push. Otherwise, we have determined that this is a shorter-lived branch, and we return the value of calling BACKEND's `topic-outgoing-base' VC API function." ;; For further discussion see bug#80006. - (let* ((branch (vc-call-backend backend 'working-branch)) - (type (or (and branch (vc--match-branch-name-regexps branch)) - (vc-call-backend backend 'trunk-or-topic-p) - 'topic))) - (and (eq type 'topic) - (vc-call-backend backend 'topic-outgoing-base)))) + (and (memq (vc-trunk-or-topic-p nil backend) '(topic nil)) + (vc-call-backend backend 'topic-outgoing-base))) (defun vc--outgoing-base-mergebase (backend &optional upstream-location refresh) "Return, under VC backend BACKEND, the merge base with UPSTREAM-LOCATION. @@ -3350,8 +3364,8 @@ For a trunk branch this is always the place \\[vc-push] would push to. For a topic branch, see whether the branch matches one of `vc-trunk-branch-regexps' or `vc-topic-branch-regexps', or else query the backend for an appropriate outgoing base. -See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding -the difference between trunk and topic branches. +See `vc-trunk-or-topic-p' regarding the difference between trunk and +topic branches. When called interactively with a prefix argument, prompt for UPSTREAM-LOCATION. In some version control systems, UPSTREAM-LOCATION @@ -3380,8 +3394,8 @@ For a trunk branch this is always the place \\[vc-push] would push to. For a topic branch, see whether the branch matches one of `vc-trunk-branch-regexps' or `vc-topic-branch-regexps', or else query the backend for an appropriate outgoing base. -See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding -the difference between trunk and topic branches. +See `vc-trunk-or-topic-p' regarding the difference between trunk and +topic branches. When called interactively with a prefix argument, prompt for UPSTREAM-LOCATION. In some version control systems, UPSTREAM-LOCATION @@ -3416,8 +3430,8 @@ For a trunk branch this is always the place \\[vc-push] would push to. For a topic branch, see whether the branch matches one of `vc-trunk-branch-regexps' or `vc-topic-branch-regexps', or else query the backend for an appropriate outgoing base. -See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding -the difference between trunk and topic branches. +See `vc-trunk-or-topic-p' regarding the difference between trunk and +topic branches. When called interactively with a prefix argument, prompt for UPSTREAM-LOCATION. In some version control systems, UPSTREAM-LOCATION @@ -3450,8 +3464,8 @@ For a trunk branch this is always the place \\[vc-push] would push to. For a topic branch, see whether the branch matches one of `vc-trunk-branch-regexps' or `vc-topic-branch-regexps', or else query the backend for an appropriate outgoing base. -See `vc-trunk-branch-regexps' and `vc-topic-branch-regexps' regarding -the difference between trunk and topic branches. +See `vc-trunk-or-topic-p' regarding the difference between trunk and +topic branches. When called interactively with a prefix argument, prompt for UPSTREAM-LOCATION. In some version control systems, UPSTREAM-LOCATION