commit | author | age
|
5cb5f7
|
1 |
;;; magit-apply.el --- apply Git diffs -*- lexical-binding: t -*- |
C |
2 |
|
|
3 |
;; Copyright (C) 2010-2018 The Magit Project Contributors |
|
4 |
;; |
|
5 |
;; You should have received a copy of the AUTHORS.md file which |
|
6 |
;; lists all contributors. If not, see http://magit.vc/authors. |
|
7 |
|
|
8 |
;; Author: Jonas Bernoulli <jonas@bernoul.li> |
|
9 |
;; Maintainer: Jonas Bernoulli <jonas@bernoul.li> |
|
10 |
|
|
11 |
;; Magit is free software; you can redistribute it and/or modify it |
|
12 |
;; under the terms of the GNU General Public License as published by |
|
13 |
;; the Free Software Foundation; either version 3, or (at your option) |
|
14 |
;; any later version. |
|
15 |
;; |
|
16 |
;; Magit is distributed in the hope that it will be useful, but WITHOUT |
|
17 |
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY |
|
18 |
;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public |
|
19 |
;; License for more details. |
|
20 |
;; |
|
21 |
;; You should have received a copy of the GNU General Public License |
|
22 |
;; along with Magit. If not, see http://www.gnu.org/licenses. |
|
23 |
|
|
24 |
;;; Commentary: |
|
25 |
|
|
26 |
;; This library implements commands for applying Git diffs or parts |
|
27 |
;; of such a diff. The supported "apply variants" are apply, stage, |
|
28 |
;; unstage, discard, and reverse - more than Git itself knows about, |
|
29 |
;; at least at the porcelain level. |
|
30 |
|
|
31 |
;;; Code: |
|
32 |
|
|
33 |
(eval-when-compile |
|
34 |
(require 'subr-x)) |
|
35 |
|
|
36 |
(require 'magit-core) |
|
37 |
(require 'magit-diff) |
|
38 |
(require 'magit-wip) |
|
39 |
|
|
40 |
;; For `magit-apply' |
|
41 |
(declare-function magit-am-popup "magit-sequence" (&optional arg)) |
|
42 |
(declare-function magit-patch-apply-popup "magit-files" (&optional arg)) |
|
43 |
;; For `magit-discard-files' |
|
44 |
(declare-function magit-checkout-stage "magit-merge" (file arg)) |
|
45 |
(declare-function magit-checkout-read-stage "magit-merge" (file)) |
|
46 |
(defvar auto-revert-verbose) |
|
47 |
;; For `magit-stage-untracked' |
|
48 |
(declare-function magit-submodule-add "magit-submodule" |
|
49 |
(url &optional path name args)) |
|
50 |
(declare-function magit-submodule-read-name-for-path "magit-submodule" |
|
51 |
(path &optional prefer-short)) |
|
52 |
(declare-function borg--maybe-absorb-gitdir "borg" (pkg)) |
|
53 |
(declare-function borg--sort-submodule-sections "borg" (file)) |
|
54 |
(defvar borg-user-emacs-directory) |
|
55 |
|
|
56 |
;;; Options |
|
57 |
|
|
58 |
(defcustom magit-delete-by-moving-to-trash t |
|
59 |
"Whether Magit uses the system's trash can. |
|
60 |
|
|
61 |
You should absolutely not disable this and also remove `discard' |
|
62 |
from `magit-no-confirm'. You shouldn't do that even if you have |
|
63 |
all of the Magit-Wip modes enabled, because those modes do not |
|
64 |
track any files that are not tracked in the proper branch." |
|
65 |
:package-version '(magit . "2.1.0") |
|
66 |
:group 'magit-essentials |
|
67 |
:type 'boolean) |
|
68 |
|
|
69 |
(defcustom magit-unstage-committed t |
|
70 |
"Whether unstaging a committed change reverts it instead. |
|
71 |
|
|
72 |
A committed change cannot be unstaged, because staging and |
|
73 |
unstaging are actions that are concerned with the differences |
|
74 |
between the index and the working tree, not with committed |
|
75 |
changes. |
|
76 |
|
|
77 |
If this option is non-nil (the default), then typing \"u\" |
|
78 |
\(`magit-unstage') on a committed change, causes it to be |
|
79 |
reversed in the index but not the working tree. For more |
|
80 |
information see command `magit-reverse-in-index'." |
|
81 |
:package-version '(magit . "2.4.1") |
|
82 |
:group 'magit-commands |
|
83 |
:type 'boolean) |
|
84 |
|
|
85 |
(defcustom magit-reverse-atomically nil |
|
86 |
"Whether to reverse changes atomically. |
|
87 |
|
|
88 |
If some changes can be reversed while others cannot, then nothing |
|
89 |
is reversed if the value of this option is non-nil. But when it |
|
90 |
is nil, then the changes that can be reversed are reversed and |
|
91 |
for the other changes diff files are created that contain the |
|
92 |
rejected reversals." |
|
93 |
:package-version '(magit . "2.7.0") |
|
94 |
:group 'magit-commands |
|
95 |
:type 'boolean) |
|
96 |
|
|
97 |
(defcustom magit-post-stage-hook nil |
|
98 |
"Hook run after staging changes. |
|
99 |
This hook is run by `magit-refresh' if `this-command' |
|
100 |
is a member of `magit-post-stage-hook-commands'." |
|
101 |
:package-version '(magit . "2.90.0") |
|
102 |
:group 'magit-commands |
|
103 |
:type 'hook) |
|
104 |
|
|
105 |
(defvar magit-post-stage-hook-commands |
|
106 |
'(magit-stage magit-stage-file magit-stage-modified)) |
|
107 |
|
|
108 |
(defcustom magit-post-unstage-hook nil |
|
109 |
"Hook run after unstaging changes. |
|
110 |
This hook is run by `magit-refresh' if `this-command' |
|
111 |
is a member of `magit-post-unstage-hook-commands'." |
|
112 |
:package-version '(magit . "2.90.0") |
|
113 |
:group 'magit-commands |
|
114 |
:type 'hook) |
|
115 |
|
|
116 |
(defvar magit-post-unstage-hook-commands |
|
117 |
'(magit-unstage magit-unstage-file magit-unstage-all)) |
|
118 |
|
|
119 |
;;; Commands |
|
120 |
;;;; Apply |
|
121 |
|
|
122 |
(defun magit-apply (&rest args) |
|
123 |
"Apply the change at point to the working tree. |
|
124 |
With a prefix argument fallback to a 3-way merge. Doing |
|
125 |
so causes the change to be applied to the index as well." |
|
126 |
(interactive (and current-prefix-arg (list "--3way"))) |
|
127 |
(--when-let (magit-apply--get-selection) |
|
128 |
(pcase (list (magit-diff-type) (magit-diff-scope)) |
|
129 |
(`(,(or `unstaged `staged) ,_) |
|
130 |
(user-error "Change is already in the working tree")) |
|
131 |
(`(untracked ,(or `file `files)) |
|
132 |
(magit-am-popup)) |
|
133 |
(`(,_ region) (magit-apply-region it args)) |
|
134 |
(`(,_ hunk) (magit-apply-hunk it args)) |
|
135 |
(`(,_ hunks) (magit-apply-hunks it args)) |
|
136 |
(`(rebase-sequence file) (magit-patch-apply-popup)) |
|
137 |
(`(,_ file) (magit-apply-diff it args)) |
|
138 |
(`(,_ files) (magit-apply-diffs it args))))) |
|
139 |
|
|
140 |
(defun magit-apply--section-content (section) |
|
141 |
(buffer-substring-no-properties (if (magit-hunk-section-p section) |
|
142 |
(oref section start) |
|
143 |
(oref section content)) |
|
144 |
(oref section end))) |
|
145 |
|
|
146 |
(defun magit-apply-diffs (sections &rest args) |
|
147 |
(setq sections (magit-apply--get-diffs sections)) |
|
148 |
(magit-apply-patch sections args |
|
149 |
(mapconcat |
|
150 |
(lambda (s) |
|
151 |
(concat (magit-diff-file-header s) |
|
152 |
(magit-apply--section-content s))) |
|
153 |
sections ""))) |
|
154 |
|
|
155 |
(defun magit-apply-diff (section &rest args) |
|
156 |
(setq section (car (magit-apply--get-diffs (list section)))) |
|
157 |
(magit-apply-patch section args |
|
158 |
(concat (magit-diff-file-header section) |
|
159 |
(magit-apply--section-content section)))) |
|
160 |
|
|
161 |
(defun magit-apply-hunks (sections &rest args) |
|
162 |
(let ((section (oref (car sections) parent))) |
|
163 |
(when (string-match "^diff --cc" (oref section value)) |
|
164 |
(user-error "Cannot un-/stage resolution hunks. Stage the whole file")) |
|
165 |
(magit-apply-patch section args |
|
166 |
(concat (oref section header) |
|
167 |
(mapconcat 'magit-apply--section-content |
|
168 |
sections ""))))) |
|
169 |
|
|
170 |
(defun magit-apply-hunk (section &rest args) |
|
171 |
(when (string-match "^diff --cc" (magit-section-parent-value section)) |
|
172 |
(user-error "Cannot un-/stage resolution hunks. Stage the whole file")) |
|
173 |
(magit-apply-patch (oref section parent) args |
|
174 |
(concat (magit-diff-file-header section) |
|
175 |
(magit-apply--section-content section)))) |
|
176 |
|
|
177 |
(defun magit-apply-region (section &rest args) |
|
178 |
(unless (magit-diff-context-p) |
|
179 |
(user-error "Not enough context to apply region. Increase the context")) |
|
180 |
(when (string-match "^diff --cc" (magit-section-parent-value section)) |
|
181 |
(user-error "Cannot un-/stage resolution hunks. Stage the whole file")) |
|
182 |
(magit-apply-patch (oref section parent) args |
|
183 |
(concat (magit-diff-file-header section) |
|
184 |
(magit-diff-hunk-region-patch section args)))) |
|
185 |
|
|
186 |
(defun magit-apply-patch (section:s args patch) |
|
187 |
(let* ((files (if (atom section:s) |
|
188 |
(list (oref section:s value)) |
|
189 |
(--map (oref it value) section:s))) |
|
190 |
(command (symbol-name this-command)) |
|
191 |
(command (if (and command (string-match "^magit-\\([^-]+\\)" command)) |
|
192 |
(match-string 1 command) |
|
193 |
"apply")) |
|
194 |
(no-context (not (magit-diff-context-p))) |
|
195 |
(ignore-context (magit-diff-ignore-any-space-p))) |
|
196 |
(when (and magit-wip-before-change-mode (not inhibit-magit-refresh)) |
|
197 |
(magit-wip-commit-before-change files (concat " before " command))) |
|
198 |
(with-temp-buffer |
|
199 |
(insert patch) |
|
200 |
(magit-run-git-with-input |
|
201 |
"apply" args "-p0" |
|
202 |
(and no-context "--unidiff-zero") |
|
203 |
(and ignore-context "-C0") |
|
204 |
"--ignore-space-change" "-")) |
|
205 |
(unless inhibit-magit-refresh |
|
206 |
(when magit-wip-after-apply-mode |
|
207 |
(magit-wip-commit-after-apply files (concat " after " command))) |
|
208 |
(magit-refresh)))) |
|
209 |
|
|
210 |
(defun magit-apply--get-selection () |
|
211 |
(or (magit-region-sections '(hunk file module) t) |
|
212 |
(let ((section (magit-current-section))) |
|
213 |
(pcase (oref section type) |
|
214 |
((or `hunk `file `module) section) |
|
215 |
((or `staged `unstaged `untracked |
|
216 |
`stashed-index `stashed-worktree `stashed-untracked) |
|
217 |
(oref section children)) |
|
218 |
(_ (user-error "Cannot apply this, it's not a change")))))) |
|
219 |
|
|
220 |
(defun magit-apply--get-diffs (sections) |
|
221 |
(magit-section-case |
|
222 |
([file diffstat] |
|
223 |
(--map (or (magit-get-section |
|
224 |
(append `((file . ,(oref it value))) |
|
225 |
(magit-section-ident magit-root-section))) |
|
226 |
(error "Cannot get required diff headers")) |
|
227 |
sections)) |
|
228 |
(t sections))) |
|
229 |
|
|
230 |
(defun magit-apply--diff-ignores-whitespace-p () |
|
231 |
(and (cl-intersection (if (derived-mode-p 'magit-diff-mode) |
|
232 |
(nth 2 magit-refresh-args) |
|
233 |
magit-diff-section-arguments) |
|
234 |
'("--ignore-space-at-eol" |
|
235 |
"--ignore-space-change" |
|
236 |
"--ignore-all-space" |
|
237 |
"--ignore-blank-lines") |
|
238 |
:test #'equal) |
|
239 |
t)) |
|
240 |
|
|
241 |
;;;; Stage |
|
242 |
|
|
243 |
(defun magit-stage (&optional intent) |
|
244 |
"Add the change at point to the staging area. |
|
245 |
With a prefix argument, INTENT, and an untracked file (or files) |
|
246 |
at point, stage the file but not its content." |
|
247 |
(interactive "P") |
|
248 |
(--if-let (and (derived-mode-p 'magit-mode) (magit-apply--get-selection)) |
|
249 |
(pcase (list (magit-diff-type) |
|
250 |
(magit-diff-scope) |
|
251 |
(magit-apply--diff-ignores-whitespace-p)) |
|
252 |
(`(untracked ,_ ,_) (magit-stage-untracked intent)) |
|
253 |
(`(unstaged region ,_) (magit-apply-region it "--cached")) |
|
254 |
(`(unstaged hunk ,_) (magit-apply-hunk it "--cached")) |
|
255 |
(`(unstaged hunks ,_) (magit-apply-hunks it "--cached")) |
|
256 |
(`(unstaged file t) (magit-apply-diff it "--cached")) |
|
257 |
(`(unstaged files t) (magit-apply-diffs it "--cached")) |
|
258 |
(`(unstaged list t) (magit-apply-diffs it "--cached")) |
|
259 |
(`(unstaged file nil) (magit-stage-1 "-u" (list (oref it value)))) |
|
260 |
(`(unstaged files nil) (magit-stage-1 "-u" (magit-region-values nil t))) |
|
261 |
(`(unstaged list nil) (magit-stage-modified)) |
|
262 |
(`(staged ,_ ,_) (user-error "Already staged")) |
|
263 |
(`(committed ,_ ,_) (user-error "Cannot stage committed changes")) |
|
264 |
(`(undefined ,_ ,_) (user-error "Cannot stage this change"))) |
|
265 |
(call-interactively 'magit-stage-file))) |
|
266 |
|
|
267 |
;;;###autoload |
|
268 |
(defun magit-stage-file (file) |
|
269 |
"Stage all changes to FILE. |
|
270 |
With a prefix argument or when there is no file at point ask for |
|
271 |
the file to be staged. Otherwise stage the file at point without |
|
272 |
requiring confirmation." |
|
273 |
(interactive |
|
274 |
(let* ((atpoint (magit-section-value-if 'file)) |
|
275 |
(current (magit-file-relative-name)) |
|
276 |
(choices (nconc (magit-unstaged-files) |
|
277 |
(magit-untracked-files))) |
|
278 |
(default (car (member (or atpoint current) choices)))) |
|
279 |
(list (if (or current-prefix-arg (not default)) |
|
280 |
(magit-completing-read "Stage file" choices |
|
281 |
nil t nil nil default) |
|
282 |
default)))) |
|
283 |
(magit-with-toplevel |
|
284 |
(magit-stage-1 nil (list file)))) |
|
285 |
|
|
286 |
;;;###autoload |
|
287 |
(defun magit-stage-modified (&optional all) |
|
288 |
"Stage all changes to files modified in the worktree. |
|
289 |
Stage all new content of tracked files and remove tracked files |
|
290 |
that no longer exist in the working tree from the index also. |
|
291 |
With a prefix argument also stage previously untracked (but not |
|
292 |
ignored) files." |
|
293 |
(interactive "P") |
|
294 |
(when (magit-anything-staged-p) |
|
295 |
(magit-confirm 'stage-all-changes)) |
|
296 |
(magit-with-toplevel |
|
297 |
(magit-stage-1 (if all "--all" "-u")))) |
|
298 |
|
|
299 |
(defun magit-stage-1 (arg &optional files) |
|
300 |
(magit-wip-commit-before-change files " before stage") |
|
301 |
(magit-run-git "add" arg (if files (cons "--" files) ".")) |
|
302 |
(when magit-auto-revert-mode |
|
303 |
(mapc #'magit-turn-on-auto-revert-mode-if-desired files)) |
|
304 |
(magit-wip-commit-after-apply files " after stage")) |
|
305 |
|
|
306 |
(defun magit-stage-untracked (&optional intent) |
|
307 |
(let* ((section (magit-current-section)) |
|
308 |
(files (pcase (magit-diff-scope) |
|
309 |
(`file (list (oref section value))) |
|
310 |
(`files (magit-region-values nil t)) |
|
311 |
(`list (magit-untracked-files)))) |
|
312 |
plain repos) |
|
313 |
(dolist (file files) |
|
314 |
(if (and (not (file-symlink-p file)) |
|
315 |
(magit-git-repo-p file t)) |
|
316 |
(push file repos) |
|
317 |
(push file plain))) |
|
318 |
(magit-wip-commit-before-change files " before stage") |
|
319 |
(when plain |
|
320 |
(magit-run-git "add" (and intent "--intent-to-add") |
|
321 |
"--" plain) |
|
322 |
(when magit-auto-revert-mode |
|
323 |
(mapc #'magit-turn-on-auto-revert-mode-if-desired plain))) |
|
324 |
(dolist (repo repos) |
|
325 |
(save-excursion |
|
326 |
(goto-char (oref (magit-get-section |
|
327 |
`((file . ,repo) (untracked) (status))) |
|
328 |
start)) |
|
329 |
(let* ((topdir (magit-toplevel)) |
|
330 |
(package |
|
331 |
(and (equal (bound-and-true-p borg-user-emacs-directory) |
|
332 |
topdir) |
|
333 |
(file-name-nondirectory (directory-file-name repo))))) |
|
334 |
(magit-submodule-add |
|
335 |
(let ((default-directory |
|
336 |
(file-name-as-directory (expand-file-name repo)))) |
|
337 |
(or (magit-get "remote" (magit-get-some-remote) "url") |
|
338 |
(concat (file-name-as-directory ".") repo))) |
|
339 |
repo |
|
340 |
(magit-submodule-read-name-for-path repo package)) |
|
341 |
(when package |
|
342 |
(borg--sort-submodule-sections |
|
343 |
(expand-file-name ".gitmodules" topdir)) |
|
344 |
(let ((default-directory borg-user-emacs-directory)) |
|
345 |
(borg--maybe-absorb-gitdir package)) |
|
346 |
(when (and (y-or-n-p |
|
347 |
(format "Also build and activate `%s' drone?" package)) |
|
348 |
(fboundp 'borg-build) |
|
349 |
(fboundp 'borg-activate)) |
|
350 |
(borg-build package) |
|
351 |
(borg-activate package)))))) |
|
352 |
(magit-wip-commit-after-apply files " after stage"))) |
|
353 |
|
|
354 |
;;;; Unstage |
|
355 |
|
|
356 |
(defun magit-unstage () |
|
357 |
"Remove the change at point from the staging area." |
|
358 |
(interactive) |
|
359 |
(--when-let (magit-apply--get-selection) |
|
360 |
(pcase (list (magit-diff-type) |
|
361 |
(magit-diff-scope) |
|
362 |
(magit-apply--diff-ignores-whitespace-p)) |
|
363 |
(`(untracked ,_ ,_) (user-error "Cannot unstage untracked changes")) |
|
364 |
(`(unstaged ,_ ,_) (user-error "Already unstaged")) |
|
365 |
(`(staged region ,_) (magit-apply-region it "--reverse" "--cached")) |
|
366 |
(`(staged hunk ,_) (magit-apply-hunk it "--reverse" "--cached")) |
|
367 |
(`(staged hunks ,_) (magit-apply-hunks it "--reverse" "--cached")) |
|
368 |
(`(staged file t) (magit-apply-diff it "--reverse" "--cached")) |
|
369 |
(`(staged files t) (magit-apply-diffs it "--reverse" "--cached")) |
|
370 |
(`(staged list t) (magit-apply-diffs it "--reverse" "--cached")) |
|
371 |
(`(staged file nil) (magit-unstage-1 (list (oref it value)))) |
|
372 |
(`(staged files nil) (magit-unstage-1 (magit-region-values nil t))) |
|
373 |
(`(staged list nil) (magit-unstage-all)) |
|
374 |
(`(committed ,_ ,_) (if magit-unstage-committed |
|
375 |
(magit-reverse-in-index) |
|
376 |
(user-error "Cannot unstage committed changes"))) |
|
377 |
(`(undefined ,_ ,_) (user-error "Cannot unstage this change"))))) |
|
378 |
|
|
379 |
;;;###autoload |
|
380 |
(defun magit-unstage-file (file) |
|
381 |
"Unstage all changes to FILE. |
|
382 |
With a prefix argument or when there is no file at point ask for |
|
383 |
the file to be unstaged. Otherwise unstage the file at point |
|
384 |
without requiring confirmation." |
|
385 |
(interactive |
|
386 |
(let* ((atpoint (magit-section-value-if 'file)) |
|
387 |
(current (magit-file-relative-name)) |
|
388 |
(choices (magit-staged-files)) |
|
389 |
(default (car (member (or atpoint current) choices)))) |
|
390 |
(list (if (or current-prefix-arg (not default)) |
|
391 |
(magit-completing-read "Unstage file" choices |
|
392 |
nil t nil nil default) |
|
393 |
default)))) |
|
394 |
(magit-with-toplevel |
|
395 |
(magit-unstage-1 (list file)))) |
|
396 |
|
|
397 |
(defun magit-unstage-1 (files) |
|
398 |
(magit-wip-commit-before-change files " before unstage") |
|
399 |
(if (magit-no-commit-p) |
|
400 |
(magit-run-git "rm" "--cached" "--" files) |
|
401 |
(magit-run-git "reset" "HEAD" "--" files)) |
|
402 |
(magit-wip-commit-after-apply files " after unstage")) |
|
403 |
|
|
404 |
;;;###autoload |
|
405 |
(defun magit-unstage-all () |
|
406 |
"Remove all changes from the staging area." |
|
407 |
(interactive) |
|
408 |
(when (or (magit-anything-unstaged-p) |
|
409 |
(magit-untracked-files)) |
|
410 |
(magit-confirm 'unstage-all-changes)) |
|
411 |
(magit-wip-commit-before-change nil " before unstage") |
|
412 |
(magit-run-git "reset" "HEAD" "--") |
|
413 |
(magit-wip-commit-after-apply nil " after unstage")) |
|
414 |
|
|
415 |
;;;; Discard |
|
416 |
|
|
417 |
(defun magit-discard () |
|
418 |
"Remove the change at point." |
|
419 |
(interactive) |
|
420 |
(--when-let (magit-apply--get-selection) |
|
421 |
(pcase (list (magit-diff-type) (magit-diff-scope)) |
|
422 |
(`(committed ,_) (user-error "Cannot discard committed changes")) |
|
423 |
(`(undefined ,_) (user-error "Cannot discard this change")) |
|
424 |
(`(,_ region) (magit-discard-region it)) |
|
425 |
(`(,_ hunk) (magit-discard-hunk it)) |
|
426 |
(`(,_ hunks) (magit-discard-hunks it)) |
|
427 |
(`(,_ file) (magit-discard-file it)) |
|
428 |
(`(,_ files) (magit-discard-files it)) |
|
429 |
(`(,_ list) (magit-discard-files it))))) |
|
430 |
|
|
431 |
(defun magit-discard-region (section) |
|
432 |
(magit-confirm 'discard "Discard region") |
|
433 |
(magit-discard-apply section 'magit-apply-region)) |
|
434 |
|
|
435 |
(defun magit-discard-hunk (section) |
|
436 |
(magit-confirm 'discard "Discard hunk") |
|
437 |
(magit-discard-apply section 'magit-apply-hunk)) |
|
438 |
|
|
439 |
(defun magit-discard-apply (section apply) |
|
440 |
(if (eq (magit-diff-type section) 'unstaged) |
|
441 |
(funcall apply section "--reverse") |
|
442 |
(if (magit-anything-unstaged-p |
|
443 |
nil (if (magit-file-section-p section) |
|
444 |
(oref section value) |
|
445 |
(magit-section-parent-value section))) |
|
446 |
(progn (let ((inhibit-magit-refresh t)) |
|
447 |
(funcall apply section "--reverse" "--cached") |
|
448 |
(funcall apply section "--reverse" "--reject")) |
|
449 |
(magit-refresh)) |
|
450 |
(funcall apply section "--reverse" "--index")))) |
|
451 |
|
|
452 |
(defun magit-discard-hunks (sections) |
|
453 |
(magit-confirm 'discard (format "Discard %s hunks from %s" |
|
454 |
(length sections) |
|
455 |
(magit-section-parent-value (car sections)))) |
|
456 |
(magit-discard-apply-n sections 'magit-apply-hunks)) |
|
457 |
|
|
458 |
(defun magit-discard-apply-n (sections apply) |
|
459 |
(let ((section (car sections))) |
|
460 |
(if (eq (magit-diff-type section) 'unstaged) |
|
461 |
(funcall apply sections "--reverse") |
|
462 |
(if (magit-anything-unstaged-p |
|
463 |
nil (if (magit-file-section-p section) |
|
464 |
(oref section value) |
|
465 |
(magit-section-parent-value section))) |
|
466 |
(progn (let ((inhibit-magit-refresh t)) |
|
467 |
(funcall apply sections "--reverse" "--cached") |
|
468 |
(funcall apply sections "--reverse" "--reject")) |
|
469 |
(magit-refresh)) |
|
470 |
(funcall apply sections "--reverse" "--index"))))) |
|
471 |
|
|
472 |
(defun magit-discard-file (section) |
|
473 |
(magit-discard-files (list section))) |
|
474 |
|
|
475 |
(defun magit-discard-files (sections) |
|
476 |
(let ((auto-revert-verbose nil) |
|
477 |
(type (magit-diff-type (car sections))) |
|
478 |
(status (magit-file-status)) |
|
479 |
files delete resurrect rename discard discard-new resolve) |
|
480 |
(dolist (section sections) |
|
481 |
(let ((file (oref section value))) |
|
482 |
(push file files) |
|
483 |
(pcase (cons (pcase type |
|
484 |
(`staged ?X) |
|
485 |
(`unstaged ?Y) |
|
486 |
(`untracked ?Z)) |
|
487 |
(cddr (assoc file status))) |
|
488 |
(`(?Z) (dolist (f (magit-untracked-files nil file)) |
|
489 |
(push f delete))) |
|
490 |
((or `(?Z ?? ??) `(?Z ?! ?!)) (push file delete)) |
|
491 |
((or `(?Z ?D ? ) `(,_ ?D ?D)) (push file delete)) |
|
492 |
((or `(,_ ?U ,_) `(,_ ,_ ?U)) (push file resolve)) |
|
493 |
(`(,_ ?A ?A) (push file resolve)) |
|
494 |
(`(?X ?M ,(or ? ?M ?D)) (push section discard)) |
|
495 |
(`(?Y ,_ ?M ) (push section discard)) |
|
496 |
(`(?X ?A ?M ) (push file discard-new)) |
|
497 |
(`(?X ?C ?M ) (push file discard-new)) |
|
498 |
(`(?X ?A ,(or ? ?D)) (push file delete)) |
|
499 |
(`(?X ?C ,(or ? ?D)) (push file delete)) |
|
500 |
(`(?X ?D ,(or ? ?M )) (push file resurrect)) |
|
501 |
(`(?Y ,_ ?D ) (push file resurrect)) |
|
502 |
(`(?X ?R ,(or ? ?M ?D)) (push file rename))))) |
|
503 |
(unwind-protect |
|
504 |
(let ((inhibit-magit-refresh t)) |
|
505 |
(magit-wip-commit-before-change files " before discard") |
|
506 |
(when resolve |
|
507 |
(magit-discard-files--resolve (nreverse resolve))) |
|
508 |
(when resurrect |
|
509 |
(magit-discard-files--resurrect (nreverse resurrect))) |
|
510 |
(when delete |
|
511 |
(magit-discard-files--delete (nreverse delete) status)) |
|
512 |
(when rename |
|
513 |
(magit-discard-files--rename (nreverse rename) status)) |
|
514 |
(when (or discard discard-new) |
|
515 |
(magit-discard-files--discard (nreverse discard) |
|
516 |
(nreverse discard-new))) |
|
517 |
(magit-wip-commit-after-apply files " after discard")) |
|
518 |
(magit-refresh)))) |
|
519 |
|
|
520 |
(defun magit-discard-files--resolve (files) |
|
521 |
(if-let ((arg (and (cdr files) |
|
522 |
(magit-read-char-case |
|
523 |
(format "For these %i files\n%s\ncheckout:\n" |
|
524 |
(length files) |
|
525 |
(mapconcat (lambda (file) |
|
526 |
(concat " " file)) |
|
527 |
files "\n")) |
|
528 |
t |
|
529 |
(?o "[o]ur stage" "--ours") |
|
530 |
(?t "[t]heir stage" "--theirs") |
|
531 |
(?c "[c]onflict" "--merge") |
|
532 |
(?i "decide [i]ndividually" nil))))) |
|
533 |
(dolist (file files) |
|
534 |
(magit-checkout-stage file arg)) |
|
535 |
(dolist (file files) |
|
536 |
(magit-checkout-stage file (magit-checkout-read-stage file))))) |
|
537 |
|
|
538 |
(defun magit-discard-files--resurrect (files) |
|
539 |
(magit-confirm-files 'resurrect files) |
|
540 |
(if (eq (magit-diff-type) 'staged) |
|
541 |
(magit-call-git "reset" "--" files) |
|
542 |
(magit-call-git "checkout" "--" files))) |
|
543 |
|
|
544 |
(defun magit-discard-files--delete (files status) |
|
545 |
(magit-confirm-files (if magit-delete-by-moving-to-trash 'trash 'delete) |
|
546 |
files) |
|
547 |
(let ((delete-by-moving-to-trash magit-delete-by-moving-to-trash)) |
|
548 |
(dolist (file files) |
|
549 |
(if (memq (magit-diff-type) '(unstaged untracked)) |
|
550 |
(progn (dired-delete-file file dired-recursive-deletes |
|
551 |
magit-delete-by-moving-to-trash) |
|
552 |
(dired-clean-up-after-deletion file)) |
|
553 |
(pcase (nth 3 (assoc file status)) |
|
554 |
(? (delete-file file t) |
|
555 |
(magit-call-git "rm" "--cached" "--" file)) |
|
556 |
(?M (let ((temp (magit-git-string "checkout-index" "--temp" file))) |
|
557 |
(string-match |
|
558 |
(format "\\(.+?\\)\t%s" (regexp-quote file)) temp) |
|
559 |
(rename-file (match-string 1 temp) |
|
560 |
(setq temp (concat file ".~{index}~"))) |
|
561 |
(delete-file temp t)) |
|
562 |
(magit-call-git "rm" "--cached" "--force" "--" file)) |
|
563 |
(?D (magit-call-git "checkout" "--" file) |
|
564 |
(delete-file file t) |
|
565 |
(magit-call-git "rm" "--cached" "--force" "--" file))))))) |
|
566 |
|
|
567 |
(defun magit-discard-files--rename (files status) |
|
568 |
(magit-confirm 'rename "Undo rename %s" "Undo %i renames" nil |
|
569 |
(mapcar (lambda (file) |
|
570 |
(setq file (assoc file status)) |
|
571 |
(format "%s -> %s" (cadr file) (car file))) |
|
572 |
files)) |
|
573 |
(dolist (file files) |
|
574 |
(let ((orig (cadr (assoc file status)))) |
|
575 |
(if (file-exists-p file) |
|
576 |
(progn |
|
577 |
(--when-let (file-name-directory orig) |
|
578 |
(make-directory it t)) |
|
579 |
(magit-call-git "mv" file orig)) |
|
580 |
(magit-call-git "rm" "--cached" "--" file) |
|
581 |
(magit-call-git "reset" "--" orig))))) |
|
582 |
|
|
583 |
(defun magit-discard-files--discard (sections new-files) |
|
584 |
(let ((files (--map (oref it value) sections))) |
|
585 |
(magit-confirm-files 'discard (append files new-files) |
|
586 |
(format "Discard %s changes in" (magit-diff-type))) |
|
587 |
(if (eq (magit-diff-type (car sections)) 'unstaged) |
|
588 |
(magit-call-git "checkout" "--" files) |
|
589 |
(when new-files |
|
590 |
(magit-call-git "add" "--" new-files) |
|
591 |
(magit-call-git "reset" "--" new-files)) |
|
592 |
(let ((binaries (magit-binary-files "--cached"))) |
|
593 |
(when binaries |
|
594 |
(setq sections |
|
595 |
(--remove (member (oref it value) binaries) |
|
596 |
sections))) |
|
597 |
(cond ((= (length sections) 1) |
|
598 |
(magit-discard-apply (car sections) 'magit-apply-diff)) |
|
599 |
(sections |
|
600 |
(magit-discard-apply-n sections 'magit-apply-diffs))) |
|
601 |
(when binaries |
|
602 |
(let ((modified (magit-unstaged-files t))) |
|
603 |
(setq binaries (--separate (member it modified) binaries))) |
|
604 |
(when (cadr binaries) |
|
605 |
(magit-call-git "reset" "--" (cadr binaries))) |
|
606 |
(when (car binaries) |
|
607 |
(user-error |
|
608 |
(concat |
|
609 |
"Cannot discard staged changes to binary files, " |
|
610 |
"which also have unstaged changes. Unstage instead.")))))))) |
|
611 |
|
|
612 |
;;;; Reverse |
|
613 |
|
|
614 |
(defun magit-reverse (&rest args) |
|
615 |
"Reverse the change at point in the working tree. |
|
616 |
With a prefix argument fallback to a 3-way merge. Doing |
|
617 |
so causes the change to be applied to the index as well." |
|
618 |
(interactive (and current-prefix-arg (list "--3way"))) |
|
619 |
(--when-let (magit-apply--get-selection) |
|
620 |
(pcase (list (magit-diff-type) (magit-diff-scope)) |
|
621 |
(`(untracked ,_) (user-error "Cannot reverse untracked changes")) |
|
622 |
(`(unstaged ,_) (user-error "Cannot reverse unstaged changes")) |
|
623 |
(`(,_ region) (magit-reverse-region it args)) |
|
624 |
(`(,_ hunk) (magit-reverse-hunk it args)) |
|
625 |
(`(,_ hunks) (magit-reverse-hunks it args)) |
|
626 |
(`(,_ file) (magit-reverse-file it args)) |
|
627 |
(`(,_ files) (magit-reverse-files it args)) |
|
628 |
(`(,_ list) (magit-reverse-files it args))))) |
|
629 |
|
|
630 |
(defun magit-reverse-region (section args) |
|
631 |
(magit-confirm 'reverse "Reverse region") |
|
632 |
(magit-reverse-apply section 'magit-apply-region args)) |
|
633 |
|
|
634 |
(defun magit-reverse-hunk (section args) |
|
635 |
(magit-confirm 'reverse "Reverse hunk") |
|
636 |
(magit-reverse-apply section 'magit-apply-hunk args)) |
|
637 |
|
|
638 |
(defun magit-reverse-hunks (sections args) |
|
639 |
(magit-confirm 'reverse |
|
640 |
(format "Reverse %s hunks from %s" |
|
641 |
(length sections) |
|
642 |
(magit-section-parent-value (car sections)))) |
|
643 |
(magit-reverse-apply sections 'magit-apply-hunks args)) |
|
644 |
|
|
645 |
(defun magit-reverse-file (section args) |
|
646 |
(magit-reverse-files (list section) args)) |
|
647 |
|
|
648 |
(defun magit-reverse-files (sections args) |
|
649 |
(pcase-let ((`(,binaries ,sections) |
|
650 |
(let ((bs (magit-binary-files |
|
651 |
(cond ((derived-mode-p 'magit-revision-mode) |
|
652 |
(let ((rev (car magit-refresh-args))) |
|
653 |
(format "%s^..%s" rev rev))) |
|
654 |
((derived-mode-p 'magit-diff-mode) |
|
655 |
(car magit-refresh-args)) |
|
656 |
(t |
|
657 |
"--cached"))))) |
|
658 |
(--separate (member (oref it value) bs) |
|
659 |
sections)))) |
|
660 |
(magit-confirm-files 'reverse (--map (oref it value) sections)) |
|
661 |
(cond ((= (length sections) 1) |
|
662 |
(magit-reverse-apply (car sections) 'magit-apply-diff args)) |
|
663 |
(sections |
|
664 |
(magit-reverse-apply sections 'magit-apply-diffs args))) |
|
665 |
(when binaries |
|
666 |
(user-error "Cannot reverse binary files")))) |
|
667 |
|
|
668 |
(defun magit-reverse-apply (section:s apply args) |
|
669 |
(funcall apply section:s "--reverse" args |
|
670 |
(and (not magit-reverse-atomically) |
|
671 |
(not (member "--3way" args)) |
|
672 |
"--reject"))) |
|
673 |
|
|
674 |
(defun magit-reverse-in-index (&rest args) |
|
675 |
"Reverse the change at point in the index but not the working tree. |
|
676 |
|
|
677 |
Use this command to extract a change from `HEAD', while leaving |
|
678 |
it in the working tree, so that it can later be committed using |
|
679 |
a separate commit. A typical workflow would be: |
|
680 |
|
|
681 |
0. Optionally make sure that there are no uncommitted changes. |
|
682 |
1. Visit the `HEAD' commit and navigate to the change that should |
|
683 |
not have been included in that commit. |
|
684 |
2. Type \"u\" (`magit-unstage') to reverse it in the index. |
|
685 |
This assumes that `magit-unstage-committed-changes' is non-nil. |
|
686 |
3. Type \"c e\" to extend `HEAD' with the staged changes, |
|
687 |
including those that were already staged before. |
|
688 |
4. Optionally stage the remaining changes using \"s\" or \"S\" |
|
689 |
and then type \"c c\" to create a new commit." |
|
690 |
(interactive) |
|
691 |
(magit-reverse (cons "--cached" args))) |
|
692 |
|
|
693 |
;;; _ |
|
694 |
(provide 'magit-apply) |
|
695 |
;;; magit-apply.el ends here |