From 7d9dad4241439569ef954e654cbf04c52dcfe5cc Mon Sep 17 00:00:00 2001 From: Sean Whitton Date: Wed, 18 Feb 2026 11:37:51 +0000 Subject: [PATCH] New VC commands for remote unintegrated changes * lisp/vc/vc.el (vc--outgoing-base, vc--outgoing-base-mergebase): New FORCE-TOPIC parameter. (vc--maybe-read-outgoing-base): New NO-DOUBLE parameter. (vc-root-diff-remote-unintegrated, vc-diff-remote-unintegrated) (vc-log-remote-unintegrated, vc-root-log-remote-unintegrated): New commands (bug#80434). * lisp/vc/vc-dir.el (vc-dir-mode-map): * lisp/vc/vc-hooks.el (vc-prefix-map): Bind them. * doc/emacs/vc1-xtra.texi (Unintegrated Changes): * etc/NEWS: Document them. --- doc/emacs/vc1-xtra.texi | 40 +++++++++++- etc/NEWS | 6 ++ lisp/vc/vc-dir.el | 4 ++ lisp/vc/vc-hooks.el | 4 ++ lisp/vc/vc.el | 140 ++++++++++++++++++++++++++++++++++++---- 5 files changed, 181 insertions(+), 13 deletions(-) diff --git a/doc/emacs/vc1-xtra.texi b/doc/emacs/vc1-xtra.texi index 1fc098c0693..2be32e8a717 100644 --- a/doc/emacs/vc1-xtra.texi +++ b/doc/emacs/vc1-xtra.texi @@ -296,6 +296,7 @@ yet merged into the target branch. @node Unintegrated Changes @subsubsection Commands to see all unintegrated changes @cindex unintegrated changes +@cindex remote unintegrated changes @table @kbd @item C-x v T = @@ -314,6 +315,26 @@ of this branch and its upstream counterpart @item C-x v T L Display log messages for all changes since the merge base of this branch and its upstream counterpart (@code{vc-root-log-unintegrated}). + +@item C-x v T R = +Display diffs of remote changes to the VC fileset since the merge base +of this topic branch and its upstream counterpart +(@code{vc-diff-remote-unintegrated}). + +@item C-x v T R D +Display a diff of all remote changes since the merge base of this topic +branch and its upstream counterpart +(@code{vc-root-diff-remote-unintegrated}). + +@item C-x v T R l +Display log messages for remote changes to the VC fileset since the +merge base of this topic branch and its upstream counterpart +(@code{vc-log-remote-unintegrated}). + +@item C-x v T R L +Display log messages for all remote changes since the merge base of this +topic branch and its upstream counterpart +(@code{vc-root-log-remote-unintegrated}). @end table For decentralized version control systems (@pxref{VCS Repositories}), @@ -388,7 +409,24 @@ changes to the current VC fileset. @kbd{C-x v T L} and @kbd{C-x v T l} show the corresponding revision logs, excluding uncommitted changes as above. -This functionality relies on Emacs correctly detecting whether the +@findex vc-diff-remote-unintegrated +@findex vc-root-diff-remote-unintegrated +@findex vc-log-remote-unintegrated +@findex vc-root-log-remote-unintegrated +On topic branches, it is sometimes also useful to see the same +information but for the remote repository's version of the topic branch. +This is the outstanding work on the topic branch that has been pushed, +but not yet merged. It is visible to your collaborators but is +otherwise still unintegrated. We call such work @dfn{remote +unintegrated changes}. There is a command corresponding to each of the +four commands discussed so far but which operates on the remote +repository's version of the current topic branch: @w{@kbd{C-x v R T =}} +(@code{vc-diff-remote-unintegrated}), @w{@kbd{C-x v R T D}} +(@code{vc-root-diff-remote-unintegrated}), @w{@kbd{C-x v R T l}} +(@code{vc-log-remote-unintegrated}), and @w{@kbd{C-x v R T L}} +(@code{vc-root-log-remote-unintegrated}). + +All this functionality relies on Emacs correctly detecting whether the current branch is a trunk or a topic branch, and in the latter case, correctly determining the branch to which the topic branch will eventually be merged. If the autodetection doesn't produce the right diff --git a/etc/NEWS b/etc/NEWS index 8e9e2ce8ed9..971feb4edaf 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -3008,6 +3008,12 @@ These are useful to view all outstanding (unmerged, unpushed) changes on the current branch. They are also available as 'T =', 'T D', 'T l' and 'T L' in VC Directory buffers. +'C-x v T R =' ('vc-diff-remote-unintegrated'), 'C-x v T R D' +('vc-root-diff-remote-unintegrated'), 'C-x v T R l' +('vc-log-remote-unintegrated') and 'C-x v T R L' +('vc-root-log-remote-unintegrated') are corresponding commands which +report information about the remote versions of a topic branch. + +++ *** New commands to report combined diffs of all local changes. 'C-x v E =' ('vc-diff-outgoing-and-edited') and 'C-x v E D' diff --git a/lisp/vc/vc-dir.el b/lisp/vc/vc-dir.el index 2b8e5d4540c..580539b6706 100644 --- a/lisp/vc/vc-dir.el +++ b/lisp/vc/vc-dir.el @@ -412,6 +412,10 @@ an \\+`up-to-date' or \\+`ignored' file." (define-key map "TL" #'vc-root-log-unintegrated) (define-key map "T=" #'vc-diff-unintegrated) (define-key map "TD" #'vc-root-diff-unintegrated) + (define-key map "TRl" #'vc-log-remote-unintegrated) + (define-key map "TRL" #'vc-root-log-remote-unintegrated) + (define-key map "TR=" #'vc-diff-remote-unintegrated) + (define-key map "TRD" #'vc-root-diff-remote-unintegrated) (define-key map "EL" #'vc-root-log-outgoing) (define-key map "E=" #'vc-diff-outgoing-and-edited) (define-key map "ED" #'vc-root-diff-outgoing-and-edited) diff --git a/lisp/vc/vc-hooks.el b/lisp/vc/vc-hooks.el index a7d7352b069..d0f292d2c9d 100644 --- a/lisp/vc/vc-hooks.el +++ b/lisp/vc/vc-hooks.el @@ -1040,6 +1040,10 @@ In the latter case, VC mode is deactivated for this buffer." "T L" #'vc-root-log-unintegrated "T =" #'vc-diff-unintegrated "T D" #'vc-root-diff-unintegrated + "T R l" #'vc-log-remote-unintegrated + "T R L" #'vc-root-log-remote-unintegrated + "T R =" #'vc-diff-remote-unintegrated + "T R D" #'vc-root-diff-remote-unintegrated ;; There are no -log-outgoing-and-edited commands because by ;; definition these are the same as -log-outgoing. ;; Additionally bind the -log-outgoing commands under C-x v E l/L as diff --git a/lisp/vc/vc.el b/lisp/vc/vc.el index 30a5c296898..0b64ea93efb 100644 --- a/lisp/vc/vc.el +++ b/lisp/vc/vc.el @@ -3357,27 +3357,30 @@ BACKEND is the VC backend." ;; branch has no name. (vc-call-backend backend 'trunk-or-topic-p branch)))) -(defun vc--outgoing-base (backend) +(defun vc--outgoing-base (backend force-topic) "Return an outgoing base for the current branch under VC backend BACKEND. The outgoing base is the upstream location for which unintegrated changes on this branch are destined once they are integrated. 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 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. +longer-lived (\"trunk\") branch. If FORCE-TOPIC is non-nil, assume this +is a shorter-lived branch. Otherwise call `vc-trunk-or-topic-p'. +If that function returns nil, also 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. - (and (memq (vc-trunk-or-topic-p nil backend) '(topic nil)) + (and (or force-topic + (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) +(defun vc--outgoing-base-mergebase + (backend &optional upstream-location refresh force-topic) "Return, under VC backend BACKEND, the merge base with UPSTREAM-LOCATION. Normally UPSTREAM-LOCATION, if non-nil, is a string. If UPSTREAM-LOCATION is nil, it means to call `vc--outgoing-base' and @@ -3387,12 +3390,15 @@ If UPSTREAM-LOCATION is the special value t, it means to use the place to which `vc-push' would push as UPSTREAM-LOCATION, unconditionally. (This is passed when the user invokes an outgoing base command with a \\`C-u C-u' prefix argument; see `vc--maybe-read-outgoing-base'.) -REFRESH is passed on to `vc--incoming-revision'." +REFRESH is passed on to `vc--incoming-revision'. +FORCE-TOPIC is passed on to `vc--outgoing-base'." (vc-call-backend backend 'mergebase (vc--incoming-revision backend (pcase upstream-location ('t nil) - ('nil (vc--outgoing-base backend)) + ('nil + (vc--outgoing-base backend + force-topic)) (_ upstream-location)) refresh))) @@ -3552,6 +3558,115 @@ topic branch." (vc--with-backend-in-rootdir "VC revision log" (vc-log-unintegrated upstream-location `(,backend (,rootdir))))) +;;;###autoload +(defun vc-root-diff-remote-unintegrated (&optional upstream-location) + "Report diff of remote changes since merge base with UPSTREAM-LOCATION. +Remote changes are changes in the incoming revision (instead of the +working revision), and the merge base with UPSTREAM-LOCATION is the +common ancestor of the incoming revision and UPSTREAM-LOCATION. +This command only makes sense for decentralized VCS, because otherwise +there is no distinction between locally committed changes and upstream +changes. + +When unspecified, UPSTREAM-LOCATION is the outgoing base when this +branch is considered as a topic branch (whether or not it actually is). +This command with unspecified UPSTREAM-LOCATION only makes sense on +topic branches. See `vc-trunk-or-topic-p'. + +When called interactively with a prefix argument, prompt for +UPSTREAM-LOCATION, which should be a remote branch name." + (interactive (list (vc--maybe-read-outgoing-base nil 'no-double))) + (vc--with-backend-in-rootdir "VC root-diff" + (vc-diff-remote-unintegrated upstream-location + `(,backend (,rootdir))))) + +;;;###autoload +(defun vc-diff-remote-unintegrated (&optional upstream-location fileset) + "Show remote fileset changes since merge base with UPSTREAM-LOCATION. +Remote changes are changes in the incoming revision (instead of the +working revision), and the merge base with UPSTREAM-LOCATION is the +common ancestor of the incoming revision and UPSTREAM-LOCATION. +This command only makes sense for decentralized VCS, because otherwise +there is no distinction between locally committed changes and remote +changes. + +When unspecified, UPSTREAM-LOCATION is the outgoing base when this +branch is considered as a topic branch (whether or not it actually is). +This command with unspecified UPSTREAM-LOCATION only makes sense on +topic branches. See `vc-trunk-or-topic-p'. + +When called interactively with a prefix argument, prompt for +UPSTREAM-LOCATION, which should be a remote branch name. + +When called from Lisp, optional argument FILESET overrides the fileset." + (interactive (let ((fileset (vc-deduce-fileset t))) + (list (vc--maybe-read-outgoing-base (car fileset) + 'no-double) + fileset))) + (let* ((fileset (or fileset (vc-deduce-fileset t))) + (backend (car fileset))) + (vc-diff-internal vc-allow-async-diff fileset + (vc--outgoing-base-mergebase backend + upstream-location + 'refresh 'force-topic) + ;; REFRESH nil here because we just refreshed. + (vc--incoming-revision backend) + (called-interactively-p 'interactive)))) + +;;;###autoload +(defun vc-log-remote-unintegrated (&optional upstream-location fileset) + "Show remote log for VC fileset since merge base with UPSTREAM-LOCATION. +Remote changes are changes in the incoming revision (instead of the +working revision), and the merge base with UPSTREAM-LOCATION is the +common ancestor of the incoming revision and UPSTREAM-LOCATION. +This command only makes sense for decentralized VCS, because otherwise +there is no distinction between locally committed changes and remote +changes. + +When unspecified, UPSTREAM-LOCATION is the outgoing base when this +branch is considered as a topic branch (whether or not it actually is). +This command with unspecified UPSTREAM-LOCATION only makes sense on +topic branches. See `vc-trunk-or-topic-p'. + +When called interactively with a prefix argument, prompt for +UPSTREAM-LOCATION, which should be a remote branch name. + +When called from Lisp, optional argument FILESET overrides the fileset." + (interactive (let ((fileset (vc-deduce-fileset t))) + (list (vc--maybe-read-outgoing-base (car fileset)) + fileset))) + (let* ((fileset (or fileset (vc-deduce-fileset t))) + (backend (car fileset))) + (vc-print-log-internal backend (cadr fileset) + (vc--incoming-revision backend nil 'refresh) + 'is-start-revision + ;; REFRESH nil here because we just refreshed. + (vc--outgoing-base-mergebase backend + upstream-location + nil 'force-topic)))) + +;;;###autoload +(defun vc-root-log-remote-unintegrated (&optional upstream-location) + "Show log of remote revisions since merge base with UPSTREAM-LOCATION. +Remote changes are changes in the incoming revision (instead of the +working revision), and the merge base with UPSTREAM-LOCATION is the +common ancestor of the incoming revision and UPSTREAM-LOCATION. +This command only makes sense for decentralized VCS, because otherwise +there is no distinction between locally committed changes and remote +changes. + +When unspecified, UPSTREAM-LOCATION is the outgoing base when this +branch is considered as a topic branch (whether or not it actually is). +This command with unspecified UPSTREAM-LOCATION only makes sense on +topic branches. See `vc-trunk-or-topic-p'. + +When called interactively with a prefix argument, prompt for +UPSTREAM-LOCATION, which should be a remote branch name." + (interactive (list (vc--maybe-read-outgoing-base nil 'no-double))) + (vc--with-backend-in-rootdir "VC revision log" + (vc-log-remote-unintegrated upstream-location + `(,backend (,rootdir))))) + (declare-function ediff-load-version-control "ediff" (&optional silent)) (declare-function ediff-vc-internal "ediff-vers" (rev1 rev2 &optional startup-hooks)) @@ -4481,14 +4596,15 @@ starting at that revision. Tags and remote references also work." nil 'vc-remote-location-history))) (and (not (string-empty-p res)) res)))) -(defun vc--maybe-read-outgoing-base (&optional backend) +(defun vc--maybe-read-outgoing-base (&optional backend no-double) "Return upstream location for interactive uses of outgoing base commands. If there is no prefix argument, return nil. -If the current prefix argument is \\`C-u C-u', return t. +If the current prefix argument is \\`C-u C-u' and NO-DOUBLE is nil, +return t. Otherwise prompt for an upstream location. BACKEND is the VC backend." (cond - ((equal current-prefix-arg '(16)) t) + ((and (not no-double) (equal current-prefix-arg '(16))) t) (current-prefix-arg (let* ((outgoing-base (vc-call-backend (or backend (vc-deduce-backend))