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