1
Fork 0
mirror of git://git.sv.gnu.org/emacs.git synced 2026-04-24 23:31:33 -07:00

Improve vc-git-topic-outgoing-base fallback algorithm

* lisp/vc/vc-hg.el (vc-hg--bookmarks): New function.
(vc-hg-trunk-or-topic-p):
* lisp/vc/vc-git.el (vc-git--branch-remotes)
(vc-git-trunk-or-topic-p): New optional BRANCH argument.
(vc-trunk-or-topic-p): Declare.
(vc-git-topic-outgoing-base): Consider only local trunks, if
there are any (bug#80006).
* lisp/vc/vc.el (trunk-or-topic-p): New optional BRANCH
argument.
(vc-trunk-or-topic-p): New function.
(vc--outgoing-base): Call it.
This commit is contained in:
Sean Whitton 2026-02-14 13:20:08 +00:00
parent cf27004e8c
commit 16f1ca1acc
3 changed files with 106 additions and 65 deletions

View file

@ -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 ".."))

View file

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

View file

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