From 11b68c6223d154b4c85e8feec63581dcf7c76a3e Mon Sep 17 00:00:00 2001 From: Sean Whitton Date: Fri, 21 Nov 2025 11:22:30 +0000 Subject: [PATCH] New commands to rewind decentralized VCS branches * lisp/vc/vc.el (vc--remove-revisions-from-end): New function. (vc-uncommit-revisions-from-end, vc-delete-revisions-from-end): * lisp/vc/log-view.el (log-view-uncommit-revisions-from-end) (log-view-delete-revisions-from-end): New commands (bug#79408). (log-view-mode-map): Bind them. * doc/emacs/maintaining.texi (VC Change Log): * doc/emacs/vc1-xtra.texi (VC Auto-Reverting): * etc/NEWS: Document them. --- doc/emacs/emacs.texi | 1 + doc/emacs/maintaining.texi | 9 ++++ doc/emacs/vc1-xtra.texi | 55 ++++++++++++++++++++++++ etc/NEWS | 7 ++++ lisp/vc/diff-mode.el | 3 +- lisp/vc/log-view.el | 35 +++++++++++++++- lisp/vc/vc.el | 86 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 194 insertions(+), 2 deletions(-) diff --git a/doc/emacs/emacs.texi b/doc/emacs/emacs.texi index d7bd581f7fa..9f84f4e3978 100644 --- a/doc/emacs/emacs.texi +++ b/doc/emacs/emacs.texi @@ -877,6 +877,7 @@ Miscellaneous Commands and Features of VC * Editing VC Commands:: Editing the VC shell commands that Emacs will run. * Preparing Patches:: Preparing and composing patches from within VC. * VC Auto-Reverting:: Updating buffer contents after VCS operations. +* Rewinding Branches:: Commands to delete revisions from ends of branches. Customizing VC diff --git a/doc/emacs/maintaining.texi b/doc/emacs/maintaining.texi index 147857aa836..db1e81d62e6 100644 --- a/doc/emacs/maintaining.texi +++ b/doc/emacs/maintaining.texi @@ -1273,6 +1273,15 @@ the revision at point, or the changes from all marked revisions @item R Undo the effects of old revisions; either the revision at point, or all marked revisions (@code{log-view-revert-or-delete-revisions}). + +@item x +Delete revisions newer than the revision at point from the current +branch without touching the working tree +(@code{log-view-uncommit-revisions-from-end}). + +@item X +Delete revisions newer than the revision at point from the current +branch (@code{log-view-delete-revisions-from-end}). @end table @vindex vc-log-show-limit diff --git a/doc/emacs/vc1-xtra.texi b/doc/emacs/vc1-xtra.texi index e020a36407b..6f1face7f40 100644 --- a/doc/emacs/vc1-xtra.texi +++ b/doc/emacs/vc1-xtra.texi @@ -21,6 +21,7 @@ * Editing VC Commands:: Editing the VC shell commands that Emacs will run. * Preparing Patches:: Preparing and composing patches from within VC. * VC Auto-Reverting:: Updating buffer contents after VCS operations. +* Rewinding Branches:: Commands to delete revisions from ends of branches. @end menu @node Change Logs and VC @@ -678,6 +679,60 @@ contents, regardless of whether Emacs initiated those operations. @xref{VC Mode Line}, for details regarding Auto Revert mode in buffers visiting tracked files (which is what @code{vc-auto-revert-mode} enables). +@node Rewinding Branches +@subsubsection Rewinding Branches +@cindex rewinding a branch (VC) + +@table @kbd +@item M-x vc-delete-revisions-from-end +Delete revisions from the end of the current branch. + +@item M-x vc-uncommit-revisions-from-end +Delete revisions from the end of the current branch without touching the +working tree. +@end table + +@findex vc-delete-revisions-from-end + For decentralized version control systems (@pxref{VCS Repositories}), +these commands provide ways to move the current branch back to an +earlier revision. @code{vc-delete-revisions-from-end} prompts for a +revision, then removes all revisions from the end of the branch up to +but not including the specified revision. We say that the branch is +@dfn{rewound} back to the specified revision. + + This command removes the changes made by the revisions from the +working tree. Therefore, if there are any uncommitted changes, they +must be reverted, first (@pxref{VC Undo}). This command will prompt you +to do that if necessary. If you supply a prefix argument, Emacs will +delete uncommitted changes without prompting. + +@cindex uncommitting revisions +@findex vc-uncommit-revisions-from-end + To ``uncommit'' a revision means to remove it from the revision +history without removing its changes from the working tree. It is as +though you had made the changes but had not yet checked them in. The +command @code{vc-uncommit-revisions-from-end} prompts for a revision, +and then uncommits all revisions from the end of the branch up to but +not including the specified revision. The branch is rewound back to the +specified revision but the changes are left behind in the working tree. + + When rewinding the current branch, if all the revisions deleted from +the revision history are among those you have pulled or pushed, then +these operations do not permanently delete anything: a simple +@w{@kbd{C-x v +}} (@pxref{Pulling / Pushing}) will bring the revisions +back. On the other hand, if there are new revisions on the end of the +branch that have not yet been pushed, then these commands will delete +them permanently. Emacs tries to detect this situation and ask you if +you are sure you want to delete them. + + Alternative ways to access this functionality are the +@code{log-view-uncommit-revisions-from-end} and +@code{log-view-delete-revisions-from-end} commands, bound to @kbd{x} and +@kbd{X}, respectively, in Log View mode buffers (@pxref{VC Change Log}). +Compared to using the commands described here directly, the Log View +mode commands can make it easier to be sure you are rewinding back to +the revision you intend. + @node Customizing VC @subsection Customizing VC diff --git a/etc/NEWS b/etc/NEWS index ff8ebe7e824..73335113180 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -2393,6 +2393,13 @@ From Log View buffers, you can use 'C' to cherry-pick the revision at point or all marked revisions, and 'R' to undo the revision at point or all marked revisions. ++++ +*** New commands to rewind branches. +In Log View mode, 'x' deletes revisions newer than the revision at point +from the history of the current branch, though without undoing the +changes made by those revisions to the working tree. 'X' is similar +except that it does remove the changes from the working tree. + *** New command 'log-edit-done-strip-cvs-lines'. This command strips all lines beginning with "CVS:" from the buffer. It is intended to be added to the 'log-edit-done-hook' so that diff --git a/lisp/vc/diff-mode.el b/lisp/vc/diff-mode.el index d58d221a658..3e45ff045e2 100644 --- a/lisp/vc/diff-mode.el +++ b/lisp/vc/diff-mode.el @@ -2235,7 +2235,8 @@ With a prefix argument, try to REVERSE the hunk." This command is useful in buffers generated by \\[vc-diff] and \\[vc-root-diff], especially when preparing to commit the patch with \\[vc-next-action]. -You can use \\\\[diff-hunk-kill] to temporarily remove changes that you intend to +You can use \\\\[diff-hunk-kill] \ +to temporarily remove changes that you intend to include in a separate commit or commits, and you can use this command to permanently drop changes you didn't intend, or no longer want. diff --git a/lisp/vc/log-view.el b/lisp/vc/log-view.el index 0419b2c077a..8a0013dc9d2 100644 --- a/lisp/vc/log-view.el +++ b/lisp/vc/log-view.el @@ -115,6 +115,7 @@ (autoload 'vc-find-revision "vc") (autoload 'vc-diff-internal "vc") (autoload 'vc--pick-or-revert "vc") +(autoload 'vc--remove-revisions-from-end "vc") (autoload 'vc--prompt-other-working-tree "vc") (defvar cvs-minor-wrap-function) @@ -142,7 +143,9 @@ "TAB" #'log-view-msg-next "" #'log-view-msg-prev "C" #'log-view-cherry-pick - "R" #'log-view-revert-or-delete-revisions) + "R" #'log-view-revert-or-delete-revisions + "x" #'log-view-uncommit-revisions-from-end + "X" #'log-view-delete-revisions-from-end) (easy-menu-define log-view-mode-menu log-view-mode-map "Log-View Display Menu." @@ -828,6 +831,36 @@ See also `vc-revert-or-delete-revision'." t current-prefix-arg)) (log-view--pick-or-revert directory nil t interactive delete)) +(defun log-view-uncommit-revisions-from-end () + "Uncommit revisions newer than the revision at point. +The revision at point must be on the current branch. The newer +revisions are deleted from the revision history but the changes made by +those revisions to files in the working tree are not undone. + +To delete revisions from the revision history and also undo the changes +in the working tree, see `log-edit-delete-revisions-from-end'." + (interactive) + (vc--remove-revisions-from-end (log-view-current-tag) + nil t log-view-vc-backend) + (revert-buffer)) + +(defun log-view-delete-revisions-from-end (&optional discard) + "Delete revisions newer than the revision at point. +The revision at point must be on the current branch. The newer +revisions are deleted from the revision history and the changes made by +those revisions to files in the working tree are undone. +If the are uncommitted changes, prompts to discard them. +With a prefix argument (when called from Lisp, with optional argument +DISCARD non-nil), discard any uncommitted changes without prompting. + +To delete revisions from the revision history without undoing the +changes in the working tree, see `log-edit-uncommit-revisions-from-end'." + (interactive "P") + (vc--remove-revisions-from-end (log-view-current-tag) + (if discard 'discard t) + t log-view-vc-backend) + (revert-buffer)) + ;; These are left unbound by default. A user who doesn't like the DWIM ;; behavior of `log-view-revert-or-delete-revisions' can unbind that and ;; bind these two commands instead. diff --git a/lisp/vc/vc.el b/lisp/vc/vc.el index ff7f0abd447..9b3e62d0a86 100644 --- a/lisp/vc/vc.el +++ b/lisp/vc/vc.el @@ -2429,6 +2429,92 @@ with a prefix argument." (interactive (list (vc-read-revision "Revision to delete: "))) (vc--pick-or-revert rev t nil t nil nil backend)) +(defun vc--remove-revisions-from-end (rev delete prompt backend) + "Delete revisions newer than REV. +DELETE non-nil means to remove the changes from the working tree. +DELETE `discard' means to silently discard uncommitted changes. +PROMPT non-nil means to always get confirmation. (This is passed by +`log-view-uncommit-revisions-from-end' and `log-view-delete-revisions' +because they have single-letter bindings and don't otherwise prompt, so +might be easy to use accidentally.) +BACKEND is the VC backend." + (let ((backend (or backend (vc-responsible-backend default-directory)))) + (unless (eq (vc-call-backend backend 'revision-granularity) + 'repository) + (error "Requires VCS with whole-repository revision granularity")) + (unless (vc-find-backend-function backend 'revision-published-p) + (signal 'vc-not-supported (list 'revision-published-p backend))) + ;; Rewinding the end of the branch to REV does not in itself mean + ;; rewriting public history because a subsequent pull will generally + ;; undo the rewinding. Rewinding and then making new commits before + ;; syncing with the upstream will necessitate merging, but that's + ;; just part of the normal workflow with a distributed VCS. + ;; Therefore we don't prompt about deleting published revisions (and + ;; so we ignore `vc-allow-rewriting-published-history'). + ;; We do care about deleting *unpublished* revisions, however, + ;; because that could potentially mean losing work permanently. + (when (if (vc-call-backend backend 'revision-published-p + (vc-call-backend backend + 'working-revision-symbol)) + (and prompt + (not (y-or-n-p + (format "Uncommit revisions newer than %s?" + rev)))) + ;; FIXME: Actually potentially not all revisions newer than + ;; REV would be permanently deleted -- only those which are + ;; unpushed. So this prompt is a little misleading. + (not (yes-or-no-p + (format "Permanently delete revisions newer than %s?" + rev)))) + (user-error "Aborted")) + (if delete + ;; FIXME: As discussed in bug#79408, instead of just failing if + ;; the user declines reverting the changes, we would leave + ;; behind some sort of conflict for the user to resolve, like we + ;; do when there is a merge conflict. + (let ((root (vc-root-dir))) + (when (vc-dir-status-files root nil backend) + (if (eq delete 'discard) + (vc-revert-file root) + (let ((vc-buffer-overriding-fileset `(,backend (,root)))) + (vc-revert)))) + (vc-call-backend backend 'delete-revisions-from-end rev)) + (vc-call-backend backend 'uncommit-revisions-from-end rev)))) + +;;;###autoload +(defun vc-uncommit-revisions-from-end (rev &optional backend) + "Delete revisions newer than REV without touching the working tree. +REV must be on the current branch. The newer revisions are deleted from +the revision history but the changes made by those revisions to files in +the working tree are not undone. +When called interactively, prompts for REV. +BACKEND is the VC backend. + +To delete revisions from the revision history and also undo the changes +in the working tree, see `vc-delete-revisions-from-end'." + (interactive (list + (vc-read-revision "Uncommit revisions newer than revision: "))) + (vc--remove-revisions-from-end rev nil nil backend)) + +;;;###autoload +(defun vc-delete-revisions-from-end (rev &optional discard backend) + "Delete revisions newer than REV. +REV must be on the current branch. The newer revisions are deleted from +the revision history and the changes made by those revisions to files in +the working tree are undone. +When called interactively, prompts for REV. +If the are uncommitted changes, prompts to discard them. +With a prefix argument (when called from Lisp, with optional argument +DISCARD non-nil), discard any uncommitted changes without prompting. +BACKEND is the VC backend. + +To delete revisions from the revision history without undoing the +changes in the working tree, see `vc-uncommit-revisions-from-end'." + (interactive (list + (vc-read-revision "Delete revisions newer than revision: ") + current-prefix-arg)) + (vc--remove-revisions-from-end rev (if discard 'discard t) nil backend)) + (declare-function diff-bounds-of-hunk "diff-mode") (defun vc-default-checkin-patch (_backend patch-string comment)