| 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 |