dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

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