;;; git-rebase.el --- Edit Git rebase files -*- lexical-binding:t -*- ;; Copyright (C) 2008-2025 The Magit Project Contributors ;; Author: Phil Jackson ;; Maintainer: Jonas Bernoulli ;; SPDX-License-Identifier: GPL-3.0-or-later ;; Magit is free software: you can redistribute it and/or modify it ;; under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; ;; Magit is distributed in the hope that it will be useful, but WITHOUT ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public ;; License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with Magit. If not, see . ;;; Commentary: ;; This package assists the user in editing the list of commits to be ;; rewritten during an interactive rebase. ;; When the user initiates an interactive rebase, e.g., using "r e" in ;; a Magit buffer or on the command line using "git rebase -i REV", ;; Git invokes the `$GIT_SEQUENCE_EDITOR' (or if that is undefined ;; `$GIT_EDITOR' or even `$EDITOR') letting the user rearrange, drop, ;; reword, edit, and squash commits. ;; This package provides the major-mode `git-rebase-mode' which makes ;; doing so much more fun, by making the buffer more colorful and ;; providing the following commands: ;; ;; C-c C-c Tell Git to make it happen. ;; C-c C-k Tell Git that you changed your mind, i.e., abort. ;; ;; p Move point to previous line. ;; n Move point to next line. ;; ;; M-p Move the commit at point up. ;; M-n Move the commit at point down. ;; ;; d Drop the commit at point. ;; c Keep the commit at point. ;; r Change the message of the commit at point. ;; e Edit the commit at point. ;; s Squash the commit at point, into the one above. ;; f Like "s" but don't also edit the commit message. ;; b Break for editing at this point in the sequence. ;; x Add a script to be run with the commit at point ;; being checked out. ;; k Un-/comment current line. ;; z Add noop action at point. ;; ;; SPC Show the commit at point in another buffer. ;; RET Show the commit at point in another buffer and ;; select its window. ;; C-/ Undo last change. ;; ;; Commands for --rebase-merges: ;; l Associate label with current HEAD in sequence. ;; MM Merge specified revisions into HEAD. ;; Mt Toggle whether the merge will invoke an editor ;; before committing. ;; t Reset HEAD to the specified label. ;; You should probably also read the `git-rebase' manpage. ;;; Code: (require 'magit) (require 'easymenu) (require 'server) (require 'with-editor) (defvar recentf-exclude) ;;; Options ;;;; Variables (defgroup git-rebase nil "Edit Git rebase sequences." :link '(info-link "(magit)Editing Rebase Sequences") :group 'tools) (defcustom git-rebase-auto-advance t "Whether to move to next line after changing a line." :group 'git-rebase :type 'boolean) (defcustom git-rebase-show-instructions t "Whether to show usage instructions inside the rebase buffer." :group 'git-rebase :type 'boolean) (defcustom git-rebase-confirm-cancel t "Whether confirmation is required to cancel." :group 'git-rebase :type 'boolean) ;;;; Faces (defgroup git-rebase-faces nil "Faces used by Git-Rebase mode." :group 'faces :group 'git-rebase) (defface git-rebase-hash '((t :inherit magit-hash)) "Face for commit hashes." :group 'git-rebase-faces) (defface git-rebase-label '((t :inherit magit-refname)) "Face for labels in label, merge, and reset lines." :group 'git-rebase-faces) (defface git-rebase-description '((t nil)) "Face for commit descriptions." :group 'git-rebase-faces) (defface git-rebase-action '((t :inherit font-lock-keyword-face)) "Face for action keywords." :group 'git-rebase-faces) (defface git-rebase-killed-action '((t :inherit font-lock-comment-face :strike-through t)) "Face for commented commit action lines." :group 'git-rebase-faces) (defface git-rebase-comment-hash '((t :inherit git-rebase-hash :weight bold)) "Face for commit hashes in commit message comments." :group 'git-rebase-faces) (defface git-rebase-comment-heading '((t :inherit font-lock-keyword-face)) "Face for headings in rebase message comments." :group 'git-rebase-faces) ;;; Keymaps (defvar-keymap git-rebase-mode-map :doc "Keymap for Git-Rebase mode." :parent special-mode-map "C-m" #'git-rebase-show-commit "p" #'git-rebase-backward-line "n" #'forward-line "M-p" #'git-rebase-move-line-up "M-n" #'git-rebase-move-line-down "c" #'git-rebase-pick "d" #'git-rebase-drop "k" #'git-rebase-kill-line "C-k" #'git-rebase-kill-line "b" #'git-rebase-break "e" #'git-rebase-edit "l" #'git-rebase-label "M M" #'git-rebase-merge "M t" #'git-rebase-merge-toggle-editmsg "m" #'git-rebase-edit "s" #'git-rebase-squash "S" #'git-rebase-squish "f" #'git-rebase-fixup "F" #'git-rebase-alter "A" #'git-rebase-alter "q" #'undefined "r" #'git-rebase-reword "w" #'git-rebase-reword "t" #'git-rebase-reset "u" #'git-rebase-update-ref "x" #'git-rebase-exec "y" #'git-rebase-insert "z" #'git-rebase-noop "SPC" #'git-rebase-show-or-scroll-up "DEL" #'git-rebase-show-or-scroll-down "C-x C-t" #'git-rebase-move-line-up "M-" #'git-rebase-move-line-up "M-" #'git-rebase-move-line-down " " #'git-rebase-undo) (put 'git-rebase-alter :advertised-binding (kbd "F")) (put 'git-rebase-reword :advertised-binding (kbd "r")) (put 'git-rebase-move-line-up :advertised-binding (kbd "M-p")) (put 'git-rebase-kill-line :advertised-binding (kbd "k")) (easy-menu-define git-rebase-mode-menu git-rebase-mode-map "Git-Rebase mode menu." '("Rebase" ["Pick" git-rebase-pick t] ["Drop" git-rebase-drop t] ["Reword" git-rebase-reword t] ["Edit" git-rebase-edit t] ["Squash" git-rebase-squash t] ["Fixup" git-rebase-fixup t] ["Kill" git-rebase-kill-line t] ["Noop" git-rebase-noop t] ["Execute" git-rebase-exec t] ["Move Down" git-rebase-move-line-down t] ["Move Up" git-rebase-move-line-up t] "---" ["Cancel" with-editor-cancel t] ["Finish" with-editor-finish t])) (defvar git-rebase-command-descriptions '((with-editor-finish . "tell Git to make it happen") (with-editor-cancel . "tell Git that you changed your mind, i.e., abort") (git-rebase-backward-line . "move point to previous line") (forward-line . "move point to next line") (git-rebase-move-line-up . "move the commit at point up") (git-rebase-move-line-down . "move the commit at point down") (git-rebase-show-or-scroll-up . "show the commit at point in another buffer") (git-rebase-show-commit . "show the commit at point in another buffer and select its window") (undo . "undo last change") (git-rebase-drop . "drop the commit at point") (git-rebase-kill-line . "un-/comment current line") (git-rebase-insert . "insert a line for an arbitrary commit") (git-rebase-noop . "add noop action at point"))) (defvar git-rebase-fixup-descriptions '((git-rebase-squish . "fixup -c = use commit, but meld into previous commit,\n#\ dropping previous commit's message, and open the editor") (git-rebase-fixup . "fixup = use commit, but meld into previous commit,\n#\ dropping 's message") (git-rebase-alter . "fixup -C = use commit, but meld into previous commit,\n#\ dropping previous commit's message"))) ;;; Commands (defun git-rebase-pick () "Use commit on current line. If the region is active, act on all lines touched by the region." (interactive) (git-rebase-set-action "pick")) (defun git-rebase-drop () "Drop commit on current line. If the region is active, act on all lines touched by the region." (interactive) (git-rebase-set-action "drop")) (defun git-rebase-reword () "Edit message of commit on current line. If the region is active, act on all lines touched by the region." (interactive) (git-rebase-set-action "reword")) (defun git-rebase-edit () "Stop at the commit on the current line. If the region is active, act on all lines touched by the region." (interactive) (git-rebase-set-action "edit")) (defun git-rebase-squash () "Fold commit on current line into previous commit, edit combined message. If the region is active, act on all lines touched by the region." (interactive) (git-rebase-set-action "squash")) (defun git-rebase-squish () "Fold current into previous commit, discard previous message and edit current. This is like `git-rebase-squash', except that the other message is kept. The action indicatore shown in the list commits is \"fixup -c\". If the region is active, act on all lines touched by the region." (interactive) (git-rebase-set-action "fixup -c")) (defun git-rebase-fixup () "Fold commit on current line into previous commit, discard current message. If the region is active, act on all lines touched by the region." (interactive) (git-rebase-set-action "fixup")) (defun git-rebase-alter () "Meld current into previous commit, discard previous message and use current. This is like `git-rebase-fixup', except that the other message is kept. The action indicatore shown in the list commits is \"fixup -C\". If the region is active, act on all lines touched by the region." (interactive) (git-rebase-set-action "fixup -C")) (defvar-local git-rebase-comment-re nil) (defvar git-rebase-short-options '((?b . "break") (?d . "drop") (?e . "edit") (?f . "fixup") (?l . "label") (?m . "merge") (?p . "pick") (?r . "reword") (?s . "squash") (?t . "reset") (?u . "update-ref") (?x . "exec")) "Alist mapping single key of an action to the full name.") (defclass git-rebase-action () (;; action-type: commit, exec, bare, label, merge (action-type :initarg :action-type :initform nil) ;; Examples for each action type: ;; | action | action options | target | trailer | ;; |--------+----------------+---------+---------| ;; | pick | | hash | subject | ;; | exec | | command | | ;; | noop | | | | ;; | reset | | name | subject | ;; | merge | -C hash | name | subject | (action :initarg :action :initform nil) (action-options :initarg :action-options :initform nil) (target :initarg :target :initform nil) (trailer :initarg :trailer :initform nil) (comment-p :initarg :comment-p :initform nil) (abbrev))) (defvar git-rebase-line-regexps ;; 1: action, 2: option, 3: target, 4: "#", 5: description. ;; ;; [[# ] ] ;; fixup [-C|-c] [[# ] ] `((commit . ,(concat (regexp-opt '("d" "drop" "e" "edit" "f" "fixup" "f -C" "fixup -C" "f -c" "fixup -c" "p" "pick" "r" "reword" "s" "squash") "\\(?1:") " \\(?3:[^ \n]+\\)" "\\(?: \\(?4:# \\)?\\(?5:.*\\)\\)?")) (exec . "\\(?1:x\\|exec\\) \\(?3:.*\\)") (bare . ,(concat (regexp-opt '("b" "break" "noop") "\\(?1:") " *$")) (label . ,(concat (regexp-opt '("l" "label" "t" "reset" "u" "update-ref") "\\(?1:") " \\(?3:[^ \n]+\\)" "\\(?: \\(?4:# \\)?\\(?5:.*\\)\\)?")) ;; merge [-C | -c ]