git-rebase.el (32889B)
1 ;;; git-rebase.el --- Edit Git rebase files -*- lexical-binding:t -*- 2 3 ;; Copyright (C) 2008-2024 The Magit Project Contributors 4 5 ;; Author: Phil Jackson <phil@shellarchive.co.uk> 6 ;; Maintainer: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev> 7 8 ;; SPDX-License-Identifier: GPL-3.0-or-later 9 10 ;; Magit is free software: you can redistribute it and/or modify it 11 ;; under the terms of the GNU General Public License as published by 12 ;; the Free Software Foundation, either version 3 of the License, or 13 ;; (at your option) any later version. 14 ;; 15 ;; Magit is distributed in the hope that it will be useful, but WITHOUT 16 ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 17 ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 18 ;; License for more details. 19 ;; 20 ;; You should have received a copy of the GNU General Public License 21 ;; along with Magit. If not, see <https://www.gnu.org/licenses/>. 22 23 ;;; Commentary: 24 25 ;; This package assists the user in editing the list of commits to be 26 ;; rewritten during an interactive rebase. 27 28 ;; When the user initiates an interactive rebase, e.g., using "r e" in 29 ;; a Magit buffer or on the command line using "git rebase -i REV", 30 ;; Git invokes the `$GIT_SEQUENCE_EDITOR' (or if that is undefined 31 ;; `$GIT_EDITOR' or even `$EDITOR') letting the user rearrange, drop, 32 ;; reword, edit, and squash commits. 33 34 ;; This package provides the major-mode `git-rebase-mode' which makes 35 ;; doing so much more fun, by making the buffer more colorful and 36 ;; providing the following commands: 37 ;; 38 ;; C-c C-c Tell Git to make it happen. 39 ;; C-c C-k Tell Git that you changed your mind, i.e., abort. 40 ;; 41 ;; p Move point to previous line. 42 ;; n Move point to next line. 43 ;; 44 ;; M-p Move the commit at point up. 45 ;; M-n Move the commit at point down. 46 ;; 47 ;; k Drop the commit at point. 48 ;; c Don't drop the commit at point. 49 ;; r Change the message of the commit at point. 50 ;; e Edit the commit at point. 51 ;; s Squash the commit at point, into the one above. 52 ;; f Like "s" but don't also edit the commit message. 53 ;; b Break for editing at this point in the sequence. 54 ;; x Add a script to be run with the commit at point 55 ;; being checked out. 56 ;; z Add noop action at point. 57 ;; 58 ;; SPC Show the commit at point in another buffer. 59 ;; RET Show the commit at point in another buffer and 60 ;; select its window. 61 ;; C-/ Undo last change. 62 ;; 63 ;; Commands for --rebase-merges: 64 ;; l Associate label with current HEAD in sequence. 65 ;; MM Merge specified revisions into HEAD. 66 ;; Mt Toggle whether the merge will invoke an editor 67 ;; before committing. 68 ;; t Reset HEAD to the specified label. 69 70 ;; You should probably also read the `git-rebase' manpage. 71 72 ;;; Code: 73 74 (require 'magit) 75 76 (require 'easymenu) 77 (require 'server) 78 (require 'with-editor) 79 80 (defvar recentf-exclude) 81 82 ;;; Options 83 ;;;; Variables 84 85 (defgroup git-rebase nil 86 "Edit Git rebase sequences." 87 :link '(info-link "(magit)Editing Rebase Sequences") 88 :group 'tools) 89 90 (defcustom git-rebase-auto-advance t 91 "Whether to move to next line after changing a line." 92 :group 'git-rebase 93 :type 'boolean) 94 95 (defcustom git-rebase-show-instructions t 96 "Whether to show usage instructions inside the rebase buffer." 97 :group 'git-rebase 98 :type 'boolean) 99 100 (defcustom git-rebase-confirm-cancel t 101 "Whether confirmation is required to cancel." 102 :group 'git-rebase 103 :type 'boolean) 104 105 ;;;; Faces 106 107 (defgroup git-rebase-faces nil 108 "Faces used by Git-Rebase mode." 109 :group 'faces 110 :group 'git-rebase) 111 112 (defface git-rebase-hash '((t :inherit magit-hash)) 113 "Face for commit hashes." 114 :group 'git-rebase-faces) 115 116 (defface git-rebase-label '((t :inherit magit-refname)) 117 "Face for labels in label, merge, and reset lines." 118 :group 'git-rebase-faces) 119 120 (defface git-rebase-description '((t nil)) 121 "Face for commit descriptions." 122 :group 'git-rebase-faces) 123 124 (defface git-rebase-action 125 '((t :inherit font-lock-keyword-face)) 126 "Face for action keywords." 127 :group 'git-rebase-faces) 128 129 (defface git-rebase-killed-action 130 '((t :inherit font-lock-comment-face :strike-through t)) 131 "Face for commented commit action lines." 132 :group 'git-rebase-faces) 133 134 (defface git-rebase-comment-hash 135 '((t :inherit git-rebase-hash :weight bold)) 136 "Face for commit hashes in commit message comments." 137 :group 'git-rebase-faces) 138 139 (defface git-rebase-comment-heading 140 '((t :inherit font-lock-keyword-face)) 141 "Face for headings in rebase message comments." 142 :group 'git-rebase-faces) 143 144 ;;; Keymaps 145 146 (defvar-keymap git-rebase-mode-map 147 :doc "Keymap for Git-Rebase mode." 148 :parent special-mode-map 149 "C-m" #'git-rebase-show-commit 150 "p" #'git-rebase-backward-line 151 "n" #'forward-line 152 "M-p" #'git-rebase-move-line-up 153 "M-n" #'git-rebase-move-line-down 154 "c" #'git-rebase-pick 155 "k" #'git-rebase-kill-line 156 "C-k" #'git-rebase-kill-line 157 "b" #'git-rebase-break 158 "e" #'git-rebase-edit 159 "l" #'git-rebase-label 160 "M M" #'git-rebase-merge 161 "M t" #'git-rebase-merge-toggle-editmsg 162 "m" #'git-rebase-edit 163 "f" #'git-rebase-fixup 164 "q" #'undefined 165 "r" #'git-rebase-reword 166 "w" #'git-rebase-reword 167 "s" #'git-rebase-squash 168 "t" #'git-rebase-reset 169 "u" #'git-rebase-update-ref 170 "x" #'git-rebase-exec 171 "y" #'git-rebase-insert 172 "z" #'git-rebase-noop 173 "SPC" #'git-rebase-show-or-scroll-up 174 "DEL" #'git-rebase-show-or-scroll-down 175 "C-x C-t" #'git-rebase-move-line-up 176 "M-<up>" #'git-rebase-move-line-up 177 "M-<down>" #'git-rebase-move-line-down 178 "<remap> <undo>" #'git-rebase-undo) 179 (put 'git-rebase-reword :advertised-binding (kbd "r")) 180 (put 'git-rebase-move-line-up :advertised-binding (kbd "M-p")) 181 (put 'git-rebase-kill-line :advertised-binding (kbd "k")) 182 183 (easy-menu-define git-rebase-mode-menu git-rebase-mode-map 184 "Git-Rebase mode menu" 185 '("Rebase" 186 ["Pick" git-rebase-pick t] 187 ["Reword" git-rebase-reword t] 188 ["Edit" git-rebase-edit t] 189 ["Squash" git-rebase-squash t] 190 ["Fixup" git-rebase-fixup t] 191 ["Kill" git-rebase-kill-line t] 192 ["Noop" git-rebase-noop t] 193 ["Execute" git-rebase-exec t] 194 ["Move Down" git-rebase-move-line-down t] 195 ["Move Up" git-rebase-move-line-up t] 196 "---" 197 ["Cancel" with-editor-cancel t] 198 ["Finish" with-editor-finish t])) 199 200 (defvar git-rebase-command-descriptions 201 '((with-editor-finish . "tell Git to make it happen") 202 (with-editor-cancel . "tell Git that you changed your mind, i.e., abort") 203 (git-rebase-backward-line . "move point to previous line") 204 (forward-line . "move point to next line") 205 (git-rebase-move-line-up . "move the commit at point up") 206 (git-rebase-move-line-down . "move the commit at point down") 207 (git-rebase-show-or-scroll-up . "show the commit at point in another buffer") 208 (git-rebase-show-commit 209 . "show the commit at point in another buffer and select its window") 210 (undo . "undo last change") 211 (git-rebase-kill-line . "drop the commit at point") 212 (git-rebase-insert . "insert a line for an arbitrary commit") 213 (git-rebase-noop . "add noop action at point"))) 214 215 ;;; Commands 216 217 (defun git-rebase-pick () 218 "Use commit on current line. 219 If the region is active, act on all lines touched by the region." 220 (interactive) 221 (git-rebase-set-action "pick")) 222 223 (defun git-rebase-reword () 224 "Edit message of commit on current line. 225 If the region is active, act on all lines touched by the region." 226 (interactive) 227 (git-rebase-set-action "reword")) 228 229 (defun git-rebase-edit () 230 "Stop at the commit on the current line. 231 If the region is active, act on all lines touched by the region." 232 (interactive) 233 (git-rebase-set-action "edit")) 234 235 (defun git-rebase-squash () 236 "Meld commit on current line into previous commit, edit message. 237 If the region is active, act on all lines touched by the region." 238 (interactive) 239 (git-rebase-set-action "squash")) 240 241 (defun git-rebase-fixup () 242 "Meld commit on current line into previous commit, discard its message. 243 If the region is active, act on all lines touched by the region." 244 (interactive) 245 (git-rebase-set-action "fixup")) 246 247 (defvar-local git-rebase-comment-re nil) 248 249 (defvar git-rebase-short-options 250 '((?b . "break") 251 (?e . "edit") 252 (?f . "fixup") 253 (?l . "label") 254 (?m . "merge") 255 (?p . "pick") 256 (?r . "reword") 257 (?s . "squash") 258 (?t . "reset") 259 (?u . "update-ref") 260 (?x . "exec")) 261 "Alist mapping single key of an action to the full name.") 262 263 (defclass git-rebase-action () 264 (;; action-type: commit, exec, bare, label, merge 265 (action-type :initarg :action-type :initform nil) 266 ;; Examples for each action type: 267 ;; | action | action options | target | trailer | 268 ;; |--------+----------------+---------+---------| 269 ;; | pick | | hash | subject | 270 ;; | exec | | command | | 271 ;; | noop | | | | 272 ;; | reset | | name | subject | 273 ;; | merge | -C hash | name | subject | 274 (action :initarg :action :initform nil) 275 (action-options :initarg :action-options :initform nil) 276 (target :initarg :target :initform nil) 277 (trailer :initarg :trailer :initform nil) 278 (comment-p :initarg :comment-p :initform nil))) 279 280 (defvar git-rebase-line-regexps 281 `((commit . ,(concat 282 (regexp-opt '("e" "edit" 283 "f" "fixup" 284 "p" "pick" 285 "r" "reword" 286 "s" "squash") 287 "\\(?1:") 288 " \\(?3:[^ \n]+\\) ?\\(?4:.*\\)")) 289 (exec . "\\(?1:x\\|exec\\) \\(?3:.*\\)") 290 (bare . ,(concat (regexp-opt '("b" "break" "noop") "\\(?1:") 291 " *$")) 292 (label . ,(concat (regexp-opt '("l" "label" 293 "t" "reset" 294 "u" "update-ref") 295 "\\(?1:") 296 " \\(?3:[^ \n]+\\) ?\\(?4:.*\\)")) 297 (merge . ,(concat "\\(?1:m\\|merge\\) " 298 "\\(?:\\(?2:-[cC] [^ \n]+\\) \\)?" 299 "\\(?3:[^ \n]+\\)" 300 " ?\\(?4:.*\\)")))) 301 302 ;;;###autoload 303 (defun git-rebase-current-line () 304 "Parse current line into a `git-rebase-action' instance. 305 If the current line isn't recognized as a rebase line, an 306 instance with all nil values is returned." 307 (save-excursion 308 (goto-char (line-beginning-position)) 309 (if-let ((re-start (concat "^\\(?5:" (regexp-quote comment-start) 310 "\\)? *")) 311 (type (seq-some (lambda (arg) 312 (let ((case-fold-search nil)) 313 (and (looking-at (concat re-start (cdr arg))) 314 (car arg)))) 315 git-rebase-line-regexps))) 316 (git-rebase-action 317 :action-type type 318 :action (and-let* ((action (match-string-no-properties 1))) 319 (or (cdr (assoc action git-rebase-short-options)) 320 action)) 321 :action-options (match-string-no-properties 2) 322 :target (match-string-no-properties 3) 323 :trailer (match-string-no-properties 4) 324 :comment-p (and (match-string 5) t)) 325 ;; Use default empty class rather than nil to ease handling. 326 (git-rebase-action)))) 327 328 (defun git-rebase-set-action (action) 329 "Set action of commit line to ACTION. 330 If the region is active, operate on all lines that it touches. 331 Otherwise, operate on the current line. As a special case, an 332 ACTION of nil comments the rebase line, regardless of its action 333 type." 334 (pcase (git-rebase-region-bounds t) 335 (`(,beg ,end) 336 (let ((end-marker (copy-marker end)) 337 (pt-below-p (and mark-active (< (mark) (point))))) 338 (set-marker-insertion-type end-marker t) 339 (goto-char beg) 340 (while (< (point) end-marker) 341 (with-slots (action-type target trailer comment-p) 342 (git-rebase-current-line) 343 (cond 344 ((and action (eq action-type 'commit)) 345 (let ((inhibit-read-only t)) 346 (magit-delete-line) 347 (insert (concat action " " target " " trailer "\n")))) 348 ((and action-type (not (or action comment-p))) 349 (let ((inhibit-read-only t)) 350 (insert comment-start " ")) 351 (forward-line)) 352 (t 353 ;; In the case of --rebase-merges, commit lines may have 354 ;; other lines with other action types, empty lines, and 355 ;; "Branch" comments interspersed. Move along. 356 (forward-line))))) 357 (goto-char 358 (if git-rebase-auto-advance 359 end-marker 360 (if pt-below-p (1- end-marker) beg))) 361 (goto-char (line-beginning-position)))) 362 (_ (ding)))) 363 364 (defun git-rebase-line-p (&optional pos) 365 (save-excursion 366 (when pos (goto-char pos)) 367 (and (oref (git-rebase-current-line) action-type) 368 t))) 369 370 (defun git-rebase-region-bounds (&optional fallback) 371 "Return region bounds if both ends touch rebase lines. 372 Each bound is extended to include the entire line touched by the 373 point or mark. If the region isn't active and FALLBACK is 374 non-nil, return the beginning and end of the current rebase line, 375 if any." 376 (cond 377 ((use-region-p) 378 (let ((beg (save-excursion (goto-char (region-beginning)) 379 (line-beginning-position))) 380 (end (save-excursion (goto-char (region-end)) 381 (line-end-position)))) 382 (when (and (git-rebase-line-p beg) 383 (git-rebase-line-p end)) 384 (list beg (1+ end))))) 385 ((and fallback (git-rebase-line-p)) 386 (list (line-beginning-position) 387 (1+ (line-end-position)))))) 388 389 (defun git-rebase-move-line-down (n) 390 "Move the current commit (or command) N lines down. 391 If N is negative, move the commit up instead. With an active 392 region, move all the lines that the region touches, not just the 393 current line." 394 (interactive "p") 395 (pcase-let* ((`(,beg ,end) 396 (or (git-rebase-region-bounds) 397 (list (line-beginning-position) 398 (1+ (line-end-position))))) 399 (pt-offset (- (point) beg)) 400 (mark-offset (and mark-active (- (mark) beg)))) 401 (save-restriction 402 (narrow-to-region 403 (point-min) 404 (1- 405 (if git-rebase-show-instructions 406 (save-excursion 407 (goto-char (point-min)) 408 (while (or (git-rebase-line-p) 409 ;; The output for --rebase-merges has empty 410 ;; lines and "Branch" comments interspersed. 411 (looking-at-p "^$") 412 (looking-at-p (concat git-rebase-comment-re 413 " Branch"))) 414 (forward-line)) 415 (line-beginning-position)) 416 (point-max)))) 417 (if (or (and (< n 0) (= beg (point-min))) 418 (and (> n 0) (= end (point-max))) 419 (> end (point-max))) 420 (ding) 421 (goto-char (if (< n 0) beg end)) 422 (forward-line n) 423 (atomic-change-group 424 (let ((inhibit-read-only t)) 425 (insert (delete-and-extract-region beg end))) 426 (let ((new-beg (- (point) (- end beg)))) 427 (when (use-region-p) 428 (setq deactivate-mark nil) 429 (set-mark (+ new-beg mark-offset))) 430 (goto-char (+ new-beg pt-offset)))))))) 431 432 (defun git-rebase-move-line-up (n) 433 "Move the current commit (or command) N lines up. 434 If N is negative, move the commit down instead. With an active 435 region, move all the lines that the region touches, not just the 436 current line." 437 (interactive "p") 438 (git-rebase-move-line-down (- n))) 439 440 (defun git-rebase-highlight-region (start end window rol) 441 (let ((inhibit-read-only t) 442 (deactivate-mark nil) 443 (bounds (git-rebase-region-bounds))) 444 (mapc #'delete-overlay magit-section-highlight-overlays) 445 (when bounds 446 (magit-section-make-overlay (car bounds) (cadr bounds) 447 'magit-section-heading-selection)) 448 (if (and bounds (not magit-section-keep-region-overlay)) 449 (funcall (default-value 'redisplay-unhighlight-region-function) rol) 450 (funcall (default-value 'redisplay-highlight-region-function) 451 start end window rol)))) 452 453 (defun git-rebase-unhighlight-region (rol) 454 (mapc #'delete-overlay magit-section-highlight-overlays) 455 (funcall (default-value 'redisplay-unhighlight-region-function) rol)) 456 457 (defun git-rebase-kill-line () 458 "Kill the current action line. 459 If the region is active, act on all lines touched by the region." 460 (interactive) 461 (git-rebase-set-action nil)) 462 463 (defun git-rebase-insert (rev) 464 "Read an arbitrary commit and insert it below current line." 465 (interactive (list (magit-read-branch-or-commit "Insert revision"))) 466 (forward-line) 467 (if-let ((info (magit-rev-format "%h %s" rev))) 468 (let ((inhibit-read-only t)) 469 (insert "pick " info ?\n)) 470 (user-error "Unknown revision"))) 471 472 (defun git-rebase-set-noncommit-action (action value-fn arg) 473 (goto-char (line-beginning-position)) 474 (pcase-let* ((inhibit-read-only t) 475 (`(,initial ,trailer ,comment-p) 476 (and (not arg) 477 (with-slots ((ln-action action) 478 target trailer comment-p) 479 (git-rebase-current-line) 480 (and (equal ln-action action) 481 (list target trailer comment-p))))) 482 (value (funcall value-fn initial))) 483 (pcase (list value initial comment-p) 484 (`("" nil ,_) 485 (ding)) 486 (`("" ,_ ,_) 487 (magit-delete-line)) 488 (_ 489 (if initial 490 (magit-delete-line) 491 (forward-line)) 492 (insert (concat action " " value 493 (and (equal value initial) 494 trailer 495 (concat " " trailer)) 496 "\n")) 497 (unless git-rebase-auto-advance 498 (forward-line -1)))))) 499 500 (defun git-rebase-exec (arg) 501 "Insert a shell command to be run after the current commit. 502 503 If there already is such a command on the current line, then edit 504 that instead. With a prefix argument insert a new command even 505 when there already is one on the current line. With empty input 506 remove the command on the current line, if any." 507 (interactive "P") 508 (git-rebase-set-noncommit-action 509 "exec" 510 (lambda (initial) (read-shell-command "Execute: " initial)) 511 arg)) 512 513 (defun git-rebase-label (arg) 514 "Add a label after the current commit. 515 If there already is a label on the current line, then edit that 516 instead. With a prefix argument, insert a new label even when 517 there is already a label on the current line. With empty input, 518 remove the label on the current line, if any." 519 (interactive "P") 520 (git-rebase-set-noncommit-action 521 "label" 522 (lambda (initial) 523 (read-from-minibuffer 524 "Label: " initial magit-minibuffer-local-ns-map)) 525 arg)) 526 527 (defun git-rebase-buffer-labels () 528 (let (labels) 529 (save-excursion 530 (goto-char (point-min)) 531 (while (re-search-forward "^\\(?:l\\|label\\) \\([^ \n]+\\)" nil t) 532 (push (match-string-no-properties 1) labels))) 533 (nreverse labels))) 534 535 (defun git-rebase-reset (arg) 536 "Reset the current HEAD to a label. 537 If there already is a reset command on the current line, then 538 edit that instead. With a prefix argument, insert a new reset 539 line even when point is already on a reset line. With empty 540 input, remove the reset command on the current line, if any." 541 (interactive "P") 542 (git-rebase-set-noncommit-action 543 "reset" 544 (lambda (initial) 545 (or (magit-completing-read "Label" (git-rebase-buffer-labels) 546 nil t initial) 547 "")) 548 arg)) 549 550 (defun git-rebase-update-ref (arg) 551 "Insert an update-ref action after the current line. 552 If there is already an update-ref action on the current line, 553 then edit that instead. With a prefix argument, insert a new 554 action even when there is already one on the current line. With 555 empty input, remove the action on the current line, if any." 556 (interactive "P") 557 (git-rebase-set-noncommit-action 558 "update-ref" 559 (lambda (initial) 560 (or (magit-completing-read "Ref" (magit-list-refs) nil nil initial) 561 "")) 562 arg)) 563 564 (defun git-rebase-merge (arg) 565 "Add a merge command after the current commit. 566 If there is already a merge command on the current line, then 567 replace that command instead. With a prefix argument, insert a 568 new merge command even when there is already one on the current 569 line. With empty input, remove the merge command on the current 570 line, if any." 571 (interactive "P") 572 (git-rebase-set-noncommit-action 573 "merge" 574 (lambda (_) 575 (or (magit-completing-read "Merge" (git-rebase-buffer-labels)) 576 "")) 577 arg)) 578 579 (defun git-rebase-merge-toggle-editmsg () 580 "Toggle whether an editor is invoked when performing the merge at point. 581 When a merge command uses a lower-case -c, the message for the 582 specified commit will be opened in an editor before creating the 583 commit. For an upper-case -C, the message will be used as is." 584 (interactive) 585 (with-slots (action-type target action-options trailer) 586 (git-rebase-current-line) 587 (if (eq action-type 'merge) 588 (let ((inhibit-read-only t)) 589 (magit-delete-line) 590 (insert 591 (format "merge %s %s %s\n" 592 (replace-regexp-in-string 593 "-[cC]" (lambda (c) 594 (if (equal c "-c") "-C" "-c")) 595 action-options t t) 596 target 597 trailer))) 598 (ding)))) 599 600 (defun git-rebase-set-bare-action (action arg) 601 (goto-char (line-beginning-position)) 602 (with-slots ((ln-action action) comment-p) 603 (git-rebase-current-line) 604 (let ((same-action-p (equal action ln-action)) 605 (inhibit-read-only t)) 606 (when (or arg 607 (not ln-action) 608 (not same-action-p) 609 (and same-action-p comment-p)) 610 (unless (or arg (not same-action-p)) 611 (magit-delete-line)) 612 (insert action ?\n) 613 (unless git-rebase-auto-advance 614 (forward-line -1)))))) 615 616 (defun git-rebase-noop (&optional arg) 617 "Add noop action at point. 618 619 If the current line already contains a noop action, leave it 620 unchanged. If there is a commented noop action present, remove 621 the comment. Otherwise add a new noop action. With a prefix 622 argument insert a new noop action regardless of what is already 623 present on the current line. 624 625 A noop action can be used to make git perform a rebase even if 626 no commits are selected. Without the noop action present, git 627 would see an empty file and therefore do nothing." 628 (interactive "P") 629 (git-rebase-set-bare-action "noop" arg)) 630 631 (defun git-rebase-break (&optional arg) 632 "Add break action at point. 633 634 If there is a commented break action present, remove the comment. 635 If the current line already contains a break action, add another 636 break action only if a prefix argument is given. 637 638 A break action can be used to interrupt the rebase at the 639 specified point. It is particularly useful for pausing before 640 the first commit in the sequence. For other cases, the 641 equivalent behavior can be achieved with `git-rebase-edit'." 642 (interactive "P") 643 (git-rebase-set-bare-action "break" arg)) 644 645 (defun git-rebase-undo (&optional arg) 646 "Undo some previous changes. 647 Like `undo' but works in read-only buffers." 648 (interactive "P") 649 (let ((inhibit-read-only t)) 650 (undo arg))) 651 652 (defun git-rebase--show-commit (&optional scroll) 653 (let ((magit--disable-save-buffers t)) 654 (save-excursion 655 (goto-char (line-beginning-position)) 656 (if-let ((rev (with-slots (action-type target) 657 (git-rebase-current-line) 658 (and (eq action-type 'commit) 659 target)))) 660 (pcase scroll 661 ('up (magit-diff-show-or-scroll-up)) 662 ('down (magit-diff-show-or-scroll-down)) 663 (_ (apply #'magit-show-commit rev 664 (magit-diff-arguments 'magit-revision-mode)))) 665 (ding))))) 666 667 (defun git-rebase-show-commit () 668 "Show the commit on the current line if any." 669 (interactive) 670 (git-rebase--show-commit)) 671 672 (defun git-rebase-show-or-scroll-up () 673 "Update the commit buffer for commit on current line. 674 675 Either show the commit at point in the appropriate buffer, or if 676 that buffer is already being displayed in the current frame and 677 contains information about that commit, then instead scroll the 678 buffer up." 679 (interactive) 680 (git-rebase--show-commit 'up)) 681 682 (defun git-rebase-show-or-scroll-down () 683 "Update the commit buffer for commit on current line. 684 685 Either show the commit at point in the appropriate buffer, or if 686 that buffer is already being displayed in the current frame and 687 contains information about that commit, then instead scroll the 688 buffer down." 689 (interactive) 690 (git-rebase--show-commit 'down)) 691 692 (defun git-rebase-backward-line (&optional n) 693 "Move N lines backward (forward if N is negative). 694 Like `forward-line' but go into the opposite direction." 695 (interactive "p") 696 (forward-line (- (or n 1)))) 697 698 ;;; Mode 699 700 ;;;###autoload 701 (define-derived-mode git-rebase-mode special-mode "Git Rebase" 702 "Major mode for editing of a Git rebase file. 703 704 Rebase files are generated when you run \"git rebase -i\" or run 705 `magit-interactive-rebase'. They describe how Git should perform 706 the rebase. See the documentation for git-rebase (e.g., by 707 running \"man git-rebase\" at the command line) for details." 708 :group 'git-rebase 709 (setq comment-start (or (magit-get "core.commentChar") "#")) 710 (setq git-rebase-comment-re (concat "^" (regexp-quote comment-start))) 711 (setq font-lock-defaults (list (git-rebase-mode-font-lock-keywords) t t)) 712 (unless git-rebase-show-instructions 713 (let ((inhibit-read-only t)) 714 (flush-lines git-rebase-comment-re))) 715 (unless with-editor-mode 716 ;; Maybe already enabled when using `shell-command' or an Emacs shell. 717 (with-editor-mode 1)) 718 (when git-rebase-confirm-cancel 719 (add-hook 'with-editor-cancel-query-functions 720 #'git-rebase-cancel-confirm nil t)) 721 (setq-local redisplay-highlight-region-function 722 #'git-rebase-highlight-region) 723 (setq-local redisplay-unhighlight-region-function 724 #'git-rebase-unhighlight-region) 725 (add-hook 'with-editor-pre-cancel-hook #'git-rebase-autostash-save nil t) 726 (add-hook 'with-editor-post-cancel-hook #'git-rebase-autostash-apply nil t) 727 (setq imenu-prev-index-position-function 728 #'magit-imenu--rebase-prev-index-position-function) 729 (setq imenu-extract-index-name-function 730 #'magit-imenu--rebase-extract-index-name-function) 731 (when (boundp 'save-place) 732 (setq save-place nil))) 733 734 (defun git-rebase-cancel-confirm (force) 735 (or (not (buffer-modified-p)) 736 force 737 (magit-confirm 'abort-rebase "Abort this rebase" nil 'noabort))) 738 739 (defun git-rebase-autostash-save () 740 (when-let ((rev (magit-file-line 741 (expand-file-name "rebase-merge/autostash" (magit-gitdir))))) 742 (push (cons 'stash rev) with-editor-cancel-alist))) 743 744 (defun git-rebase-autostash-apply () 745 (when-let ((rev (cdr (assq 'stash with-editor-cancel-alist)))) 746 (magit-stash-apply rev))) 747 748 (defun git-rebase-match-comment-line (limit) 749 (re-search-forward (concat git-rebase-comment-re ".*") limit t)) 750 751 (defun git-rebase-mode-font-lock-keywords () 752 "Font lock keywords for Git-Rebase mode." 753 `((,(concat "^" (cdr (assq 'commit git-rebase-line-regexps))) 754 (1 'git-rebase-action) 755 (3 'git-rebase-hash) 756 (4 'git-rebase-description)) 757 (,(concat "^" (cdr (assq 'exec git-rebase-line-regexps))) 758 (1 'git-rebase-action) 759 (3 'git-rebase-description)) 760 (,(concat "^" (cdr (assq 'bare git-rebase-line-regexps))) 761 (1 'git-rebase-action)) 762 (,(concat "^" (cdr (assq 'label git-rebase-line-regexps))) 763 (1 'git-rebase-action) 764 (3 'git-rebase-label) 765 (4 'font-lock-comment-face)) 766 ("^\\(m\\(?:erge\\)?\\) -[Cc] \\([^ \n]+\\) \\([^ \n]+\\)\\( #.*\\)?" 767 (1 'git-rebase-action) 768 (2 'git-rebase-hash) 769 (3 'git-rebase-label) 770 (4 'font-lock-comment-face)) 771 ("^\\(m\\(?:erge\\)?\\) \\([^ \n]+\\)" 772 (1 'git-rebase-action) 773 (2 'git-rebase-label)) 774 (,(concat git-rebase-comment-re " *" 775 (cdr (assq 'commit git-rebase-line-regexps))) 776 0 'git-rebase-killed-action t) 777 (git-rebase-match-comment-line 0 'font-lock-comment-face) 778 ("\\[[^[]*\\]" 779 0 'magit-keyword t) 780 ("\\(?:fixup!\\|squash!\\)" 781 0 'magit-keyword-squash t) 782 (,(format "^%s Rebase \\([^ ]*\\) onto \\([^ ]*\\)" comment-start) 783 (1 'git-rebase-comment-hash t) 784 (2 'git-rebase-comment-hash t)) 785 (,(format "^%s \\(Commands:\\)" comment-start) 786 (1 'git-rebase-comment-heading t)) 787 (,(format "^%s Branch \\(.*\\)" comment-start) 788 (1 'git-rebase-label t)))) 789 790 (defun git-rebase-mode-show-keybindings () 791 "Modify the \"Commands:\" section of the comment Git generates 792 at the bottom of the file so that in place of the one-letter 793 abbreviation for the command, it shows the command's keybinding. 794 By default, this is the same except for the \"pick\" command." 795 (let ((inhibit-read-only t)) 796 (save-excursion 797 (goto-char (point-min)) 798 (when (and git-rebase-show-instructions 799 (re-search-forward 800 (concat git-rebase-comment-re "\\s-+p, pick") 801 nil t)) 802 (goto-char (line-beginning-position)) 803 (pcase-dolist (`(,cmd . ,desc) git-rebase-command-descriptions) 804 (insert (format (propertize "%s %s %s\n" 805 'font-lock-face 'font-lock-comment-face) 806 comment-start 807 (string-pad 808 (substitute-command-keys (format "\\[%s]" cmd)) 8) 809 desc))) 810 (while (re-search-forward 811 (concat git-rebase-comment-re "\\(?:" 812 "\\( \\.? *\\)\\|" 813 "\\( +\\)\\([^\n,],\\) \\([^\n ]+\\) \\)") 814 nil t) 815 (if (match-string 1) 816 (replace-match (make-string 10 ?\s) t t nil 1) 817 (let ((cmd (intern (concat "git-rebase-" (match-string 4))))) 818 (if (not (fboundp cmd)) 819 (delete-region (line-beginning-position) 820 (1+ (line-end-position))) 821 (add-text-properties (line-beginning-position) 822 (1+ (line-end-position)) 823 '(font-lock-face font-lock-comment-face)) 824 (replace-match " " t t nil 2) 825 (replace-match 826 (string-pad 827 (save-match-data 828 (substitute-command-keys (format "\\[%s]" cmd))) 829 8) 830 t t nil 3))))))))) 831 832 (add-hook 'git-rebase-mode-hook #'git-rebase-mode-show-keybindings t) 833 834 (defun git-rebase-mode-disable-before-save-hook () 835 (setq-local before-save-hook nil)) 836 837 (add-hook 'git-rebase-mode-hook #'git-rebase-mode-disable-before-save-hook) 838 839 ;;;###autoload 840 (defconst git-rebase-filename-regexp "/git-rebase-todo\\'") 841 ;;;###autoload 842 (add-to-list 'auto-mode-alist 843 (cons git-rebase-filename-regexp #'git-rebase-mode)) 844 845 (add-to-list 'with-editor-server-window-alist 846 (cons git-rebase-filename-regexp #'switch-to-buffer)) 847 848 (with-eval-after-load 'recentf 849 (add-to-list 'recentf-exclude git-rebase-filename-regexp)) 850 851 (add-to-list 'with-editor-file-name-history-exclude git-rebase-filename-regexp) 852 853 ;;; Imenu Support 854 855 (defun magit-imenu--rebase-prev-index-position-function () 856 "Move point to previous commit in git-rebase buffer. 857 Used as a value for `imenu-prev-index-position-function'." 858 (catch 'found 859 (while (not (bobp)) 860 (git-rebase-backward-line) 861 (when (git-rebase-line-p) 862 (throw 'found t))))) 863 864 (defun magit-imenu--rebase-extract-index-name-function () 865 "Return imenu name for line at point. 866 Point should be at the beginning of the line. This function 867 is used as a value for `imenu-extract-index-name-function'." 868 (buffer-substring-no-properties (line-beginning-position) 869 (line-end-position))) 870 871 ;;; _ 872 (provide 'git-rebase) 873 ;;; git-rebase.el ends here