diff --git a/doc/emacs/files.texi b/doc/emacs/files.texi index 567c1492518..a9bcee0b060 100644 --- a/doc/emacs/files.texi +++ b/doc/emacs/files.texi @@ -1835,7 +1835,8 @@ the start of the @var{n}th previous file. @findex diff-hunk-kill @item M-k -Kill the hunk at point (@code{diff-hunk-kill}). +Kill the hunk at point (@code{diff-hunk-kill}). If the region is +active, kills all hunks the region overlaps. @findex diff-file-kill @item M-K diff --git a/etc/NEWS b/etc/NEWS index 78d627068c4..be507f525ba 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -2284,9 +2284,10 @@ one as before. This makes them different from 'vc-diff' and *** 'diff-apply-hunk' now supports creating and deleting files. +++ -*** 'diff-apply-hunk' and 'diff-apply-buffer' now consider the region. -If the region is active, these commands now apply all hunks that the -region overlaps. Otherwise, they have their existing behavior. +*** Diff mode's application and killing commands now consider the region. +If the region is active, 'diff-apply-hunk', 'diff-apply-buffer' and +'diff-hunk-kill' now apply or kill all hunks that the region overlaps. +Otherwise, they have their existing behavior. +++ *** 'diff-apply-buffer' can reverse-apply. diff --git a/lisp/vc/diff-mode.el b/lisp/vc/diff-mode.el index f57f39de37c..28e29cf36c5 100644 --- a/lisp/vc/diff-mode.el +++ b/lisp/vc/diff-mode.el @@ -883,31 +883,19 @@ If the prefix ARG is given, restrict the view to the current file instead." (goto-char (point-min)) (re-search-forward diff-hunk-header-re nil t))) -(defun diff-hunk-kill () - "Kill the hunk at point." - (interactive) - (if (not (diff--some-hunks-p)) - (error "No hunks") - (diff-beginning-of-hunk t) - (let* ((hunk-bounds (diff-bounds-of-hunk)) - (file-bounds (ignore-errors (diff-bounds-of-file))) - ;; If the current hunk is the only one for its file, kill the - ;; file header too. - (bounds (if (and file-bounds - (progn (goto-char (car file-bounds)) - (= (progn (diff-hunk-next) (point)) - (car hunk-bounds))) - (progn (goto-char (cadr hunk-bounds)) - ;; bzr puts a newline after the last hunk. - (while (looking-at "^\n") - (forward-char 1)) - (= (point) (cadr file-bounds)))) - file-bounds - hunk-bounds)) - (inhibit-read-only t)) - (apply #'kill-region bounds) - (goto-char (car bounds)) - (ignore-errors (diff-beginning-of-hunk t))))) +(defun diff-hunk-kill (&optional beg end) + "Kill the hunk at point. +When killing the last hunk left for a file, kill the file header too. +Interactively, if the region is active, kill all hunks that the region +overlaps. + +When called from Lisp with optional arguments BEG and END non-nil, kill +all hunks overlapped by the region from BEG to END as though called +interactively with an active region delimited by BEG and END." + (interactive "R") + (when (xor beg end) + (error "Invalid call to `diff-hunk-kill'")) + (diff--revert-kill-hunks beg end nil)) ;; This is not `diff-kill-other-hunks' because we might need to make ;; copies of file headers in order to ensure the new kill ring entry @@ -2283,6 +2271,83 @@ With a prefix argument, try to REVERSE the hunk." :type 'boolean :version "31.1") +(defun diff--revert-kill-hunks (beg end revertp) + "Workhorse routine for killing hunks, after possibly reverting them. +If BEG and END are nil, kill the hunk at point. +Otherwise kill all hunks overlapped by region delimited by BEG and END. +When killing a hunk that's the only one remaining for its file, kill the +file header too. +If REVERTP is non-nil, reverse-apply hunks before killing them." + ;; With BEG and END non-nil, we push each hunk to the kill ring + ;; separately. If we want to push to the kill ring just once, we have + ;; to decide how to handle file headers such that the meanings of the + ;; hunks in the kill ring entry, considered as a whole patch, do not + ;; deviate too far from the meanings the hunks had in this buffer. + ;; + ;; For example, if we have a single hunk for one file followed by + ;; multiple hunks for another file, and we naïvely kill the single + ;; hunk and the first of the multiple hunks, our kill ring entry will + ;; be a patch applying those two hunks to the first file. This is + ;; because killing the single hunk will have brought its file header + ;; with it, but not so killing the second hunk. So we will have put + ;; together hunks that were previously for two different files. + ;; + ;; One option is to *copy* every file header that the region overlaps + ;; (and that we will not kill, because we are leaving other hunks for + ;; that file behind). But then the text this command pushes to the + ;; kill ring would be different from the text it removes from the + ;; buffer, which would be unintuitive for an Emacs kill command. + ;; + ;; An alternative might be to have restrictions as follows: + ;; + ;; Interactively, if the region is active, try to kill all hunks that the + ;; region overlaps. This works when either + ;; - all the hunks the region overlaps are for the same file; or + ;; - the last hunk the region overlaps is the last hunk for its file. + ;; These restrictions are so that the text added to the kill ring does not + ;; merge together hunks for different files under a single file header. + ;; + ;; We would error out if neither property is met. When either holds, + ;; any file headers the region overlaps are ones we should kill. + (unless (diff--some-hunks-p) + (error "No hunks")) + (if beg + (save-excursion + (goto-char beg) + (setq beg (car (diff-bounds-of-hunk))) + (goto-char end) + (unless (looking-at diff-hunk-header-re) + (setq end (cadr (diff-bounds-of-hunk))))) + (pcase-setq `(,beg ,end) (diff-bounds-of-hunk))) + (when (or (not revertp) (null (diff-apply-buffer beg end t))) + (goto-char end) + (when-let* ((pos (diff--at-diff-header-p))) + (goto-char pos)) + (setq beg (copy-marker beg) end (point-marker)) + (unwind-protect + (cl-loop initially (goto-char beg) + for (hunk-beg hunk-end) = (diff-bounds-of-hunk) + for file-bounds = (ignore-errors (diff-bounds-of-file)) + for (file-beg file-end) = file-bounds + for inhibit-read-only = t + if (and file-bounds + (progn + (goto-char file-beg) + (diff-hunk-next) + (eq (point) hunk-beg)) + (progn + (goto-char hunk-end) + ;; bzr puts a newline after the last hunk. + (while (looking-at "^\n") (forward-char 1)) + (eq (point) file-end))) + do (kill-region file-beg file-end) (goto-char file-beg) + else do (kill-region hunk-beg hunk-end) (goto-char hunk-beg) + do (ignore-errors (diff-beginning-of-hunk t)) + until (or (< (point) (marker-position beg)) + (eql (point) (marker-position end)))) + (set-marker beg nil) + (set-marker end nil)))) + (defun diff-revert-and-kill-hunk (&optional beg end) "Reverse-apply and then kill the hunk at point. Save changed buffer. Interactively, if the region is active, reverse-apply and kill all @@ -2308,27 +2373,7 @@ BEG and END." (error "Invalid call to `diff-revert-and-kill-hunk'")) (when (or (not diff-ask-before-revert-and-kill-hunk) (y-or-n-p "Really reverse-apply and kill hunk(s)?")) - (if beg - (save-excursion - (goto-char beg) - (setq beg (car (diff-bounds-of-hunk))) - (goto-char end) - (unless (looking-at diff-hunk-header-re) - (setq end (cadr (diff-bounds-of-hunk))))) - (pcase-setq `(,beg ,end) (diff-bounds-of-hunk))) - (when (null (diff-apply-buffer beg end t)) - ;; Use `diff-hunk-kill' because it properly handles file headers. - (goto-char end) - (when-let* ((pos (diff--at-diff-header-p))) - (goto-char pos)) - (setq beg (copy-marker beg) end (point-marker)) - (unwind-protect - (cl-loop initially (goto-char beg) - do (diff-hunk-kill) - until (or (< (point) (marker-position beg)) - (eql (point) (marker-position end)))) - (set-marker beg nil) - (set-marker end nil))))) + (diff--revert-kill-hunks beg end t))) (defun diff-apply-buffer (&optional beg end reverse test-or-no-save) "Apply the diff in the entire diff buffer. diff --git a/lisp/vc/vc-git.el b/lisp/vc/vc-git.el index b1a60aeeb23..d6a7145b34e 100644 --- a/lisp/vc/vc-git.el +++ b/lisp/vc/vc-git.el @@ -811,7 +811,7 @@ This works by considering the current branch as a topic branch (whether or not it actually is). If there is a distinct push remote for this branch, assume the target -for outstanding changes is the tracking branch, so return that. +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