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