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