config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

git-rebase.el (32908B)


      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   :interactive nil
    709   :group 'git-rebase
    710   (setq comment-start (or (magit-get "core.commentChar") "#"))
    711   (setq git-rebase-comment-re (concat "^" (regexp-quote comment-start)))
    712   (setq font-lock-defaults (list (git-rebase-mode-font-lock-keywords) t t))
    713   (unless git-rebase-show-instructions
    714     (let ((inhibit-read-only t))
    715       (flush-lines git-rebase-comment-re)))
    716   (unless with-editor-mode
    717     ;; Maybe already enabled when using `shell-command' or an Emacs shell.
    718     (with-editor-mode 1))
    719   (when git-rebase-confirm-cancel
    720     (add-hook 'with-editor-cancel-query-functions
    721               #'git-rebase-cancel-confirm nil t))
    722   (setq-local redisplay-highlight-region-function
    723               #'git-rebase-highlight-region)
    724   (setq-local redisplay-unhighlight-region-function
    725               #'git-rebase-unhighlight-region)
    726   (add-hook 'with-editor-pre-cancel-hook  #'git-rebase-autostash-save  nil t)
    727   (add-hook 'with-editor-post-cancel-hook #'git-rebase-autostash-apply nil t)
    728   (setq imenu-prev-index-position-function
    729         #'magit-imenu--rebase-prev-index-position-function)
    730   (setq imenu-extract-index-name-function
    731         #'magit-imenu--rebase-extract-index-name-function)
    732   (when (boundp 'save-place)
    733     (setq save-place nil)))
    734 
    735 (defun git-rebase-cancel-confirm (force)
    736   (or (not (buffer-modified-p))
    737       force
    738       (magit-confirm 'abort-rebase "Abort this rebase" nil 'noabort)))
    739 
    740 (defun git-rebase-autostash-save ()
    741   (when-let ((rev (magit-file-line
    742                    (expand-file-name "rebase-merge/autostash" (magit-gitdir)))))
    743     (push (cons 'stash rev) with-editor-cancel-alist)))
    744 
    745 (defun git-rebase-autostash-apply ()
    746   (when-let ((rev (cdr (assq 'stash with-editor-cancel-alist))))
    747     (magit-stash-apply rev)))
    748 
    749 (defun git-rebase-match-comment-line (limit)
    750   (re-search-forward (concat git-rebase-comment-re ".*") limit t))
    751 
    752 (defun git-rebase-mode-font-lock-keywords ()
    753   "Font lock keywords for Git-Rebase mode."
    754   `((,(concat "^" (cdr (assq 'commit git-rebase-line-regexps)))
    755      (1 'git-rebase-action)
    756      (3 'git-rebase-hash)
    757      (4 'git-rebase-description))
    758     (,(concat "^" (cdr (assq 'exec git-rebase-line-regexps)))
    759      (1 'git-rebase-action)
    760      (3 'git-rebase-description))
    761     (,(concat "^" (cdr (assq 'bare git-rebase-line-regexps)))
    762      (1 'git-rebase-action))
    763     (,(concat "^" (cdr (assq 'label git-rebase-line-regexps)))
    764      (1 'git-rebase-action)
    765      (3 'git-rebase-label)
    766      (4 'font-lock-comment-face))
    767     ("^\\(m\\(?:erge\\)?\\) -[Cc] \\([^ \n]+\\) \\([^ \n]+\\)\\( #.*\\)?"
    768      (1 'git-rebase-action)
    769      (2 'git-rebase-hash)
    770      (3 'git-rebase-label)
    771      (4 'font-lock-comment-face))
    772     ("^\\(m\\(?:erge\\)?\\) \\([^ \n]+\\)"
    773      (1 'git-rebase-action)
    774      (2 'git-rebase-label))
    775     (,(concat git-rebase-comment-re " *"
    776               (cdr (assq 'commit git-rebase-line-regexps)))
    777      0 'git-rebase-killed-action t)
    778     (git-rebase-match-comment-line 0 'font-lock-comment-face)
    779     ("\\[[^[]*\\]"
    780      0 'magit-keyword t)
    781     ("\\(?:fixup!\\|squash!\\)"
    782      0 'magit-keyword-squash t)
    783     (,(format "^%s Rebase \\([^ ]*\\) onto \\([^ ]*\\)" comment-start)
    784      (1 'git-rebase-comment-hash t)
    785      (2 'git-rebase-comment-hash t))
    786     (,(format "^%s \\(Commands:\\)" comment-start)
    787      (1 'git-rebase-comment-heading t))
    788     (,(format "^%s Branch \\(.*\\)" comment-start)
    789      (1 'git-rebase-label t))))
    790 
    791 (defun git-rebase-mode-show-keybindings ()
    792   "Modify the \"Commands:\" section of the comment Git generates
    793 at the bottom of the file so that in place of the one-letter
    794 abbreviation for the command, it shows the command's keybinding.
    795 By default, this is the same except for the \"pick\" command."
    796   (let ((inhibit-read-only t))
    797     (save-excursion
    798       (goto-char (point-min))
    799       (when (and git-rebase-show-instructions
    800                  (re-search-forward
    801                   (concat git-rebase-comment-re "\\s-+p, pick")
    802                   nil t))
    803         (goto-char (line-beginning-position))
    804         (pcase-dolist (`(,cmd . ,desc) git-rebase-command-descriptions)
    805           (insert (format (propertize "%s %s %s\n"
    806                                       'font-lock-face 'font-lock-comment-face)
    807                           comment-start
    808                           (string-pad
    809                            (substitute-command-keys (format "\\[%s]" cmd)) 8)
    810                           desc)))
    811         (while (re-search-forward
    812                 (concat git-rebase-comment-re "\\(?:"
    813                         "\\( \\.?     *\\)\\|"
    814                         "\\( +\\)\\([^\n,],\\) \\([^\n ]+\\) \\)")
    815                 nil t)
    816           (if (match-string 1)
    817               (replace-match (make-string 10 ?\s) t t nil 1)
    818             (let ((cmd (intern (concat "git-rebase-" (match-string 4)))))
    819               (if (not (fboundp cmd))
    820                   (delete-region (line-beginning-position)
    821                                  (1+ (line-end-position)))
    822                 (add-text-properties (line-beginning-position)
    823                                      (1+ (line-end-position))
    824                                      '(font-lock-face font-lock-comment-face))
    825                 (replace-match " " t t nil 2)
    826                 (replace-match
    827                  (string-pad
    828                   (save-match-data
    829                     (substitute-command-keys (format "\\[%s]" cmd)))
    830                   8)
    831                  t t nil 3)))))))))
    832 
    833 (add-hook 'git-rebase-mode-hook #'git-rebase-mode-show-keybindings t)
    834 
    835 (defun git-rebase-mode-disable-before-save-hook ()
    836   (setq-local before-save-hook nil))
    837 
    838 (add-hook 'git-rebase-mode-hook #'git-rebase-mode-disable-before-save-hook)
    839 
    840 ;;;###autoload
    841 (defconst git-rebase-filename-regexp "/git-rebase-todo\\'")
    842 ;;;###autoload
    843 (add-to-list 'auto-mode-alist
    844              (cons git-rebase-filename-regexp #'git-rebase-mode))
    845 
    846 (add-to-list 'with-editor-server-window-alist
    847              (cons git-rebase-filename-regexp #'switch-to-buffer))
    848 
    849 (with-eval-after-load 'recentf
    850   (add-to-list 'recentf-exclude git-rebase-filename-regexp))
    851 
    852 (add-to-list 'with-editor-file-name-history-exclude git-rebase-filename-regexp)
    853 
    854 ;;; Imenu Support
    855 
    856 (defun magit-imenu--rebase-prev-index-position-function ()
    857   "Move point to previous commit in git-rebase buffer.
    858 Used as a value for `imenu-prev-index-position-function'."
    859   (catch 'found
    860     (while (not (bobp))
    861       (git-rebase-backward-line)
    862       (when (git-rebase-line-p)
    863         (throw 'found t)))))
    864 
    865 (defun magit-imenu--rebase-extract-index-name-function ()
    866   "Return imenu name for line at point.
    867 Point should be at the beginning of the line.  This function
    868 is used as a value for `imenu-extract-index-name-function'."
    869   (buffer-substring-no-properties (line-beginning-position)
    870                                   (line-end-position)))
    871 
    872 ;;; _
    873 (provide 'git-rebase)
    874 ;;; git-rebase.el ends here