fordon_greeman / voidfiles

Branch: 2079ec5
Switch branches/tags
main
theidioticdev add emacs 2079ec5 23h ago
866 lines · 35.4 KB Raw Blame History
1 ;;; magit-apply.el --- Apply Git diffs -*- lexical-binding:t -*-
2
3 ;; Copyright (C) 2008-2026 The Magit Project Contributors
4
5 ;; Author: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev>
6 ;; Maintainer: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev>
7
8 ;; SPDX-License-Identifier: GPL-3.0-or-later
9
10 ;; Magit is free software: you can redistribute it and/or modify it
11 ;; under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
14 ;;
15 ;; Magit is distributed in the hope that it will be useful, but WITHOUT
16 ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
17 ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
18 ;; License for more details.
19 ;;
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with Magit. If not, see <https://www.gnu.org/licenses/>.
22
23 ;;; Commentary:
24
25 ;; This library implements commands for applying Git diffs or parts
26 ;; of such a diff. The supported "apply variants" are apply, stage,
27 ;; unstage, discard, and reverse - more than Git itself knows about,
28 ;; at least at the porcelain level.
29
30 ;;; Code:
31
32 (require 'magit-core)
33 (require 'magit-diff)
34 (require 'magit-wip)
35
36 (require 'dired)
37 (require 'transient) ; See #3732.
38
39 ;; For `magit-apply'
40 (declare-function magit-am "magit-sequence" () t)
41 (declare-function magit-patch-apply "magit-patch" () t)
42 ;; For `magit-discard-files'
43 (declare-function magit-checkout-stage "magit-merge" (file arg))
44 (declare-function magit-checkout-read-stage "magit-merge" (file))
45 (defvar auto-revert-verbose)
46 ;; For `magit-stage-untracked'
47 (declare-function magit-submodule-add-1 "magit-submodule"
48 (url &optional path name args))
49 (declare-function magit-submodule-read-name-for-path "magit-submodule"
50 (path &optional prefer-short))
51 (defvar borg-user-emacs-directory)
52
53 ;;; Options
54
55 (defcustom magit-delete-by-moving-to-trash t
56 "Whether Magit uses the system's trash can.
57
58 You should absolutely not disable this and also remove `discard'
59 from `magit-no-confirm'. You shouldn't do that even if you have
60 all of the Magit-Wip modes enabled, because those modes do not
61 track any files that are not tracked in the proper branch."
62 :package-version '(magit . "2.1.0")
63 :group 'magit-essentials
64 :type 'boolean)
65
66 (defcustom magit-unstage-committed t
67 "Whether unstaging a committed change reverts it instead.
68
69 A committed change cannot be unstaged, because staging and
70 unstaging are actions that are concerned with the differences
71 between the index and the working tree, not with committed
72 changes.
73
74 If this option is non-nil (the default), then typing \"u\"
75 \(`magit-unstage') on a committed change, causes it to be
76 reversed in the index but not the working tree. For more
77 information see command `magit-reverse-in-index'."
78 :package-version '(magit . "2.4.1")
79 :group 'magit-commands
80 :type 'boolean)
81
82 (defcustom magit-reverse-atomically nil
83 "Whether to reverse changes atomically.
84
85 If some changes can be reversed while others cannot, then nothing
86 is reversed if the value of this option is non-nil. But when it
87 is nil, then the changes that can be reversed are reversed and
88 for the other changes diff files are created that contain the
89 rejected reversals."
90 :package-version '(magit . "2.7.0")
91 :group 'magit-commands
92 :type 'boolean)
93
94 (defcustom magit-post-stage-hook nil
95 "Hook run after staging changes.
96 This hook is run by `magit-refresh' if `this-command'
97 is a member of `magit-post-stage-hook-commands'."
98 :package-version '(magit . "2.90.0")
99 :group 'magit-commands
100 :type 'hook)
101
102 (defcustom magit-post-unstage-hook nil
103 "Hook run after unstaging changes.
104 This hook is run by `magit-refresh' if `this-command'
105 is a member of `magit-post-unstage-hook-commands'."
106 :package-version '(magit . "2.90.0")
107 :group 'magit-commands
108 :type 'hook)
109
110 ;;; Commands
111 ;;;; Apply
112
113 (defun magit-apply (&rest args)
114 "Apply the change at point to the working tree.
115 With a prefix argument fallback to a 3-way merge. Doing
116 so causes the change to be applied to the index as well."
117 (interactive (and current-prefix-arg (list "--3way")))
118 (when$ (magit-apply--get-selection)
119 (pcase (list (magit-diff-type) (magit-diff-scope))
120 (`(,(or 'unstaged 'staged) ,_)
121 (user-error "Change is already in the working tree"))
122 (`(untracked ,(or 'file 'files))
123 (call-interactively #'magit-am))
124 (`(rebase-sequence file)
125 (call-interactively #'magit-patch-apply))
126 (`(,_ region) (magit-apply-region $ args))
127 (`(,_ hunk) (magit-apply-hunk $ args))
128 (`(,_ hunks) (magit-apply-hunks $ args))
129 (`(,_ file) (magit-apply-diff $ args))
130 (`(,_ files) (magit-apply-diffs $ args)))))
131
132 (defun magit-apply--section-content (section)
133 (buffer-substring-no-properties (if (magit-hunk-section-p section)
134 (oref section start)
135 (oref section content))
136 (oref section end)))
137
138 (defun magit-apply-diffs (sections &rest args)
139 (setq sections (magit-apply--get-diffs sections))
140 (magit-apply-patch sections args
141 (mapconcat (##concat (magit-diff-file-header %)
142 (magit-apply--section-content %))
143 sections "")))
144
145 (defun magit-apply-diff (section &rest args)
146 (setq section (car (magit-apply--get-diffs (list section))))
147 (magit-apply-patch section args
148 (concat (magit-diff-file-header section)
149 (magit-apply--section-content section))))
150
151 (defun magit-apply--adjust-hunk-new-starts (hunks)
152 "Adjust new line numbers in headers of HUNKS for partial application.
153 HUNKS should be a list of ordered, contiguous hunks to be applied
154 from a file. For example, if there is a sequence of hunks with
155 the headers
156
157 @@ -2,6 +2,7 @@
158 @@ -10,6 +11,7 @@
159 @@ -18,6 +20,7 @@
160
161 and only the second and third are to be applied, they would be
162 adjusted as \"@@ -10,6 +10,7 @@\" and \"@@ -18,6 +19,7 @@\"."
163 (let* ((first-hunk (car hunks))
164 (offset (if (string-match diff-hunk-header-re-unified first-hunk)
165 (- (string-to-number (match-str 3 first-hunk))
166 (string-to-number (match-str 1 first-hunk)))
167 (error "Header hunks have to be applied individually"))))
168 (if (= offset 0)
169 hunks
170 (mapcar (lambda (hunk)
171 (if (string-match diff-hunk-header-re-unified hunk)
172 (replace-match (number-to-string
173 (- (string-to-number (match-str 3 hunk))
174 offset))
175 t t hunk 3)
176 (error "Hunk does not have expected header")))
177 hunks))))
178
179 (defun magit-apply--adjust-hunk-new-start (hunk)
180 (car (magit-apply--adjust-hunk-new-starts (list hunk))))
181
182 (defun magit-apply-hunks (hunks &rest args)
183 (let ((file (oref (car hunks) parent)))
184 (when (magit-diff--combined-p file)
185 (user-error "Cannot un-/stage resolution hunks. Stage the whole file"))
186 (magit-apply-patch
187 file args
188 (concat (oref file header)
189 (string-join (magit-apply--adjust-hunk-new-starts
190 (mapcar #'magit-apply--section-content hunks)))))))
191
192 (defun magit-apply-hunk (hunk &rest args)
193 (let ((file (oref hunk parent)))
194 (when (magit-diff--combined-p file)
195 (user-error "Cannot un-/stage resolution hunks. Stage the whole file"))
196 (let* ((header (car (oref hunk value)))
197 (header (and (symbolp header) header))
198 (content (magit-apply--section-content hunk)))
199 (magit-apply-patch
200 file args
201 (concat (magit-diff-file-header hunk (not (eq header 'rename)))
202 (if header
203 content
204 (magit-apply--adjust-hunk-new-start content)))))))
205
206 (defun magit-apply-region (hunk &rest args)
207 (let ((file (oref hunk parent)))
208 (when (magit-diff--combined-p file)
209 (user-error "Cannot un-/stage resolution hunks. Stage the whole file"))
210 (magit-apply-patch
211 file args
212 (concat (magit-diff-file-header hunk)
213 (magit-apply--adjust-hunk-new-start
214 (magit-diff-hunk-region-patch hunk args))))))
215
216 (defun magit-apply-patch (section:s args patch)
217 (let* ((files (if (atom section:s)
218 (list (oref section:s value))
219 (mapcar (##oref % value) section:s)))
220 (command (symbol-name this-command))
221 (command (if (and command (string-match "^magit-\\([^-]+\\)" command))
222 (match-str 1 command)
223 "apply"))
224 (context (magit-diff-get-context))
225 (ignore-context (magit-diff-ignore-any-space-p)))
226 (unless (magit-diff-context-p)
227 (user-error "Not enough context to apply patch. Increase the context"))
228 (unless magit-inhibit-refresh
229 (magit-run-before-change-functions files command))
230 (if (eq this-command 'magit-copy-diff-as-kill)
231 (kill-new patch)
232 (with-temp-buffer
233 (insert patch)
234 (let ((magit-inhibit-refresh t))
235 (magit-run-git-with-input
236 "apply" args "-p0"
237 (if ignore-context "-C0" (format "-C%s" context))
238 "--ignore-space-change" "-")))
239 (unless magit-inhibit-refresh
240 (magit-run-after-apply-functions files command)
241 (magit-refresh)))))
242
243 (defun magit-apply--get-selection (&optional noerror)
244 (or (magit-region-sections '(hunk file module) t)
245 (let ((section (magit-current-section)))
246 (pcase (oref section type)
247 ((or 'hunk 'file 'module) section)
248 ((or 'staged 'unstaged
249 'stashed-index 'stashed-worktree 'stashed-untracked)
250 (oref section children))
251 ('untracked t)
252 ((guard noerror) nil)
253 (_ (user-error "Cannot apply this, it's not a change"))))))
254
255 (defun magit-apply--get-diffs (sections)
256 (magit-section-case
257 ([file diffstat]
258 (mapcar (lambda (section)
259 (or (magit-get-section
260 (append `((file . ,(oref section value)))
261 (magit-section-ident magit-root-section)))
262 (error "Cannot get required diff headers")))
263 sections))
264 (t sections)))
265
266 (defun magit-apply--ignore-whitespace-p (selection type scope)
267 "Return t if it is necessary and possible to ignore whitespace.
268 It is necessary to do so when the diff ignores whitespace changes
269 and whole files are being applied. It is possible when no binary
270 files are involved. If it is both necessary and impossible, then
271 return nil, possibly causing whitespace changes to be applied."
272 (and (memq type '(unstaged staged))
273 (memq scope '(file files list))
274 (cl-find-if (lambda (arg)
275 (member arg '("--ignore-space-at-eol"
276 "--ignore-space-change"
277 "--ignore-all-space"
278 "--ignore-blank-lines")))
279 magit-buffer-diff-args)
280 (not (cl-find-if (##oref % binary)
281 (ensure-list selection)))))
282
283 ;;;; Stage
284
285 (defun magit-stage (&optional intent)
286 "Add the change at point to the staging area.
287 With a prefix argument, INTENT, and an untracked file (or files)
288 at point, stage the file but not its content."
289 (interactive "P")
290 (if-let ((s (and (derived-mode-p 'magit-mode)
291 (magit-apply--get-selection)))
292 (type (magit-diff-type))
293 (scope (magit-diff-scope)))
294 (pcase (list type scope
295 (magit-apply--ignore-whitespace-p s type scope))
296 (`(untracked ,_ ,_) (magit-stage-untracked intent))
297 (`(unstaged region ,_) (magit-apply-region s "--cached"))
298 (`(unstaged hunk ,_) (magit-apply-hunk s "--cached"))
299 (`(unstaged hunks ,_) (magit-apply-hunks s "--cached"))
300 ('(unstaged file t) (magit-apply-diff s "--cached"))
301 ('(unstaged files t) (magit-apply-diffs s "--cached"))
302 ('(unstaged list t) (magit-apply-diffs s "--cached"))
303 ('(unstaged file nil) (magit-stage-1 "-u" (list (oref s value))))
304 ('(unstaged files nil) (magit-stage-1 "-u" (magit-region-values nil t)))
305 ('(unstaged list nil) (magit-stage-modified))
306 (`(staged ,_ ,_) (user-error "Already staged"))
307 (`(committed ,_ ,_) (user-error "Cannot stage committed changes"))
308 (`(undefined ,_ ,_) (user-error "Cannot stage this change")))
309 (call-interactively #'magit-stage-files)))
310
311 ;;;###autoload
312 (defun magit-stage-files (files &optional force)
313 "Read one or more files and stage all changes in those files.
314 With prefix argument FORCE, offer ignored files for completion."
315 (interactive
316 (let* ((choices (if current-prefix-arg
317 (magit-ignored-files)
318 (nconc (magit-unstaged-files)
319 (magit-untracked-files))))
320 (default (or (magit-section-value-if 'file)
321 (magit-file-relative-name)))
322 (default (car (member default choices))))
323 (list (magit-completing-read-multiple
324 (if current-prefix-arg "Stage ignored file,s: " "Stage file,s: ")
325 choices nil t nil nil default)
326 current-prefix-arg)))
327 (magit-with-toplevel
328 (magit-stage-1 (and force "--force") files)))
329
330 ;;;###autoload
331 (defun magit-stage-modified (&optional all)
332 "Stage all changes to files modified in the worktree.
333 Stage all new content of tracked files and remove tracked files
334 that no longer exist in the working tree from the index also.
335 With a prefix argument also stage previously untracked (but not
336 ignored) files."
337 (interactive "P")
338 (when (magit-anything-staged-p)
339 (magit-confirm 'stage-all-changes))
340 (magit-with-toplevel
341 (magit-stage-1 (if all "--all" "-u") magit-buffer-diff-files)))
342
343 (defun magit-stage-1 (arg &optional files)
344 (magit-run-before-change-functions files "stage")
345 (magit-run-git "add" arg (if files (cons "--" files) "."))
346 (when magit-auto-revert-mode
347 (mapc #'magit-turn-on-auto-revert-mode-if-desired files))
348 (magit-run-after-apply-functions files "stage"))
349
350 (defun magit-stage-untracked (&optional intent)
351 (let* ((section (magit-current-section))
352 (files (pcase (magit-diff-scope)
353 ('file (list (oref section value)))
354 ('files (magit-region-values nil t))
355 ('list (magit-untracked-files))))
356 plain repos)
357 (dolist (file files)
358 (if (and (not (file-symlink-p file))
359 (magit-git-repo-p file t))
360 (push file repos)
361 (push file plain)))
362 (magit-run-before-change-functions files "stage")
363 (when plain
364 (magit-run-git "add" (and intent "--intent-to-add")
365 "--" plain)
366 (when magit-auto-revert-mode
367 (mapc #'magit-turn-on-auto-revert-mode-if-desired plain)))
368 (dolist (repo repos)
369 (save-excursion
370 (when-let ((section (magit-get-section
371 `((file . ,repo) (untracked) (status)))))
372 (goto-char (oref section start))
373 (let* ((topdir (magit-toplevel))
374 (url (let ((default-directory
375 (file-name-as-directory (expand-file-name repo))))
376 (or (magit-get "remote" (magit-get-some-remote) "url")
377 (file-name-concat "." repo))))
378 (package
379 (and (boundp 'borg-user-emacs-directory)
380 (equal borg-user-emacs-directory topdir)
381 (file-name-nondirectory (directory-file-name repo)))))
382 (if (and package
383 (fboundp 'borg-assimilate)
384 (y-or-n-p (format "Also assimilate `%s' drone?" package)))
385 (borg-assimilate package url)
386 (magit-submodule-add-1
387 url repo (magit-submodule-read-name-for-path repo package))
388 (when (and package
389 (fboundp 'borg--sort-submodule-sections)
390 (fboundp 'borg--maybe-absorb-gitdir))
391 (borg--sort-submodule-sections
392 (expand-file-name ".gitmodules" topdir))
393 (let ((default-directory borg-user-emacs-directory))
394 (borg--maybe-absorb-gitdir package))))))))
395 (magit-run-after-apply-functions files "stage")))
396
397 (defvar magit-post-stage-hook-commands
398 (list #'magit-stage
399 #'magit-stage-files
400 #'magit-stage-modified
401 'magit-file-stage))
402
403 ;;;###autoload
404 (defun magit-run-post-stage-hook ()
405 (when (memq this-command magit-post-stage-hook-commands)
406 (magit-run-hook-with-benchmark 'magit-post-stage-hook)))
407
408 ;;;; Unstage
409
410 (defun magit-unstage ()
411 "Remove the change at point from the staging area."
412 (interactive)
413 (when-let ((s (magit-apply--get-selection))
414 (type (magit-diff-type))
415 (scope (magit-diff-scope)))
416 (pcase (list type scope
417 (magit-apply--ignore-whitespace-p s type scope))
418 (`(untracked ,_ ,_) (user-error "Cannot unstage untracked changes"))
419 (`(unstaged file ,_) (magit-unstage-intent (list (oref s value))))
420 (`(unstaged files ,_) (magit-unstage-intent (magit-region-values nil t)))
421 (`(unstaged ,_ ,_) (user-error "Already unstaged"))
422 (`(staged region ,_) (magit-apply-region s "--reverse" "--cached"))
423 (`(staged hunk ,_) (magit-apply-hunk s "--reverse" "--cached"))
424 (`(staged hunks ,_) (magit-apply-hunks s "--reverse" "--cached"))
425 ('(staged file t) (magit-apply-diff s "--reverse" "--cached"))
426 ('(staged files t) (magit-apply-diffs s "--reverse" "--cached"))
427 ('(staged list t) (magit-apply-diffs s "--reverse" "--cached"))
428 ('(staged file nil) (magit-unstage-1 (list (oref s value))))
429 ('(staged files nil) (magit-unstage-1 (magit-region-values nil t)))
430 ('(staged list nil) (magit-unstage-all))
431 (`(committed ,_ ,_) (if magit-unstage-committed
432 (magit-reverse-in-index)
433 (user-error "Cannot unstage committed changes")))
434 (`(undefined ,_ ,_) (user-error "Cannot unstage this change")))))
435
436 ;;;###autoload
437 (defun magit-unstage-files (files)
438 "Read one or more files and unstage all changes to those files."
439 (interactive
440 (let* ((choices (magit-staged-files))
441 (default (or (magit-section-value-if 'file)
442 (magit-file-relative-name)))
443 (default (car (member default choices))))
444 (list (magit-completing-read-multiple "Unstage file,s: " choices
445 nil t nil nil default))))
446 (magit-with-toplevel
447 (magit-unstage-1 files)))
448
449 (defun magit-unstage-1 (files)
450 (magit-run-before-change-functions files "unstage")
451 (if (magit-no-commit-p)
452 (magit-run-git "rm" "--cached" "--" files)
453 (magit-run-git "reset" "HEAD" "--" files))
454 (magit-run-after-apply-functions files "unstage"))
455
456 (defun magit-unstage-intent (files)
457 (if-let* ((staged (magit-staged-files))
458 (intent (seq-filter (##member % staged) files)))
459 (magit-unstage-1 intent)
460 (user-error "Already unstaged")))
461
462 ;;;###autoload
463 (defun magit-unstage-all ()
464 "Remove all changes from the staging area."
465 (interactive)
466 (unless (magit-anything-staged-p)
467 (user-error "Nothing to unstage"))
468 (when (or (magit-anything-unstaged-p)
469 (magit-untracked-files))
470 (magit-confirm 'unstage-all-changes))
471 (magit-run-before-change-functions nil "unstage")
472 (magit-run-git "reset" "HEAD" "--" magit-buffer-diff-files)
473 (magit-run-after-apply-functions nil "unstage"))
474
475 (defvar magit-post-unstage-hook-commands
476 (list #'magit-unstage
477 #'magit-unstage-files
478 #'magit-unstage-all
479 'magit-file-unstage))
480
481 ;;;###autoload
482 (defun magit-run-post-unstage-hook ()
483 (when (memq this-command magit-post-unstage-hook-commands)
484 (magit-run-hook-with-benchmark 'magit-post-unstage-hook)))
485
486 ;;;; Discard
487
488 (defun magit-discard ()
489 "Remove the change at point.
490
491 On a hunk or file with unresolved conflicts prompt which side to
492 keep (while discarding the other). If point is within the text
493 of a side, then keep that side without prompting."
494 (interactive)
495 (when$ (magit-apply--get-selection)
496 (pcase (list (magit-diff-type) (magit-diff-scope))
497 (`(committed ,_) (user-error "Cannot discard committed changes"))
498 (`(undefined ,_) (user-error "Cannot discard this change"))
499 (`(untracked list) (magit-discard-untracked))
500 (`(,_ region) (magit-discard-region $))
501 (`(,_ hunk) (magit-discard-hunk $))
502 (`(,_ hunks) (magit-discard-hunks $))
503 (`(,_ file) (magit-discard-file $))
504 (`(,_ files) (magit-discard-files $))
505 (`(,_ list) (magit-discard-files $)))))
506
507 (defun magit-discard-untracked ()
508 (magit-discard-files--delete
509 (magit-with-toplevel (magit-list-untracked-files))
510 nil)
511 (magit-refresh))
512
513 (defun magit-discard-region (section)
514 (magit-confirm 'discard "Discard region")
515 (magit-discard-apply section 'magit-apply-region))
516
517 (defun magit-discard-hunk (section)
518 (magit-confirm 'discard "Discard hunk")
519 (let ((file (magit-section-parent-value section)))
520 (pcase (cddr (car (magit-file-status file)))
521 ('(?U ?U) (magit-smerge-keep-current))
522 (_ (magit-discard-apply section #'magit-apply-hunk)))))
523
524 (defun magit-discard-hunks (sections)
525 (magit-confirm 'discard
526 (list "Discard %d hunks from %s"
527 (length sections)
528 (magit-section-parent-value (car sections))))
529 (magit-discard-apply sections #'magit-apply-hunks))
530
531 (defun magit-discard-apply (section:s apply)
532 (let ((primus (if (atom section:s) section:s (car section:s))))
533 (cond ((eq (magit-diff-type primus) 'unstaged)
534 (funcall apply section:s "--reverse"))
535 ((magit-anything-unstaged-p
536 nil (if (magit-file-section-p primus)
537 (oref primus value)
538 (magit-section-parent-value primus)))
539 (let ((magit-inhibit-refresh t))
540 (funcall apply section:s "--reverse" "--cached")
541 (funcall apply section:s "--reverse" "--reject"))
542 (magit-refresh))
543 ((funcall apply section:s "--reverse" "--index")))))
544
545 (defun magit-discard-file (section)
546 (magit-discard-files (list section)))
547
548 (defun magit-discard-files (sections)
549 (let ((auto-revert-verbose nil)
550 (type (magit-diff-type (car sections)))
551 (status (magit-file-status))
552 files delete resurrect rename discard discard-new resolve)
553 (dolist (section sections)
554 (let ((file (oref section value)))
555 (push file files)
556 (pcase (cons (pcase type
557 (`staged ?X)
558 (`unstaged ?Y)
559 (`untracked ?Z))
560 (cddr (assoc file status)))
561 ('(?Z) (dolist (f (magit-untracked-files nil file))
562 (push f delete)))
563 ((or '(?Z ?? ??) '(?Z ?! ?!)) (push file delete))
564 ('(?Z ?D ? ) (push file delete))
565 (`(,_ ?D ?D) (push file resolve))
566 ((or `(,_ ?U ,_) `(,_ ,_ ?U)) (push file resolve))
567 (`(,_ ?A ?A) (push file resolve))
568 (`(?X ?M ,(or ? ?M ?D)) (push section discard))
569 (`(?Y ,_ ?M ) (push section discard))
570 ('(?X ?A ?M ) (push file discard-new))
571 ('(?X ?C ?M ) (push file discard-new))
572 (`(?X ?A ,(or ? ?D)) (push file delete))
573 (`(?X ?C ,(or ? ?D)) (push file delete))
574 (`(?X ?D ,(or ? ?M )) (push file resurrect))
575 (`(?Y ,_ ?D ) (push file resurrect))
576 (`(?X ?R ,(or ? ?M ?D)) (push file rename)))))
577 (unwind-protect
578 (let ((magit-inhibit-refresh t))
579 (magit-run-before-change-functions files "discard")
580 (when resolve
581 (magit-discard-files--resolve (nreverse resolve)))
582 (when resurrect
583 (magit-discard-files--resurrect (nreverse resurrect)))
584 (when delete
585 (magit-discard-files--delete (nreverse delete) status))
586 (when rename
587 (magit-discard-files--rename (nreverse rename) status))
588 (when (or discard discard-new)
589 (magit-discard-files--discard (nreverse discard)
590 (nreverse discard-new)))
591 (magit-run-after-apply-functions files "discard"))
592 (magit-refresh))))
593
594 (defun magit-discard-files--resolve (files)
595 (if-let ((arg (and (cdr files)
596 (magit-read-char-case
597 (format "For these %d files\n%s\ncheckout:\n"
598 (length files)
599 (mapconcat (##concat " " %) files "\n"))
600 t
601 (?o "[o]ur stage" "--ours")
602 (?t "[t]heir stage" "--theirs")
603 (?c "[c]onflict" "--merge")
604 (?i "decide [i]ndividually" nil)))))
605 (dolist (file files)
606 (magit-checkout-stage file arg))
607 (dolist (file files)
608 (magit-checkout-stage file (magit-checkout-read-stage file)))))
609
610 (defun magit-discard-files--resurrect (files)
611 (magit-confirm-files 'resurrect files)
612 (if (eq (magit-diff-type) 'staged)
613 (magit-call-git "reset" "--" files)
614 (magit-call-git "checkout" "--" files)))
615
616 (defun magit-discard-files--delete (files status)
617 (magit-confirm-files (if magit-delete-by-moving-to-trash 'trash 'delete)
618 files)
619 (let ((delete-by-moving-to-trash magit-delete-by-moving-to-trash))
620 (dolist (file files)
621 (when (string-match-p "\\`\\\\?~" file)
622 (error "Refusing to delete %S, too dangerous" file))
623 (pcase (nth 3 (assoc file status))
624 ((guard (memq (magit-diff-type) '(unstaged untracked)))
625 (dired-delete-file file dired-recursive-deletes
626 magit-delete-by-moving-to-trash)
627 (dired-clean-up-after-deletion file))
628 (?\s (delete-file file t)
629 (magit-call-git "rm" "--cached" "--" file))
630 (?M (let ((temp (magit-git-string "checkout-index" "--temp" file)))
631 (string-match
632 (format "\\(.+?\\)\t%s" (regexp-quote file)) temp)
633 (rename-file (match-str 1 temp)
634 (setq temp (concat file ".~{index}~")))
635 (delete-file temp t))
636 (magit-call-git "rm" "--cached" "--force" "--" file))
637 (?D (magit-call-git "checkout" "--" file)
638 (delete-file file t)
639 (magit-call-git "rm" "--cached" "--force" "--" file))))))
640
641 (defun magit-discard-files--rename (files status)
642 (magit-confirm 'rename "Undo rename %s" "Undo %d renames" nil
643 (mapcar (lambda (file)
644 (setq file (assoc file status))
645 (format "%s -> %s" (cadr file) (car file)))
646 files))
647 (dolist (file files)
648 (let ((orig (cadr (assoc file status))))
649 (cond ((file-exists-p file)
650 (when$ (file-name-directory orig)
651 (make-directory $ t))
652 (magit-call-git "mv" file orig))
653 (t
654 (magit-call-git "rm" "--cached" "--" file)
655 (magit-call-git "reset" "--" orig))))))
656
657 (defun magit-discard-files--discard (sections new-files)
658 (let ((files (mapcar (##oref % value) sections)))
659 (magit-confirm-files 'discard (append files new-files)
660 (format "Discard %s changes in" (magit-diff-type)))
661 (if (eq (magit-diff-type (car sections)) 'unstaged)
662 (magit-call-git "checkout" "--" files)
663 (when new-files
664 (magit-call-git "add" "--" new-files)
665 (magit-call-git "reset" "--" new-files))
666 (let ((binaries (magit-binary-files "--cached")))
667 (when binaries
668 (setq sections
669 (seq-remove (##member (oref % value) binaries)
670 sections)))
671 (when sections
672 (magit-discard-apply sections #'magit-apply-diffs))
673 (when binaries
674 (let ((modified (magit-unstaged-files t)))
675 (setq binaries (magit--separate (##member % modified) binaries)))
676 (when (cadr binaries)
677 (magit-call-git "reset" "--" (cadr binaries)))
678 (when (car binaries)
679 (user-error
680 (concat
681 "Cannot discard staged changes to binary files, "
682 "which also have unstaged changes. Unstage instead."))))))))
683
684 ;;;; Reverse
685
686 (defun magit-reverse (&rest args)
687 "Reverse the change at point in the working tree.
688 With a prefix argument fallback to a 3-way merge. Doing
689 so causes the change to be applied to the index as well."
690 (interactive (and current-prefix-arg (list "--3way")))
691 (when$ (magit-apply--get-selection)
692 (pcase (list (magit-diff-type) (magit-diff-scope))
693 (`(untracked ,_) (user-error "Cannot reverse untracked changes"))
694 (`(unstaged ,_) (user-error "Cannot reverse unstaged changes"))
695 (`(,_ region) (magit-reverse-region $ args))
696 (`(,_ hunk) (magit-reverse-hunk $ args))
697 (`(,_ hunks) (magit-reverse-hunks $ args))
698 (`(,_ file) (magit-reverse-file $ args))
699 (`(,_ files) (magit-reverse-files $ args))
700 (`(,_ list) (magit-reverse-files $ args)))))
701
702 (defun magit-reverse-region (section args)
703 (magit-confirm 'reverse "Reverse region")
704 (magit-reverse-apply section #'magit-apply-region args))
705
706 (defun magit-reverse-hunk (section args)
707 (magit-confirm 'reverse "Reverse hunk")
708 (magit-reverse-apply section #'magit-apply-hunk args))
709
710 (defun magit-reverse-hunks (sections args)
711 (magit-confirm 'reverse
712 (list "Reverse %d hunks from %s"
713 (length sections)
714 (magit-section-parent-value (car sections))))
715 (magit-reverse-apply sections #'magit-apply-hunks args))
716
717 (defun magit-reverse-file (section args)
718 (magit-reverse-files (list section) args))
719
720 (defun magit-reverse-files (sections args)
721 (pcase-let ((`(,binaries ,sections)
722 (let ((bs (magit-binary-files
723 (cond ((derived-mode-p 'magit-revision-mode)
724 magit-buffer-diff-range)
725 ((derived-mode-p 'magit-diff-mode)
726 magit-buffer-diff-range)
727 ("--cached")))))
728 (magit--separate (##member (oref % value) bs)
729 sections))))
730 (magit-confirm-files 'reverse (mapcar (##oref % value) sections))
731 (cond ((length= sections 1)
732 (magit-reverse-apply (car sections) #'magit-apply-diff args))
733 (sections
734 (magit-reverse-apply sections #'magit-apply-diffs args)))
735 (when binaries
736 (user-error "Cannot reverse binary files"))))
737
738 (defun magit-reverse-apply (section:s apply args)
739 (funcall apply section:s "--reverse" args
740 (and (not magit-reverse-atomically)
741 (not (member "--3way" args))
742 "--reject")))
743
744 (defun magit-reverse-in-index (&rest args)
745 "Reverse the change at point in the index but not the working tree.
746
747 Use this command to extract a change from `HEAD', while leaving
748 it in the working tree, so that it can later be committed using
749 a separate commit. A typical workflow would be:
750
751 0. Optionally make sure that there are no uncommitted changes.
752 1. Visit the `HEAD' commit and navigate to the change that should
753 not have been included in that commit.
754 2. Type \"u\" (`magit-unstage') to reverse it in the index.
755 This assumes that `magit-unstage-committed' is non-nil.
756 3. Type \"c e\" to extend `HEAD' with the staged changes,
757 including those that were already staged before.
758 4. Optionally stage the remaining changes using \"s\" or \"S\"
759 and then type \"c c\" to create a new commit."
760 (interactive)
761 (magit-reverse (cons "--cached" args)))
762
763 ;;;; Copy
764
765 (defun magit-copy-diff-as-kill ()
766 "Save the diff at point to the kill ring.
767 This command can be used on staged and unstaged changes, and anywhere
768 `magit-apply' can be used. Additionally, if there is no diff at point
769 but `magit-buffer-revision' is non-nil, or there is a commit at point,
770 the diff for that commit is saved."
771 (interactive)
772 (cond-let
773 ([$ (magit-apply--get-selection t)]
774 (pcase (list (magit-diff-type) (magit-diff-scope))
775 (`(untracked ,(or 'file 'files))
776 (user-error "Cannot copy this as a diff"))
777 (`(rebase-sequence file)
778 (user-error "Cannot copy this as a diff"))
779 (`(,_ region) (magit-apply-region $))
780 (`(,_ hunk) (magit-apply-hunk $))
781 (`(,_ hunks) (magit-apply-hunks $))
782 (`(,_ file) (magit-apply-diff $))
783 (`(,_ files) (magit-apply-diffs $))))
784 ([rev (or (magit-commit-at-point) magit-buffer-revision)]
785 (magit--with-temp-process-buffer
786 (magit-git-insert "show" "-p" "--format=" rev)
787 (kill-new (buffer-string))))
788 ((user-error "Cannot copy this as a diff"))))
789
790 ;;; Smerge Support
791
792 (defun magit-smerge-keep-current ()
793 "Keep the current version of the conflict at point."
794 (interactive)
795 (magit-call-smerge #'smerge-keep-current))
796
797 (defun magit-smerge-keep-upper ()
798 "Keep the upper/our version of the conflict at point."
799 (interactive)
800 (magit-call-smerge #'smerge-keep-upper))
801
802 (defun magit-smerge-keep-base ()
803 "Keep the base version of the conflict at point."
804 (interactive)
805 (magit-call-smerge #'smerge-keep-base))
806
807 (defun magit-smerge-keep-lower ()
808 "Keep the lower/their version of the conflict at point."
809 (interactive)
810 (magit-call-smerge #'smerge-keep-lower))
811
812 (defun magit-smerge-keep-all ()
813 "Keep all versions of the conflict at point."
814 (interactive)
815 (magit-call-smerge #'smerge-keep-all))
816
817 (defun magit-call-smerge (fn)
818 (pcase-let* ((file (magit-file-at-point t t))
819 (keep (get-file-buffer file))
820 (`(,buf ,pos) (magit-diff-visit-file--noselect))
821 (keep (eq keep buf)))
822 (with-current-buffer buf
823 (save-excursion
824 (save-restriction
825 (unless (<= (point-min) pos (point-max))
826 (widen))
827 (goto-char pos)
828 (condition-case nil
829 (smerge-match-conflict)
830 (error
831 (if (eq fn #'smerge-keep-current)
832 (when (eq this-command #'magit-discard)
833 (re-search-forward smerge-begin-re nil t)
834 (setq fn
835 (magit-read-char-case "Keep side: " t
836 (?o "[o]urs/upper" #'smerge-keep-upper)
837 (?b "[b]ase" #'smerge-keep-base)
838 (?t "[t]heirs/lower" #'smerge-keep-lower))))
839 (re-search-forward smerge-begin-re nil t))))
840 (funcall fn)))
841 (when (and keep (magit-anything-unmerged-p file))
842 (smerge-start-session))
843 (save-buffer))
844 (unless keep
845 (kill-buffer buf))
846 (magit-refresh)))
847
848 ;;; _
849 (provide 'magit-apply)
850 ;; Local Variables:
851 ;; read-symbol-shorthands: (
852 ;; ("and$" . "cond-let--and$")
853 ;; ("thread$" . "cond-let--thread$")
854 ;; ("when$" . "cond-let--when$")
855 ;; ("and-let*" . "cond-let--and-let*")
856 ;; ("and-let" . "cond-let--and-let")
857 ;; ("if-let*" . "cond-let--if-let*")
858 ;; ("if-let" . "cond-let--if-let")
859 ;; ("when-let*" . "cond-let--when-let*")
860 ;; ("when-let" . "cond-let--when-let")
861 ;; ("while-let*" . "cond-let--while-let*")
862 ;; ("while-let" . "cond-let--while-let")
863 ;; ("match-string" . "match-string")
864 ;; ("match-str" . "match-string-no-properties"))
865 ;; End:
866 ;;; magit-apply.el ends here