| 1 | ;;; magit-sequence.el --- History manipulation in Magit -*- 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 | ;; Support for Git commands that replay commits and help the user make |
| 26 | ;; changes along the way. Supports `cherry-pick', `revert', `rebase', |
| 27 | ;; `rebase--interactive' and `am'. |
| 28 | |
| 29 | ;;; Code: |
| 30 | |
| 31 | (eval-when-compile (require 'subr-x)) ; For macros thread-{first,last}. |
| 32 | |
| 33 | (require 'magit) |
| 34 | |
| 35 | ;; For `magit-rebase--todo'. |
| 36 | (declare-function git-rebase-current-line "git-rebase" (&optional batch)) |
| 37 | |
| 38 | (eval-and-compile ;declare slot names |
| 39 | (cl-pushnew 'abbrev eieio--known-slot-names) |
| 40 | (cl-pushnew 'action eieio--known-slot-names) |
| 41 | (cl-pushnew 'action-options eieio--known-slot-names) |
| 42 | (cl-pushnew 'action-type eieio--known-slot-names) |
| 43 | (cl-pushnew 'target eieio--known-slot-names) |
| 44 | (cl-pushnew 'trailer eieio--known-slot-names)) |
| 45 | |
| 46 | ;;; Options |
| 47 | ;;;; Faces |
| 48 | |
| 49 | (defface magit-sequence-pick |
| 50 | '((t :inherit default)) |
| 51 | "Face used in sequence sections." |
| 52 | :group 'magit-faces) |
| 53 | |
| 54 | (defface magit-sequence-stop |
| 55 | '((((class color) (background light)) :foreground "DarkOliveGreen4") |
| 56 | (((class color) (background dark)) :foreground "DarkSeaGreen2")) |
| 57 | "Face used in sequence sections." |
| 58 | :group 'magit-faces) |
| 59 | |
| 60 | (defface magit-sequence-part |
| 61 | '((((class color) (background light)) :foreground "Goldenrod4") |
| 62 | (((class color) (background dark)) :foreground "LightGoldenrod2")) |
| 63 | "Face used in sequence sections." |
| 64 | :group 'magit-faces) |
| 65 | |
| 66 | (defface magit-sequence-head |
| 67 | '((((class color) (background light)) :foreground "SkyBlue4") |
| 68 | (((class color) (background dark)) :foreground "LightSkyBlue1")) |
| 69 | "Face used in sequence sections." |
| 70 | :group 'magit-faces) |
| 71 | |
| 72 | (defface magit-sequence-drop |
| 73 | '((((class color) (background light)) :foreground "IndianRed") |
| 74 | (((class color) (background dark)) :foreground "IndianRed")) |
| 75 | "Face used in sequence sections." |
| 76 | :group 'magit-faces) |
| 77 | |
| 78 | (defface magit-sequence-done |
| 79 | '((t :inherit magit-hash)) |
| 80 | "Face used in sequence sections." |
| 81 | :group 'magit-faces) |
| 82 | |
| 83 | (defface magit-sequence-onto |
| 84 | '((t :inherit magit-sequence-done)) |
| 85 | "Face used in sequence sections." |
| 86 | :group 'magit-faces) |
| 87 | |
| 88 | (defface magit-sequence-exec |
| 89 | '((t :inherit magit-hash)) |
| 90 | "Face used in sequence sections." |
| 91 | :group 'magit-faces) |
| 92 | |
| 93 | ;;; Common |
| 94 | |
| 95 | ;;;###autoload |
| 96 | (defun magit-sequencer-continue () |
| 97 | "Resume the current cherry-pick or revert sequence." |
| 98 | (interactive) |
| 99 | (cond |
| 100 | ((not (magit-sequencer-in-progress-p)) |
| 101 | (user-error "No cherry-pick or revert in progress")) |
| 102 | ((magit-anything-unmerged-p) |
| 103 | (user-error "Cannot continue due to unresolved conflicts")) |
| 104 | ((magit-run-git-sequencer |
| 105 | (if (magit-revert-in-progress-p) "revert" "cherry-pick") "--continue")))) |
| 106 | |
| 107 | ;;;###autoload |
| 108 | (defun magit-sequencer-skip () |
| 109 | "Skip the stopped at commit during a cherry-pick or revert sequence." |
| 110 | (interactive) |
| 111 | (unless (magit-sequencer-in-progress-p) |
| 112 | (user-error "No cherry-pick or revert in progress")) |
| 113 | (magit-call-git "reset" "--hard") |
| 114 | (magit-sequencer-continue)) |
| 115 | |
| 116 | ;;;###autoload |
| 117 | (defun magit-sequencer-abort () |
| 118 | "Abort the current cherry-pick or revert sequence. |
| 119 | This discards all changes made since the sequence started." |
| 120 | (interactive) |
| 121 | (cond |
| 122 | ((not (magit-sequencer-in-progress-p)) |
| 123 | (user-error "No cherry-pick or revert in progress")) |
| 124 | ((magit-revert-in-progress-p) |
| 125 | (magit-confirm 'abort-revert "Really abort revert") |
| 126 | (magit-run-git-sequencer "revert" "--abort")) |
| 127 | ((magit-confirm 'abort-cherry-pick "Really abort cherry-pick") |
| 128 | (magit-run-git-sequencer "cherry-pick" "--abort")))) |
| 129 | |
| 130 | (defun magit-sequencer-in-progress-p () |
| 131 | (or (magit-cherry-pick-in-progress-p) |
| 132 | (magit-revert-in-progress-p))) |
| 133 | |
| 134 | ;;; Cherry-Pick |
| 135 | |
| 136 | (defvar magit-perl-executable "perl" |
| 137 | "The Perl executable.") |
| 138 | |
| 139 | ;;;###autoload(autoload 'magit-cherry-pick "magit-sequence" nil t) |
| 140 | (transient-define-prefix magit-cherry-pick () |
| 141 | "Apply or transplant commits." |
| 142 | :man-page "git-cherry-pick" |
| 143 | :value '("--ff") |
| 144 | :incompatible '(("--ff" "-x")) |
| 145 | ["Arguments" |
| 146 | :if-not magit-sequencer-in-progress-p |
| 147 | (magit-cherry-pick:--mainline) |
| 148 | ("=s" magit-merge:--strategy) |
| 149 | ("-F" "Attempt fast-forward" "--ff") |
| 150 | ("-x" "Reference cherry in commit message" "-x") |
| 151 | ("-e" "Edit commit messages" ("-e" "--edit")) |
| 152 | (magit:--gpg-sign) |
| 153 | (magit:--signoff)] |
| 154 | [:if-not magit-sequencer-in-progress-p |
| 155 | ["Apply here" |
| 156 | ("A" "Pick" magit-cherry-copy) |
| 157 | ("a" "Apply" magit-cherry-apply) |
| 158 | ("h" "Harvest" magit-cherry-harvest) |
| 159 | ("m" "Squash" magit-merge-squash)] |
| 160 | ["Apply elsewhere" |
| 161 | ("d" "Donate" magit-cherry-donate) |
| 162 | ("n" "Spinout" magit-cherry-spinout) |
| 163 | ("s" "Spinoff" magit-cherry-spinoff)]] |
| 164 | ["Actions" |
| 165 | :if magit-sequencer-in-progress-p |
| 166 | ("A" "Continue" magit-sequencer-continue) |
| 167 | ("s" "Skip" magit-sequencer-skip) |
| 168 | ("a" "Abort" magit-sequencer-abort)]) |
| 169 | |
| 170 | (transient-define-argument magit-cherry-pick:--mainline () |
| 171 | :description "Replay merge relative to parent" |
| 172 | :class 'transient-option |
| 173 | :shortarg "-m" |
| 174 | :argument "--mainline=" |
| 175 | :reader #'transient-read-number-N+) |
| 176 | |
| 177 | (defun magit-cherry-pick-read-args (prompt) |
| 178 | (list (or (nreverse (magit-region-values 'commit)) |
| 179 | (magit-read-other-branch-or-commit prompt)) |
| 180 | (transient-args 'magit-cherry-pick))) |
| 181 | |
| 182 | (defun magit--cherry-move-read-args (verb away fn &optional allow-detached) |
| 183 | (declare (indent defun)) |
| 184 | (let ((commits (or (nreverse (magit-region-values 'commit)) |
| 185 | (list (funcall (if away |
| 186 | #'magit-read-branch-or-commit |
| 187 | #'magit-read-other-branch-or-commit) |
| 188 | (format "%s cherry" (capitalize verb)))))) |
| 189 | (current (or (magit-get-current-branch) |
| 190 | (and allow-detached (magit-rev-parse "HEAD"))))) |
| 191 | (unless current |
| 192 | (user-error "Cannot %s cherries while HEAD is detached" verb)) |
| 193 | (let ((reachable (magit-rev-ancestor-p (car commits) current)) |
| 194 | (msg "Cannot %s cherries that %s reachable from HEAD")) |
| 195 | (pcase (list away reachable) |
| 196 | ('(nil t) (user-error msg verb "are")) |
| 197 | ('(t nil) (user-error msg verb "are not")))) |
| 198 | `(,commits |
| 199 | ,@(funcall fn commits) |
| 200 | ,(transient-args 'magit-cherry-pick)))) |
| 201 | |
| 202 | (defun magit--cherry-spinoff-read-args (verb) |
| 203 | (magit--cherry-move-read-args verb t |
| 204 | (lambda (commits) |
| 205 | (magit-branch-read-args |
| 206 | (format "Create branch from %s cherries" (length commits)) |
| 207 | (magit-get-upstream-branch))))) |
| 208 | |
| 209 | ;;;###autoload |
| 210 | (defun magit-cherry-copy (commits &optional args) |
| 211 | "Copy COMMITS from another branch onto the current branch. |
| 212 | Prompt for a commit, defaulting to the commit at point. If |
| 213 | the region selects multiple commits, then pick all of them, |
| 214 | without prompting." |
| 215 | (interactive (magit-cherry-pick-read-args "Cherry-pick")) |
| 216 | (magit--cherry-pick commits args)) |
| 217 | |
| 218 | ;;;###autoload |
| 219 | (defun magit-cherry-apply (commits &optional args) |
| 220 | "Apply the changes in COMMITS but do not commit them. |
| 221 | Prompt for a commit, defaulting to the commit at point. If |
| 222 | the region selects multiple commits, then apply all of them, |
| 223 | without prompting." |
| 224 | (interactive (magit-cherry-pick-read-args "Apply changes from commit")) |
| 225 | (magit--cherry-pick commits (cons "--no-commit" (remove "--ff" args)))) |
| 226 | |
| 227 | ;;;###autoload |
| 228 | (defun magit-cherry-harvest (commits branch &optional args) |
| 229 | "Move COMMITS from another BRANCH onto the current branch. |
| 230 | Remove the COMMITS from BRANCH and stay on the current branch. |
| 231 | If a conflict occurs, then you have to fix that and finish the |
| 232 | process manually." |
| 233 | (interactive |
| 234 | (magit--cherry-move-read-args "harvest" nil |
| 235 | (lambda (commits) |
| 236 | (list (let ((branches (magit-list-containing-branches (car commits)))) |
| 237 | (pcase (length branches) |
| 238 | (0 nil) |
| 239 | (1 (car branches)) |
| 240 | (_ (magit-completing-read |
| 241 | (let ((len (length commits))) |
| 242 | (if (= len 1) |
| 243 | "Remove 1 cherry from branch" |
| 244 | (format "Remove %s cherries from branch" len))) |
| 245 | branches nil t)))))))) |
| 246 | (magit--cherry-move commits branch (magit-get-current-branch) args nil t)) |
| 247 | |
| 248 | ;;;###autoload |
| 249 | (defun magit-cherry-donate (commits branch &optional args) |
| 250 | "Move COMMITS from the current branch onto another existing BRANCH. |
| 251 | Remove COMMITS from the current branch and stay on that branch. |
| 252 | If a conflict occurs, then you have to fix that and finish the |
| 253 | process manually. `HEAD' is allowed to be detached initially." |
| 254 | (interactive |
| 255 | (magit--cherry-move-read-args "donate" t |
| 256 | (lambda (commits) |
| 257 | (list (magit-read-other-branch |
| 258 | (let ((len (length commits))) |
| 259 | (if (= len 1) |
| 260 | "Move 1 cherry to branch" |
| 261 | (format "Move %s cherries to branch" len)))))) |
| 262 | 'allow-detached)) |
| 263 | (magit--cherry-move commits |
| 264 | (or (magit-get-current-branch) |
| 265 | (magit-rev-parse "HEAD")) |
| 266 | branch args)) |
| 267 | |
| 268 | ;;;###autoload |
| 269 | (defun magit-cherry-spinout (commits branch start-point &optional args) |
| 270 | "Move COMMITS from the current branch onto a new BRANCH. |
| 271 | Remove COMMITS from the current branch and stay on that branch. |
| 272 | If a conflict occurs, then you have to fix that and finish the |
| 273 | process manually." |
| 274 | (interactive (magit--cherry-spinoff-read-args "spinout")) |
| 275 | (magit--cherry-move commits (magit-get-current-branch) branch args |
| 276 | start-point)) |
| 277 | |
| 278 | ;;;###autoload |
| 279 | (defun magit-cherry-spinoff (commits branch start-point &optional args) |
| 280 | "Move COMMITS from the current branch onto a new BRANCH. |
| 281 | Remove COMMITS from the current branch and checkout BRANCH. |
| 282 | If a conflict occurs, then you have to fix that and finish |
| 283 | the process manually." |
| 284 | (interactive (magit--cherry-spinoff-read-args "spinoff")) |
| 285 | (magit--cherry-move commits (magit-get-current-branch) branch args |
| 286 | start-point t)) |
| 287 | |
| 288 | (defun magit--cherry-move (commits src dst args |
| 289 | &optional start-point checkout-dst) |
| 290 | (let ((current (magit-get-current-branch))) |
| 291 | (unless (magit-branch-p dst) |
| 292 | (let ((magit-process-raise-error t)) |
| 293 | (magit-call-git "branch" dst start-point)) |
| 294 | (when$ (magit-get-indirect-upstream-branch start-point) |
| 295 | (magit-call-git "branch" "--set-upstream-to" $ dst))) |
| 296 | (unless (equal dst current) |
| 297 | (let ((magit-process-raise-error t)) |
| 298 | (magit-call-git "checkout" dst))) |
| 299 | (if (not src) ; harvest only |
| 300 | (magit--cherry-pick commits args) |
| 301 | (let ((tip (car (last commits))) |
| 302 | (keep (concat (car commits) "^"))) |
| 303 | (magit--cherry-pick commits args) |
| 304 | (set-process-sentinel |
| 305 | magit-this-process |
| 306 | (lambda (process event) |
| 307 | (when (memq (process-status process) '(exit signal)) |
| 308 | (if (> (process-exit-status process) 0) |
| 309 | (magit-process-sentinel process event) |
| 310 | (process-put process 'inhibit-refresh t) |
| 311 | (magit-process-sentinel process event) |
| 312 | (cond |
| 313 | ((magit-rev-equal tip src) |
| 314 | (magit-call-git "update-ref" |
| 315 | "-m" (format "reset: moving to %s" keep) |
| 316 | (magit-ref-fullname src) |
| 317 | keep tip) |
| 318 | (if (not checkout-dst) |
| 319 | (magit-run-git "checkout" src) |
| 320 | (magit-refresh))) |
| 321 | (t |
| 322 | (magit-git "checkout" src) |
| 323 | (with-environment-variables |
| 324 | (("GIT_SEQUENCE_EDITOR" |
| 325 | (format "%s -i -ne '/^pick (%s)/ or print'" |
| 326 | magit-perl-executable |
| 327 | (mapconcat #'magit-rev-abbrev commits "|")))) |
| 328 | (magit-run-git-sequencer "rebase" "-i" keep)) |
| 329 | (when checkout-dst |
| 330 | (set-process-sentinel |
| 331 | magit-this-process |
| 332 | (lambda (process event) |
| 333 | (when (memq (process-status process) '(exit signal)) |
| 334 | (if (> (process-exit-status process) 0) |
| 335 | (magit-process-sentinel process event) |
| 336 | (process-put process 'inhibit-refresh t) |
| 337 | (magit-process-sentinel process event) |
| 338 | (magit-run-git "checkout" dst)))))))))))))))) |
| 339 | |
| 340 | (defun magit--cherry-pick (commits args &optional revert) |
| 341 | (let ((command (if revert "revert" "cherry-pick"))) |
| 342 | (when (stringp commits) |
| 343 | (setq commits (if (string-search ".." commits) |
| 344 | (split-string commits "\\.\\.") |
| 345 | (list commits)))) |
| 346 | (magit-run-git-sequencer |
| 347 | (if revert "revert" "cherry-pick") |
| 348 | (let ((merges (seq-filter #'magit-merge-commit-p commits))) |
| 349 | (cond |
| 350 | ((not merges) |
| 351 | (seq-remove (##string-prefix-p "--mainline=" %) args)) |
| 352 | ((cl-set-difference commits merges :test #'equal) |
| 353 | (user-error "Cannot %s merge and non-merge commits at once" |
| 354 | command)) |
| 355 | ((seq-find (##string-prefix-p "--mainline=" %) args) |
| 356 | args) |
| 357 | ((cons (format "--mainline=%s" |
| 358 | (read-number "Replay merges relative to parent: ")) |
| 359 | args)))) |
| 360 | commits))) |
| 361 | |
| 362 | (defun magit-cherry-pick-in-progress-p () |
| 363 | ;; .git/sequencer/todo does not exist when there is only one commit left. |
| 364 | (let ((dir (magit-gitdir))) |
| 365 | (or (file-exists-p (expand-file-name "CHERRY_PICK_HEAD" dir)) |
| 366 | ;; And CHERRY_PICK_HEAD does not exist when a conflict happens |
| 367 | ;; while picking a series of commits with --no-commit. |
| 368 | (and$ (magit-file-line (expand-file-name "sequencer/todo" dir)) |
| 369 | (string-prefix-p "pick" $))))) |
| 370 | |
| 371 | ;;; Revert |
| 372 | |
| 373 | ;;;###autoload(autoload 'magit-revert "magit-sequence" nil t) |
| 374 | (transient-define-prefix magit-revert () |
| 375 | "Revert existing commits, with or without creating new commits." |
| 376 | :man-page "git-revert" |
| 377 | :value '("--edit") |
| 378 | ["Arguments" |
| 379 | :if-not magit-sequencer-in-progress-p |
| 380 | (magit-cherry-pick:--mainline) |
| 381 | ("-e" "Edit commit message" ("-e" "--edit")) |
| 382 | ("-E" "Don't edit commit message" "--no-edit") |
| 383 | ("=s" magit-merge:--strategy) |
| 384 | (magit:--gpg-sign) |
| 385 | (magit:--signoff)] |
| 386 | ["Actions" |
| 387 | :if-not magit-sequencer-in-progress-p |
| 388 | ("V" "Revert commit(s)" magit-revert-and-commit) |
| 389 | ("v" "Revert changes" magit-revert-no-commit)] |
| 390 | ["Actions" |
| 391 | :if magit-sequencer-in-progress-p |
| 392 | ("V" "Continue" magit-sequencer-continue) |
| 393 | ("s" "Skip" magit-sequencer-skip) |
| 394 | ("a" "Abort" magit-sequencer-abort)]) |
| 395 | |
| 396 | (defun magit-revert-read-args (prompt) |
| 397 | (list (or (magit-region-values 'commit) |
| 398 | (magit-read-branch-or-commit prompt)) |
| 399 | (transient-args 'magit-revert))) |
| 400 | |
| 401 | ;;;###autoload |
| 402 | (defun magit-revert-and-commit (commit &optional args) |
| 403 | "Revert COMMIT by creating a new commit. |
| 404 | Prompt for a commit, defaulting to the commit at point. If |
| 405 | the region selects multiple commits, then revert all of them, |
| 406 | without prompting." |
| 407 | (interactive (magit-revert-read-args "Revert commit")) |
| 408 | (magit--cherry-pick commit args t)) |
| 409 | |
| 410 | ;;;###autoload |
| 411 | (defun magit-revert-no-commit (commit &optional args) |
| 412 | "Revert COMMIT by applying it in reverse to the worktree. |
| 413 | Prompt for a commit, defaulting to the commit at point. If |
| 414 | the region selects multiple commits, then revert all of them, |
| 415 | without prompting." |
| 416 | (interactive (magit-revert-read-args "Revert changes")) |
| 417 | (magit--cherry-pick commit (cons "--no-commit" args) t)) |
| 418 | |
| 419 | (defun magit-revert-in-progress-p () |
| 420 | ;; .git/sequencer/todo does not exist when there is only one commit left. |
| 421 | (let ((dir (magit-gitdir))) |
| 422 | (or (file-exists-p (expand-file-name "REVERT_HEAD" dir)) |
| 423 | ;; And REVERT_HEAD does not exist when a conflict happens |
| 424 | ;; while reverting a series of commits with --no-commit. |
| 425 | (and$ (magit-file-line (expand-file-name "sequencer/todo" dir)) |
| 426 | (string-prefix-p "revert" $))))) |
| 427 | |
| 428 | ;;; Patch |
| 429 | |
| 430 | ;;;###autoload(autoload 'magit-am "magit-sequence" nil t) |
| 431 | (transient-define-prefix magit-am () |
| 432 | "Apply patches received by email." |
| 433 | :man-page "git-am" |
| 434 | :value '("--3way") |
| 435 | ["Arguments" |
| 436 | :if-not magit-am-in-progress-p |
| 437 | ("-3" "Fall back on 3way merge" ("-3" "--3way")) |
| 438 | ("-R" "Reject only failed hunks" "--reject") |
| 439 | (magit-apply:-p) |
| 440 | ("-c" "Remove text before scissors line" ("-c" "--scissors")) |
| 441 | ("-k" "Inhibit removal of email cruft" ("-k" "--keep")) |
| 442 | ("-b" "Limit removal of email cruft" "--keep-non-patch") |
| 443 | ("-d" "Use author date as committer date" "--committer-date-is-author-date") |
| 444 | ("-t" "Use current time as author date" "--ignore-date") |
| 445 | (magit:--gpg-sign) |
| 446 | (magit:--signoff)] |
| 447 | ["Apply" |
| 448 | :if-not magit-am-in-progress-p |
| 449 | ("m" "maildir" magit-am-apply-maildir) |
| 450 | ("w" "patches" magit-am-apply-patches) |
| 451 | ("a" "plain patch" magit-patch-apply)] |
| 452 | ["Actions" |
| 453 | :if magit-am-in-progress-p |
| 454 | ("w" "Continue" magit-am-continue) |
| 455 | ("s" "Skip" magit-am-skip) |
| 456 | ("a" "Abort" magit-am-abort)]) |
| 457 | |
| 458 | (defun magit-am-arguments () |
| 459 | (transient-args 'magit-am)) |
| 460 | |
| 461 | (transient-define-argument magit-apply:-p () |
| 462 | :description "Remove leading slashes from paths" |
| 463 | :class 'transient-option |
| 464 | :argument "-p" |
| 465 | :allow-empty t |
| 466 | :reader #'transient-read-number-N+) |
| 467 | |
| 468 | ;;;###autoload |
| 469 | (defun magit-am-apply-patches (&optional files args) |
| 470 | "Apply the patches FILES." |
| 471 | (interactive (list (or (magit-region-values 'file) |
| 472 | (list (let ((default (magit-file-at-point))) |
| 473 | (read-file-name |
| 474 | (if default |
| 475 | (format "Apply patch (%s): " default) |
| 476 | "Apply patch: ") |
| 477 | nil default)))) |
| 478 | (magit-am-arguments))) |
| 479 | (magit-run-git-sequencer "am" args "--" |
| 480 | (mapcar (##magit-convert-filename-for-git |
| 481 | (expand-file-name %)) |
| 482 | files))) |
| 483 | |
| 484 | ;;;###autoload |
| 485 | (defun magit-am-apply-maildir (&optional maildir args) |
| 486 | "Apply the patches from MAILDIR." |
| 487 | (interactive (list (read-file-name "Apply mbox or Maildir: ") |
| 488 | (magit-am-arguments))) |
| 489 | (magit-run-git-sequencer "am" args (magit-convert-filename-for-git |
| 490 | (expand-file-name maildir)))) |
| 491 | |
| 492 | ;;;###autoload |
| 493 | (defun magit-am-continue () |
| 494 | "Resume the current patch applying sequence." |
| 495 | (interactive) |
| 496 | (cond |
| 497 | ((not (magit-am-in-progress-p)) |
| 498 | (user-error "Not applying any patches")) |
| 499 | ((magit-anything-unstaged-p t) |
| 500 | (user-error "Cannot continue due to unstaged changes")) |
| 501 | ((magit-run-git-sequencer "am" "--continue")))) |
| 502 | |
| 503 | ;;;###autoload |
| 504 | (defun magit-am-skip () |
| 505 | "Skip the stopped at patch during a patch applying sequence." |
| 506 | (interactive) |
| 507 | (unless (magit-am-in-progress-p) |
| 508 | (user-error "Not applying any patches")) |
| 509 | (magit-run-git-sequencer "am" "--skip")) |
| 510 | |
| 511 | ;;;###autoload |
| 512 | (defun magit-am-abort () |
| 513 | "Abort the current patch applying sequence. |
| 514 | This discards all changes made since the sequence started." |
| 515 | (interactive) |
| 516 | (unless (magit-am-in-progress-p) |
| 517 | (user-error "Not applying any patches")) |
| 518 | (magit-run-git "am" "--abort")) |
| 519 | |
| 520 | (defun magit-am-in-progress-p () |
| 521 | (file-exists-p (expand-file-name "rebase-apply/applying" (magit-gitdir)))) |
| 522 | |
| 523 | ;;; Rebase |
| 524 | |
| 525 | ;;;###autoload(autoload 'magit-rebase "magit-sequence" nil t) |
| 526 | (transient-define-prefix magit-rebase () |
| 527 | "Transplant commits and/or modify existing commits." |
| 528 | :man-page "git-rebase" |
| 529 | :value '("--autostash") |
| 530 | ["Arguments" |
| 531 | :if-not magit-rebase-in-progress-p |
| 532 | ("-k" "Keep empty commits" "--keep-empty") |
| 533 | ("-p" "Preserve merges" ("-p" "--preserve-merges") |
| 534 | :if (##magit-git-version< "2.33.0")) |
| 535 | ("-r" "Rebase merges" ("-r" "--rebase-merges=") |
| 536 | magit-rebase-merges-select-mode) |
| 537 | ("-u" "Update branches" "--update-refs" |
| 538 | :if (##magit-git-version>= "2.38.0")) |
| 539 | (7 magit-merge:--strategy) |
| 540 | (7 magit-merge:--strategy-option) |
| 541 | (7 "=X" magit-diff:--diff-algorithm :argument "-Xdiff-algorithm=") |
| 542 | (7 "-f" "Force rebase" ("-f" "--force-rebase")) |
| 543 | ("-d" "Use author date as committer date" "--committer-date-is-author-date") |
| 544 | ("-t" "Use current time as author date" "--ignore-date") |
| 545 | ("-a" "Autosquash" "--autosquash") |
| 546 | ("-A" "Autostash" "--autostash") |
| 547 | ("-i" "Interactive" ("-i" "--interactive")) |
| 548 | ("-h" "Disable hooks" "--no-verify") |
| 549 | (7 magit-rebase:--exec) |
| 550 | (magit:--gpg-sign) |
| 551 | (magit:--signoff)] |
| 552 | [:if-not magit-rebase-in-progress-p |
| 553 | :description (##format (propertize "Rebase %s onto" 'face 'transient-heading) |
| 554 | (propertize (or (magit-get-current-branch) "HEAD") |
| 555 | 'face 'magit-branch-local)) |
| 556 | ("p" magit-rebase-onto-pushremote) |
| 557 | ("u" magit-rebase-onto-upstream) |
| 558 | ("e" "elsewhere" magit-rebase-branch)] |
| 559 | ["Rebase" |
| 560 | :if-not magit-rebase-in-progress-p |
| 561 | [("i" "interactively" magit-rebase-interactive) |
| 562 | ("s" "a subset" magit-rebase-subset)] |
| 563 | [("m" "to modify a commit" magit-rebase-edit-commit) |
| 564 | ("w" "to reword a commit" magit-rebase-reword-commit) |
| 565 | ("k" "to remove a commit" magit-rebase-remove-commit)] |
| 566 | [("f" "to autosquash" magit-rebase-autosquash) |
| 567 | (6 "t" "to change dates" magit-reshelve-since)]] |
| 568 | ["Actions" |
| 569 | :if magit-rebase-in-progress-p |
| 570 | ("r" "Continue" magit-rebase-continue) |
| 571 | ("s" "Skip" magit-rebase-skip) |
| 572 | ("e" "Edit" magit-rebase-edit) |
| 573 | ("a" "Abort" magit-rebase-abort)]) |
| 574 | |
| 575 | (transient-define-argument magit-rebase:--exec () |
| 576 | :description "Run command after commits" |
| 577 | :class 'transient-option |
| 578 | :shortarg "-x" |
| 579 | :argument "--exec=" |
| 580 | :reader #'read-shell-command) |
| 581 | |
| 582 | (defun magit-rebase-merges-select-mode (&rest _ignore) |
| 583 | (magit-read-char-case nil t |
| 584 | (?n "[n]o-rebase-cousins" "no-rebase-cousins") |
| 585 | (?r "[r]ebase-cousins" "rebase-cousins"))) |
| 586 | |
| 587 | (defun magit-rebase-arguments () |
| 588 | (transient-args 'magit-rebase)) |
| 589 | |
| 590 | (defun magit-git-rebase (target args) |
| 591 | (magit-run-git-sequencer "rebase" args target)) |
| 592 | |
| 593 | ;;;###autoload(autoload 'magit-rebase-onto-pushremote "magit-sequence" nil t) |
| 594 | (transient-define-suffix magit-rebase-onto-pushremote (args) |
| 595 | "Rebase the current branch onto its push-remote branch. |
| 596 | |
| 597 | With a prefix argument or when the push-remote is either not |
| 598 | configured or unusable, then let the user first configure the |
| 599 | push-remote." |
| 600 | :if #'magit-get-current-branch |
| 601 | :description #'magit-pull--pushbranch-description |
| 602 | (interactive (list (magit-rebase-arguments))) |
| 603 | (pcase-let ((`(,branch ,remote) |
| 604 | (magit--select-push-remote "rebase onto that"))) |
| 605 | (magit-git-rebase (concat remote "/" branch) args))) |
| 606 | |
| 607 | ;;;###autoload(autoload 'magit-rebase-onto-upstream "magit-sequence" nil t) |
| 608 | (transient-define-suffix magit-rebase-onto-upstream (args) |
| 609 | "Rebase the current branch onto its upstream branch. |
| 610 | |
| 611 | With a prefix argument or when the upstream is either not |
| 612 | configured or unusable, then let the user first configure |
| 613 | the upstream." |
| 614 | :if #'magit-get-current-branch |
| 615 | :description #'magit-rebase--upstream-description |
| 616 | (interactive (list (magit-rebase-arguments))) |
| 617 | (let* ((branch (or (magit-get-current-branch) |
| 618 | (user-error "No branch is checked out"))) |
| 619 | (upstream (magit-get-upstream-branch branch))) |
| 620 | (when (or current-prefix-arg (not upstream)) |
| 621 | (setq upstream |
| 622 | (magit-read-upstream-branch |
| 623 | branch (format "Set upstream of %s and rebase onto that" branch))) |
| 624 | (magit-set-upstream-branch branch upstream)) |
| 625 | (magit-git-rebase upstream args))) |
| 626 | |
| 627 | (defun magit-rebase--upstream-description () |
| 628 | (and-let ((branch (magit-get-current-branch))) |
| 629 | (or (magit-get-upstream-branch branch) |
| 630 | (let ((remote (magit-get "branch" branch "remote")) |
| 631 | (merge (magit-get "branch" branch "merge")) |
| 632 | (u (magit--propertize-face "@{upstream}" 'bold))) |
| 633 | (cond |
| 634 | ((magit--unnamed-upstream-p remote merge) |
| 635 | (concat u ", replacing unnamed")) |
| 636 | ((magit--valid-upstream-p remote merge) |
| 637 | (concat u ", replacing non-existent")) |
| 638 | ((or remote merge) |
| 639 | (concat u ", replacing invalid")) |
| 640 | ((concat u ", setting that"))))))) |
| 641 | |
| 642 | ;;;###autoload |
| 643 | (defun magit-rebase-branch (target args) |
| 644 | "Rebase the current branch onto a branch read in the minibuffer. |
| 645 | All commits that are reachable from `HEAD' but not from the |
| 646 | selected branch TARGET are being rebased." |
| 647 | (interactive (list (magit-read-other-branch-or-commit "Rebase onto") |
| 648 | (magit-rebase-arguments))) |
| 649 | (message "Rebasing...") |
| 650 | (magit-git-rebase target args) |
| 651 | (message "Rebasing...done")) |
| 652 | |
| 653 | ;;;###autoload |
| 654 | (defun magit-rebase-subset (newbase start args) |
| 655 | "Rebase a subset of the current branch's history onto a new base. |
| 656 | Rebase commits from START to `HEAD' onto NEWBASE. |
| 657 | START has to be selected from a list of recent commits." |
| 658 | (interactive (list (magit-read-other-branch-or-commit |
| 659 | "Rebase subset onto" nil |
| 660 | (magit-get-upstream-branch)) |
| 661 | nil |
| 662 | (magit-rebase-arguments))) |
| 663 | (cond (start |
| 664 | (message "Rebasing...") |
| 665 | (magit-run-git-sequencer "rebase" "--onto" newbase start args) |
| 666 | (message "Rebasing...done")) |
| 667 | ((magit-log-select |
| 668 | `(lambda (commit) |
| 669 | (magit-rebase-subset ,newbase (concat commit "^") (list ,@args))) |
| 670 | (concat "Type %p on a commit to rebase it " |
| 671 | "and commits above it onto " newbase ","))))) |
| 672 | |
| 673 | (defun magit-rebase-interactive-1 |
| 674 | ( commit args message |
| 675 | &optional editor delay-edit-confirm noassert confirm exact) |
| 676 | (declare (indent 2)) |
| 677 | (cond ((not commit)) |
| 678 | ((not (magit-rev-ancestor-p commit "HEAD")) |
| 679 | (user-error "%s isn't an ancestor of HEAD" commit)) |
| 680 | ((not (magit-commit-parents commit)) |
| 681 | (setq args (cons "--root" args))) |
| 682 | ((not exact) |
| 683 | (setq commit (concat commit "^")))) |
| 684 | (when (and commit (not noassert)) |
| 685 | (setq commit (magit-rebase-interactive-assert |
| 686 | commit delay-edit-confirm |
| 687 | (seq-some (##string-prefix-p "--rebase-merges" %) args)))) |
| 688 | (if (and commit (not confirm)) |
| 689 | ;; Use a copy because the old value may contain GIT_SEQUENCE_EDITOR. |
| 690 | (let ((process-environment (copy-sequence process-environment))) |
| 691 | (when editor |
| 692 | (setenv "GIT_SEQUENCE_EDITOR" |
| 693 | (if (functionp editor) |
| 694 | (funcall editor commit) |
| 695 | editor))) |
| 696 | (magit-run-git-sequencer "rebase" "-i" args |
| 697 | (and (not (member "--root" args)) commit))) |
| 698 | (magit-log-select |
| 699 | `(lambda (commit) |
| 700 | ;; In some cases (currently just magit-rebase-remove-commit), "-c |
| 701 | ;; commentChar=#" is added to the global arguments for git. Ensure |
| 702 | ;; that the same happens when we chose the commit via |
| 703 | ;; magit-log-select, below. |
| 704 | (let ((magit-git-global-arguments (list ,@magit-git-global-arguments))) |
| 705 | (magit-rebase-interactive-1 commit (list ,@args) |
| 706 | ,message ,editor ,delay-edit-confirm ,noassert))) |
| 707 | message))) |
| 708 | |
| 709 | (defvar magit--rebase-published-symbol nil) |
| 710 | (defvar magit--rebase-public-edit-confirmed nil) |
| 711 | |
| 712 | (defun magit-rebase-interactive-assert |
| 713 | (since &optional delay-edit-confirm rebase-merges) |
| 714 | (let* ((commit (magit-rebase--target-commit since)) |
| 715 | (branches (magit-list-publishing-branches commit))) |
| 716 | (setq magit--rebase-public-edit-confirmed |
| 717 | (delete (magit-toplevel) magit--rebase-public-edit-confirmed)) |
| 718 | (when (and branches |
| 719 | (or (not delay-edit-confirm) |
| 720 | ;; The user might have stopped at a published commit |
| 721 | ;; merely to add new commits *after* it. Try not to |
| 722 | ;; ask users whether they really want to edit public |
| 723 | ;; commits, when they don't actually intend to do so. |
| 724 | (not (all (##magit-rev-equal % commit) branches)))) |
| 725 | (let ((m1 "Some of these commits have already been published to ") |
| 726 | (m2 ".\nDo you really want to modify them")) |
| 727 | (magit-confirm (or magit--rebase-published-symbol 'rebase-published) |
| 728 | (concat m1 "%s" m2) |
| 729 | (concat m1 "%d public branches" m2) |
| 730 | nil branches)) |
| 731 | (push (magit-toplevel) magit--rebase-public-edit-confirmed))) |
| 732 | (if (and (magit-git-lines "rev-list" "--merges" (concat since "..HEAD")) |
| 733 | (not rebase-merges)) |
| 734 | (magit-read-char-case "Proceed despite merge in rebase range? " nil |
| 735 | (?c "[c]ontinue" since) |
| 736 | (?s "[s]elect other" nil) |
| 737 | (?a "[a]bort" (user-error "Quit"))) |
| 738 | since)) |
| 739 | |
| 740 | (defun magit-rebase--target-commit (since) |
| 741 | (if (string-suffix-p "^" since) |
| 742 | ;; If SINCE is "REV^", then the user selected |
| 743 | ;; "REV", which is the first commit that will |
| 744 | ;; be replaced. (from^..to] <=> [from..to] |
| 745 | (substring since 0 -1) |
| 746 | ;; The "--root" argument is being used. |
| 747 | since)) |
| 748 | |
| 749 | (defvar magit-rebase-interactive-include-selected t) |
| 750 | |
| 751 | ;;;###autoload |
| 752 | (defun magit-rebase-interactive (commit args) |
| 753 | "Start an interactive rebase sequence." |
| 754 | (interactive (list (magit-commit-at-point) |
| 755 | (magit-rebase-arguments))) |
| 756 | (magit-rebase-interactive-1 commit args |
| 757 | "Type %p on a commit to rebase it and all commits above it," |
| 758 | nil t nil nil |
| 759 | (not magit-rebase-interactive-include-selected))) |
| 760 | |
| 761 | ;;;###autoload |
| 762 | (defun magit-rebase-autosquash (select args) |
| 763 | "Combine squash and fixup commits with their intended targets. |
| 764 | By default only squash into commits that are not reachable from |
| 765 | the upstream branch. If no upstream is configured or with a prefix |
| 766 | argument, prompt for the first commit to potentially squash into." |
| 767 | (interactive (list current-prefix-arg |
| 768 | (magit-rebase-arguments))) |
| 769 | (magit-rebase-interactive-1 |
| 770 | (and-let ((_(not select)) |
| 771 | (upstream (magit-get-upstream-branch))) |
| 772 | (magit-merge-base upstream "HEAD")) |
| 773 | (nconc (list "--autosquash" "--keep-empty") args) |
| 774 | "Type %p on a commit to squash into it and then rebase as necessary," |
| 775 | "true" nil t nil t)) |
| 776 | |
| 777 | ;;;###autoload |
| 778 | (defun magit-rebase-edit-commit (commit args) |
| 779 | "Edit a single older commit using rebase." |
| 780 | (interactive (list (magit-commit-at-point) |
| 781 | (magit-rebase-arguments))) |
| 782 | (magit-rebase-interactive-1 commit args |
| 783 | "Type %p on a commit to edit it," |
| 784 | (apply-partially #'magit-rebase--perl-editor 'edit) |
| 785 | t)) |
| 786 | |
| 787 | ;;;###autoload |
| 788 | (defun magit-rebase-reword-commit (commit args) |
| 789 | "Reword a single older commit using rebase." |
| 790 | (interactive (list (magit-commit-at-point) |
| 791 | (magit-rebase-arguments))) |
| 792 | (magit-rebase-interactive-1 commit args |
| 793 | "Type %p on a commit to reword its message," |
| 794 | (apply-partially #'magit-rebase--perl-editor 'reword))) |
| 795 | |
| 796 | ;;;###autoload |
| 797 | (defun magit-rebase-remove-commit (commit args) |
| 798 | "Remove a single older commit using rebase." |
| 799 | (interactive (list (magit-commit-at-point) |
| 800 | (magit-rebase-arguments))) |
| 801 | ;; magit-rebase--perl-editor assumes that the comment character is "#". |
| 802 | (let ((magit-git-global-arguments |
| 803 | (nconc (list "-c" "core.commentChar=#") |
| 804 | magit-git-global-arguments))) |
| 805 | (magit-rebase-interactive-1 commit args |
| 806 | "Type %p on a commit to remove it," |
| 807 | (apply-partially #'magit-rebase--perl-editor 'remove) |
| 808 | nil nil t))) |
| 809 | |
| 810 | (defun magit-rebase--perl-editor (action since) |
| 811 | (let ((commit (magit-rev-abbrev (magit-rebase--target-commit since)))) |
| 812 | (format "%s -i -p -e '++$x if not $x and s/^pick %s/%s %s/'" |
| 813 | magit-perl-executable |
| 814 | commit |
| 815 | (cl-case action |
| 816 | (edit "edit") |
| 817 | (remove "noop\n# pick") |
| 818 | (reword "reword") |
| 819 | (t (error "Unknown action: %s" action))) |
| 820 | commit))) |
| 821 | |
| 822 | ;;;###autoload |
| 823 | (defun magit-rebase-continue (&optional noedit) |
| 824 | "Restart the current rebasing operation. |
| 825 | In some cases this pops up a commit message buffer for you do |
| 826 | edit. With a prefix argument the old message is reused as-is." |
| 827 | (interactive "P") |
| 828 | (cond |
| 829 | ((not (magit-rebase-in-progress-p)) |
| 830 | (user-error "No rebase in progress")) |
| 831 | ((magit-anything-unstaged-p t) |
| 832 | (user-error "Cannot continue rebase with unstaged changes")) |
| 833 | (t |
| 834 | (let ((dir (magit-gitdir))) |
| 835 | (when (and (magit-anything-staged-p) |
| 836 | (file-exists-p (expand-file-name "rebase-merge" dir)) |
| 837 | (not (member (magit-toplevel) |
| 838 | magit--rebase-public-edit-confirmed))) |
| 839 | (magit-commit-amend-assert |
| 840 | (magit-file-line (expand-file-name "rebase-merge/orig-head" dir))))) |
| 841 | (if noedit |
| 842 | (with-environment-variables (("GIT_EDITOR" "true")) |
| 843 | (magit-run-git-async (magit--rebase-resume-command) "--continue") |
| 844 | (set-process-sentinel magit-this-process |
| 845 | #'magit-sequencer-process-sentinel) |
| 846 | magit-this-process) |
| 847 | (magit-run-git-sequencer (magit--rebase-resume-command) "--continue"))))) |
| 848 | |
| 849 | ;;;###autoload |
| 850 | (defun magit-rebase-skip () |
| 851 | "Skip the current commit and restart the current rebase operation." |
| 852 | (interactive) |
| 853 | (unless (magit-rebase-in-progress-p) |
| 854 | (user-error "No rebase in progress")) |
| 855 | (magit-run-git-sequencer (magit--rebase-resume-command) "--skip")) |
| 856 | |
| 857 | ;;;###autoload |
| 858 | (defun magit-rebase-edit () |
| 859 | "Edit the todo list of the current rebase operation." |
| 860 | (interactive) |
| 861 | (unless (magit-rebase-in-progress-p) |
| 862 | (user-error "No rebase in progress")) |
| 863 | (magit-run-git-sequencer "rebase" "--edit-todo")) |
| 864 | |
| 865 | ;;;###autoload |
| 866 | (defun magit-rebase-abort () |
| 867 | "Abort the current rebase operation, restoring the original branch." |
| 868 | (interactive) |
| 869 | (unless (magit-rebase-in-progress-p) |
| 870 | (user-error "No rebase in progress")) |
| 871 | (magit-confirm 'abort-rebase "Abort this rebase") |
| 872 | (magit-run-git (magit--rebase-resume-command) "--abort")) |
| 873 | |
| 874 | (defun magit-rebase-in-progress-p () |
| 875 | "Return t if a rebase is in progress." |
| 876 | (let ((dir (magit-gitdir))) |
| 877 | (or (file-exists-p (expand-file-name "rebase-merge" dir)) |
| 878 | (file-exists-p (expand-file-name "rebase-apply/onto" dir))))) |
| 879 | |
| 880 | (defun magit--rebase-resume-command () |
| 881 | (if (file-exists-p (expand-file-name "rebase-recursive" (magit-gitdir))) |
| 882 | "rbr" |
| 883 | "rebase")) |
| 884 | |
| 885 | (defun magit-rebase--get-state-lines (file) |
| 886 | (and (magit-rebase-in-progress-p) |
| 887 | (let ((dir (magit-gitdir))) |
| 888 | (magit-file-line |
| 889 | (expand-file-name |
| 890 | (concat (if (file-directory-p (expand-file-name "rebase-merge" dir)) |
| 891 | "rebase-merge/" |
| 892 | "rebase-apply/") |
| 893 | file) |
| 894 | dir))))) |
| 895 | |
| 896 | ;;; Sections |
| 897 | |
| 898 | (defun magit-insert-sequencer-sequence () |
| 899 | "Insert section for the on-going cherry-pick or revert sequence. |
| 900 | If no such sequence is in progress, do nothing." |
| 901 | (let ((picking (magit-cherry-pick-in-progress-p))) |
| 902 | (when (or picking (magit-revert-in-progress-p)) |
| 903 | (let ((dir (magit-gitdir))) |
| 904 | (magit-insert-section (sequence) |
| 905 | (magit-insert-heading (if picking "Cherry Picking" "Reverting")) |
| 906 | (when-let ((lines (cdr (magit-file-lines |
| 907 | (expand-file-name "sequencer/todo" dir))))) |
| 908 | (dolist (line (nreverse lines)) |
| 909 | (when (string-match |
| 910 | "^\\(pick\\|revert\\) \\([^ ]+\\) \\(.*\\)$" line) |
| 911 | (magit-bind-match-strings (cmd hash msg) line |
| 912 | (magit-insert-section (commit hash) |
| 913 | (insert (propertize cmd 'font-lock-face 'magit-sequence-pick) |
| 914 | " " (propertize hash 'font-lock-face 'magit-hash) |
| 915 | " " msg "\n")))))) |
| 916 | (magit-sequence-insert-sequence |
| 917 | (magit-file-line |
| 918 | (expand-file-name (if picking "CHERRY_PICK_HEAD" "REVERT_HEAD") |
| 919 | dir)) |
| 920 | (magit-file-line (expand-file-name "sequencer/head" dir))) |
| 921 | (insert "\n")))))) |
| 922 | |
| 923 | (defun magit-insert-am-sequence () |
| 924 | "Insert section for the on-going patch applying sequence. |
| 925 | If no such sequence is in progress, do nothing." |
| 926 | (when (magit-am-in-progress-p) |
| 927 | (magit-insert-section (rebase-sequence) |
| 928 | (magit-insert-heading "Applying patches") |
| 929 | (let* ((patches (nreverse (magit-rebase-patches))) |
| 930 | (dir (expand-file-name "rebase-apply" (magit-gitdir))) |
| 931 | (i (string-to-number |
| 932 | (magit-file-line (expand-file-name "last" dir)))) |
| 933 | (cur (string-to-number |
| 934 | (magit-file-line (expand-file-name "next" dir)))) |
| 935 | patch commit) |
| 936 | (while (and patches (>= i cur)) |
| 937 | (setq patch (pop patches)) |
| 938 | (setq commit |
| 939 | (magit-commit-oid (cadr (split-string (magit-file-line patch))) |
| 940 | t)) |
| 941 | (cond ((and commit (= i cur)) |
| 942 | (magit-sequence-insert-commit |
| 943 | "stop" commit 'magit-sequence-stop)) |
| 944 | ((= i cur) |
| 945 | (magit-sequence-insert-am-patch |
| 946 | "stop" patch 'magit-sequence-stop)) |
| 947 | (commit |
| 948 | (magit-sequence-insert-commit |
| 949 | "pick" commit 'magit-sequence-pick)) |
| 950 | ((magit-sequence-insert-am-patch |
| 951 | "pick" patch 'magit-sequence-pick))) |
| 952 | (decf i))) |
| 953 | (magit-sequence-insert-sequence nil "ORIG_HEAD") |
| 954 | (insert ?\n)))) |
| 955 | |
| 956 | (defun magit-sequence-insert-am-patch (type patch face) |
| 957 | (magit-insert-section (file patch) |
| 958 | (let ((title |
| 959 | (with-temp-buffer |
| 960 | (insert-file-contents patch nil nil 4096) |
| 961 | (unless (re-search-forward "^Subject: " nil t) |
| 962 | (goto-char (point-min))) |
| 963 | (buffer-substring (point) (line-end-position))))) |
| 964 | (insert (propertize type 'font-lock-face face) |
| 965 | ?\s (propertize (file-name-nondirectory patch) |
| 966 | 'font-lock-face 'magit-hash) |
| 967 | ?\s title |
| 968 | ?\n)))) |
| 969 | |
| 970 | (defun magit-insert-rebase-sequence () |
| 971 | "Insert section for the on-going rebase sequence. |
| 972 | If no such sequence is in progress, do nothing." |
| 973 | (when (magit-rebase-in-progress-p) |
| 974 | (let* ((gitdir (magit-gitdir)) |
| 975 | (mergep (file-directory-p (expand-file-name "rebase-merge" gitdir))) |
| 976 | (dir (if mergep "rebase-merge/" "rebase-apply/")) |
| 977 | (name (thread-first (concat dir "head-name") |
| 978 | (expand-file-name gitdir) |
| 979 | magit-file-line)) |
| 980 | (onto (thread-first (concat dir "onto") |
| 981 | (expand-file-name gitdir) |
| 982 | magit-file-line)) |
| 983 | (onto (or (magit-rev-name onto name) |
| 984 | (magit-rev-name onto "refs/heads/*") onto)) |
| 985 | (name (or (magit-rev-name name "refs/heads/*") name))) |
| 986 | (magit-insert-section (rebase-sequence) |
| 987 | (magit-insert-heading (format "Rebasing %s onto %s" name onto)) |
| 988 | (if mergep |
| 989 | (magit-rebase-insert-merge-sequence onto) |
| 990 | (magit-rebase-insert-apply-sequence onto)) |
| 991 | (insert ?\n))))) |
| 992 | |
| 993 | (defun magit-rebase--todo () |
| 994 | "Return `git-rebase-action' instances for remaining rebase actions. |
| 995 | These are ordered the same way they'll be sorted in the status |
| 996 | buffer (i.e., the reverse of how they will be applied)." |
| 997 | (let ((comment-start (or (magit-get "core.commentChar") "#")) |
| 998 | (commits ()) |
| 999 | (actions ())) |
| 1000 | (with-temp-buffer |
| 1001 | (insert-file-contents |
| 1002 | (expand-file-name "rebase-merge/git-rebase-todo" (magit-gitdir))) |
| 1003 | (while (not (eobp)) |
| 1004 | (when-let ((obj (git-rebase-current-line t))) |
| 1005 | (push obj actions) |
| 1006 | (when (memq (oref obj action-type) '(commit merge)) |
| 1007 | (push obj commits))) |
| 1008 | (forward-line))) |
| 1009 | (let ((abbrevs |
| 1010 | (and commits |
| 1011 | (magit-git-lines |
| 1012 | "log" "--no-walk=unsorted" "--format=%h" |
| 1013 | (mapcar (lambda (obj) |
| 1014 | (if (eq (oref obj action-type) 'merge) |
| 1015 | (let ((options (oref obj action-options))) |
| 1016 | (and (string-match "-[cC] \\([^ ]+\\)" options) |
| 1017 | (match-str 1 options))) |
| 1018 | (oref obj target))) |
| 1019 | commits))))) |
| 1020 | (cl-assert (equal (length commits) (length abbrevs))) |
| 1021 | (while-let ((obj (pop commits)) |
| 1022 | (val (pop abbrevs))) |
| 1023 | (oset obj abbrev val))) |
| 1024 | actions)) |
| 1025 | |
| 1026 | (defun magit-rebase-insert-merge-sequence (onto) |
| 1027 | (dolist (obj (magit-rebase--todo)) |
| 1028 | (with-slots (action-type action action-options target abbrev trailer) obj |
| 1029 | (pcase action-type |
| 1030 | ((or 'commit (and 'merge (guard abbrev))) |
| 1031 | (magit-sequence-insert-commit action target 'magit-sequence-pick |
| 1032 | abbrev trailer)) |
| 1033 | ((guard action) (magit-sequence-insert-step action target))))) |
| 1034 | (let ((dir (magit-gitdir))) |
| 1035 | (magit-sequence-insert-sequence |
| 1036 | (magit-file-line (expand-file-name "rebase-merge/stopped-sha" dir)) |
| 1037 | onto |
| 1038 | (and$ (magit-file-lines (expand-file-name "rebase-merge/done" dir)) |
| 1039 | (cadr (split-string (car (last $)))))))) |
| 1040 | |
| 1041 | (defun magit-rebase-insert-apply-sequence (onto) |
| 1042 | (let* ((dir (magit-gitdir)) |
| 1043 | (rewritten |
| 1044 | (mapcar (##car (split-string %)) |
| 1045 | (magit-file-lines |
| 1046 | (expand-file-name "rebase-apply/rewritten" dir)))) |
| 1047 | (stop (magit-file-line |
| 1048 | (expand-file-name "rebase-apply/original-commit" dir)))) |
| 1049 | (dolist (patch (nreverse (cdr (magit-rebase-patches)))) |
| 1050 | (let ((hash (cadr (split-string (magit-file-line patch))))) |
| 1051 | (unless (or (member hash rewritten) |
| 1052 | (equal hash stop)) |
| 1053 | (magit-sequence-insert-commit "pick" hash 'magit-sequence-pick)))) |
| 1054 | (magit-sequence-insert-sequence |
| 1055 | (magit-file-line (expand-file-name "rebase-apply/original-commit" dir)) |
| 1056 | onto))) |
| 1057 | |
| 1058 | (defun magit-rebase-patches () |
| 1059 | (directory-files (expand-file-name "rebase-apply" (magit-gitdir)) |
| 1060 | t "\\`[0-9]\\{4\\}\\'")) |
| 1061 | |
| 1062 | (defun magit-sequence-insert-sequence (stop onto &optional orig) |
| 1063 | (let ((head (magit-rev-parse "HEAD")) done) |
| 1064 | (setq onto (if onto (magit-rev-parse onto) head)) |
| 1065 | (setq done (mapcar (##split-string % "\0") |
| 1066 | (magit-git-lines "log" "--format=%H%x00%h%x00%s" |
| 1067 | (concat onto "..HEAD")))) |
| 1068 | (when (and stop (not (assoc (magit-rev-parse stop) done))) |
| 1069 | (let ((id (magit-patch-id stop))) |
| 1070 | (if-let ((matched (car (assoc (##equal (magit-patch-id %) id) done)))) |
| 1071 | (setq stop matched) |
| 1072 | (cond |
| 1073 | ((assoc (##magit-rev-equal % stop) done) |
| 1074 | ;; The commit's testament has been executed. |
| 1075 | (magit-sequence-insert-commit "void" stop 'magit-sequence-drop)) |
| 1076 | ;; The faith of the commit is still undecided... |
| 1077 | ((magit-anything-unmerged-p) |
| 1078 | ;; ...and time travel isn't for the faint of heart. |
| 1079 | (magit-sequence-insert-commit "join" stop 'magit-sequence-part)) |
| 1080 | ((magit-anything-modified-p t) |
| 1081 | ;; ...and the dust hasn't settled yet... |
| 1082 | (magit-sequence-insert-commit |
| 1083 | (let* ((magit--refresh-cache nil) |
| 1084 | (staged (magit-commit-tree "oO" nil "HEAD")) |
| 1085 | (unstaged (magit-commit-worktree "oO" "--reset"))) |
| 1086 | (cond |
| 1087 | ;; ...but we could end up at the same tree just by committing. |
| 1088 | ((or (magit-rev-equal staged stop) |
| 1089 | (magit-rev-equal unstaged stop)) |
| 1090 | "goal") |
| 1091 | ;; ...but the changes are still there, untainted. |
| 1092 | ((or (equal (magit-patch-id staged) id) |
| 1093 | (equal (magit-patch-id unstaged) id)) |
| 1094 | "same") |
| 1095 | ;; ...and some changes are gone and/or others were added. |
| 1096 | ("work"))) |
| 1097 | stop 'magit-sequence-part)) |
| 1098 | ;; The commit is definitely gone... |
| 1099 | ((assoc (##magit-rev-equal % stop) done) |
| 1100 | ;; ...but all of its changes are still in effect. |
| 1101 | (magit-sequence-insert-commit "poof" stop 'magit-sequence-drop)) |
| 1102 | ;; ...and some changes are gone and/or other changes were added. |
| 1103 | ((magit-sequence-insert-commit "gone" stop 'magit-sequence-drop))) |
| 1104 | (setq stop nil)))) |
| 1105 | (pcase-dolist (`(,rev ,abbrev ,msg) done) |
| 1106 | (apply #'magit-sequence-insert-commit |
| 1107 | (cond ((equal rev stop) |
| 1108 | ;; ...but its reincarnation lives on. |
| 1109 | ;; Or it didn't die in the first place. |
| 1110 | (list (if (and (equal rev head) |
| 1111 | (equal (magit-patch-id rev) |
| 1112 | (magit-patch-id orig))) |
| 1113 | "stop" ; We haven't done anything yet. |
| 1114 | "like") ; There are new commits. |
| 1115 | rev (if (equal rev head) |
| 1116 | 'magit-sequence-head |
| 1117 | 'magit-sequence-stop) |
| 1118 | abbrev msg)) |
| 1119 | ((equal rev head) |
| 1120 | (list "done" rev 'magit-sequence-head abbrev msg)) |
| 1121 | ((list "done" rev 'magit-sequence-done abbrev msg))))) |
| 1122 | (magit-sequence-insert-commit "onto" onto |
| 1123 | (if (equal onto head) |
| 1124 | 'magit-sequence-head |
| 1125 | 'magit-sequence-onto)))) |
| 1126 | |
| 1127 | (defun magit-sequence-insert-commit (type hash face &optional abbrev msg) |
| 1128 | (magit-insert-section (commit hash) |
| 1129 | (magit-insert-heading |
| 1130 | (magit--propertize-face type face) " " |
| 1131 | (if abbrev |
| 1132 | (concat (magit--propertize-face abbrev 'magit-hash) " " msg "\n") |
| 1133 | (concat (magit-format-rev-summary hash) "\n"))))) |
| 1134 | |
| 1135 | (defun magit-sequence-insert-step (type target) |
| 1136 | (magit-insert-section (rebase-step (cons type target)) |
| 1137 | (magit-insert-heading |
| 1138 | (propertize type 'font-lock-face 'magit-sequence-pick) |
| 1139 | (and target |
| 1140 | (concat "\s" |
| 1141 | (propertize target 'font-lock-face 'git-rebase-label)))))) |
| 1142 | |
| 1143 | ;;; _ |
| 1144 | (provide 'magit-sequence) |
| 1145 | ;; Local Variables: |
| 1146 | ;; read-symbol-shorthands: ( |
| 1147 | ;; ("and$" . "cond-let--and$") |
| 1148 | ;; ("thread$" . "cond-let--thread$") |
| 1149 | ;; ("when$" . "cond-let--when$") |
| 1150 | ;; ("and-let*" . "cond-let--and-let*") |
| 1151 | ;; ("and-let" . "cond-let--and-let") |
| 1152 | ;; ("if-let*" . "cond-let--if-let*") |
| 1153 | ;; ("if-let" . "cond-let--if-let") |
| 1154 | ;; ("when-let*" . "cond-let--when-let*") |
| 1155 | ;; ("when-let" . "cond-let--when-let") |
| 1156 | ;; ("while-let*" . "cond-let--while-let*") |
| 1157 | ;; ("while-let" . "cond-let--while-let") |
| 1158 | ;; ("match-string" . "match-string") |
| 1159 | ;; ("match-str" . "match-string-no-properties")) |
| 1160 | ;; End: |
| 1161 | ;;; magit-sequence.el ends here |