394 lines
13 KiB
EmacsLisp
394 lines
13 KiB
EmacsLisp
;;; git-rebase-mode.el --- Major mode for editing git rebase files
|
|
|
|
;; Copyright (C) 2010-2015 The Magit Project Developers
|
|
|
|
;; Author: Phil Jackson <phil@shellarchive.co.uk>
|
|
;; Maintainer: Jonas Bernoulli <jonas@bernoul.li>
|
|
;; Homepage: https://github.com/magit/git-modes
|
|
;; Keywords: convenience vc git
|
|
;; Package-Version: 1.0.0
|
|
|
|
;; This file is not part of GNU Emacs.
|
|
|
|
;; This file 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, or (at your option)
|
|
;; any later version.
|
|
|
|
;; This file 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 this file. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
;;; Commentary:
|
|
|
|
;; Allows the editing of a git rebase file (which you might get when
|
|
;; using 'git rebase -i' or hitting 'E' in Magit). Assumes editing is
|
|
;; happening in a server.
|
|
|
|
;;; Code:
|
|
|
|
(require 'easymenu)
|
|
(require 'server)
|
|
(require 'thingatpt)
|
|
|
|
;;; Options
|
|
;;;; Variables
|
|
|
|
(defgroup git-rebase nil
|
|
"Edit Git rebase sequences."
|
|
:group 'tools)
|
|
|
|
(defcustom git-rebase-auto-advance nil
|
|
"If non-nil, moves point forward a line after running an action."
|
|
:group 'git-rebase
|
|
:type 'boolean)
|
|
|
|
(defcustom git-rebase-remove-instructions nil
|
|
"Whether to remove the instructions from the rebase buffer.
|
|
Because you have seen them before and can still remember."
|
|
: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
|
|
'((((class color) (background light))
|
|
:foreground "firebrick")
|
|
(((class color) (background dark))
|
|
:foreground "tomato"))
|
|
"Face for commit hashes."
|
|
:group 'git-rebase-faces)
|
|
|
|
(defface git-rebase-description nil
|
|
"Face for commit descriptions."
|
|
:group 'git-rebase-faces)
|
|
|
|
(defface git-rebase-killed-action
|
|
'((((class color))
|
|
:inherit font-lock-comment-face
|
|
:strike-through t))
|
|
"Face for commented action and exec lines."
|
|
:group 'git-rebase-faces)
|
|
|
|
(define-obsolete-face-alias 'git-rebase-description-face
|
|
'git-rebase-description "1.0.0")
|
|
(define-obsolete-face-alias 'git-rebase-killed-action-face
|
|
'git-rebase-killed-action "1.0.0")
|
|
|
|
;;; Regexps
|
|
|
|
(defconst git-rebase-action-line-re
|
|
(concat "^#?"
|
|
"\\([efprs]\\|pick\\|reword\\|edit\\|squash\\|fixup\\) "
|
|
"\\([a-z0-9]\\{4,40\\}\\) "
|
|
"\\(.*\\)")
|
|
"Regexp matching action lines in rebase buffers.")
|
|
|
|
(defconst git-rebase-exec-line-re
|
|
"^#?\\(x\\|exec\\)[[:space:]]\\(.*\\)"
|
|
"Regexp matching exec lines in rebase buffer.")
|
|
|
|
(defconst git-rebase-dead-line-re
|
|
(format "^#\\(?:%s\\|%s\\)"
|
|
(substring git-rebase-action-line-re 1)
|
|
(substring git-rebase-exec-line-re 1))
|
|
"Regexp matching commented action and exex lines in rebase buffers.")
|
|
|
|
;;; Keymaps
|
|
|
|
(defvar git-rebase-mode-map
|
|
(let ((map (make-sparse-keymap)))
|
|
(set-keymap-parent map special-mode-map)
|
|
(define-key map (kbd "q") 'git-rebase-server-edit)
|
|
(define-key map (kbd "C-c C-c") 'git-rebase-server-edit)
|
|
(define-key map (kbd "a") 'git-rebase-abort)
|
|
(define-key map (kbd "C-c C-k") 'git-rebase-abort)
|
|
(define-key map [remap undo] 'git-rebase-undo)
|
|
(define-key map (kbd "RET") 'git-rebase-show-commit)
|
|
(define-key map (kbd "x") 'git-rebase-exec)
|
|
(define-key map (kbd "c") 'git-rebase-pick)
|
|
(define-key map (kbd "r") 'git-rebase-reword)
|
|
(define-key map (kbd "e") 'git-rebase-edit)
|
|
(define-key map (kbd "s") 'git-rebase-squash)
|
|
(define-key map (kbd "f") 'git-rebase-fixup)
|
|
(define-key map (kbd "y") 'git-rebase-insert)
|
|
(define-key map (kbd "k") 'git-rebase-kill-line)
|
|
(define-key map (kbd "C-k") 'git-rebase-kill-line)
|
|
(define-key map (kbd "p") 'git-rebase-backward-line)
|
|
(define-key map (kbd "n") 'forward-line)
|
|
(define-key map (kbd "M-p") 'git-rebase-move-line-up)
|
|
(define-key map (kbd "M-n") 'git-rebase-move-line-down)
|
|
(define-key map (kbd "M-<up>") 'git-rebase-move-line-up)
|
|
(define-key map (kbd "M-<down>") 'git-rebase-move-line-down)
|
|
map)
|
|
"Keymap for Git-Rebase mode.")
|
|
|
|
(easy-menu-define git-rebase-mode-menu git-rebase-mode-map
|
|
"Git-Rebase mode menu"
|
|
'("Rebase"
|
|
["Pick" git-rebase-pick 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]
|
|
["Move Down" git-rebase-move-line-down t]
|
|
["Move Up" git-rebase-move-line-up t]
|
|
["Execute" git-rebase-exec t]
|
|
"---"
|
|
["Abort" git-rebase-abort t]
|
|
["Done" git-rebase-server-edit t]))
|
|
|
|
;;; Utilities
|
|
|
|
(defun git-rebase-edit-line (change-to)
|
|
(when (git-rebase-looking-at-action)
|
|
(let ((buffer-read-only nil)
|
|
(start (point)))
|
|
(goto-char (point-at-bol))
|
|
(delete-region (point) (progn (forward-word 1) (point)))
|
|
(insert change-to)
|
|
(goto-char start)
|
|
(when git-rebase-auto-advance
|
|
(forward-line)))))
|
|
|
|
(defmacro git-rebase-define-action (sym)
|
|
(declare (indent defun))
|
|
(let ((fn (intern (format "git-rebase-%s" sym))))
|
|
`(progn
|
|
(defun ,fn ()
|
|
(interactive)
|
|
(git-rebase-edit-line ,(symbol-name sym)))
|
|
(put ',fn 'definition-name ',sym))))
|
|
|
|
(defun git-rebase-looking-at-action ()
|
|
"Return non-nil if looking at an action line."
|
|
(save-excursion
|
|
(goto-char (point-at-bol))
|
|
(looking-at git-rebase-action-line-re)))
|
|
|
|
(defun git-rebase-looking-at-action-or-exec ()
|
|
"Return non-nil if looking at an action line or exec line."
|
|
(save-excursion
|
|
(goto-char (point-at-bol))
|
|
(or (looking-at git-rebase-action-line-re)
|
|
(looking-at git-rebase-exec-line-re))))
|
|
|
|
(defun git-rebase-looking-at-exec ()
|
|
"Return non-nil if cursor is on an exec line."
|
|
(string-match git-rebase-exec-line-re (thing-at-point 'line)))
|
|
|
|
(defun git-rebase-looking-at-killed-exec ()
|
|
"Return non-nil if looking at an exec line that has been commented out."
|
|
(let ((line (thing-at-point 'line)))
|
|
(and (eq (aref line 0) ?#)
|
|
(string-match git-rebase-exec-line-re line))))
|
|
|
|
;;; Commands
|
|
|
|
(git-rebase-define-action pick)
|
|
(git-rebase-define-action reword)
|
|
(git-rebase-define-action edit)
|
|
(git-rebase-define-action squash)
|
|
(git-rebase-define-action fixup)
|
|
|
|
(defun git-rebase-move-line-up ()
|
|
"Move the current action line up."
|
|
(interactive)
|
|
(when (git-rebase-looking-at-action-or-exec)
|
|
(let ((buffer-read-only nil)
|
|
(col (current-column)))
|
|
(goto-char (point-at-bol))
|
|
(unless (bobp)
|
|
(transpose-lines 1)
|
|
(forward-line -2))
|
|
(move-to-column col))))
|
|
|
|
(defun git-rebase-move-line-down ()
|
|
"Assuming the next line is also an action line, move the current line down."
|
|
(interactive)
|
|
;; if we're on an action and the next line is also an action
|
|
(when (and (git-rebase-looking-at-action-or-exec)
|
|
(save-excursion
|
|
(forward-line)
|
|
(git-rebase-looking-at-action-or-exec)))
|
|
(let ((buffer-read-only nil)
|
|
(col (current-column)))
|
|
(forward-line 1)
|
|
(transpose-lines 1)
|
|
(forward-line -1)
|
|
(move-to-column col))))
|
|
|
|
(defun git-rebase-server-edit ()
|
|
"Save the action buffer and end the session."
|
|
(interactive)
|
|
(save-buffer)
|
|
(server-edit))
|
|
|
|
(defun git-rebase-abort ()
|
|
"Abort this rebase.
|
|
This is dune by emptying the buffer, saving and closing server
|
|
connection."
|
|
(interactive)
|
|
(when (or (not (buffer-modified-p))
|
|
(y-or-n-p "Abort this rebase? "))
|
|
(let ((buffer-read-only nil))
|
|
(erase-buffer)
|
|
(save-buffer)
|
|
(server-edit))))
|
|
|
|
(defun git-rebase-kill-line ()
|
|
"Kill the current action line."
|
|
(interactive)
|
|
(when (and (not (eq (char-after (point-at-bol)) ?#))
|
|
(git-rebase-looking-at-action-or-exec))
|
|
(beginning-of-line)
|
|
(let ((inhibit-read-only t))
|
|
(insert "#"))
|
|
(forward-line)))
|
|
|
|
(defun git-rebase-insert (rev)
|
|
"Read an arbitrary commit and insert it below current line."
|
|
(interactive
|
|
(list (if (fboundp 'magit-read-branch-or-commit)
|
|
(magit-read-branch-or-commit "Insert revision")
|
|
(read-string "Insert revision: "))))
|
|
(forward-line)
|
|
(let ((summary (if (fboundp 'magit-rev-format)
|
|
(magit-rev-format "%h %s" rev)
|
|
(process-lines "git" "show" "-s" "--format=%h %s" rev))))
|
|
(if summary
|
|
(let ((inhibit-read-only t))
|
|
(insert "pick " summary ?\n))
|
|
(user-error "Unknown revision"))))
|
|
|
|
(defun git-rebase-exec (edit)
|
|
"Prompt the user for a shell command to be executed, and
|
|
add it to the todo list.
|
|
|
|
If the cursor is on a commented-out exec line, uncomment the
|
|
current line instead of prompting.
|
|
|
|
When the prefix argument EDIT is non-nil and the cursor is on an
|
|
exec line, edit that line instead of inserting a new one. If the
|
|
exec line was commented out, also uncomment it."
|
|
(interactive "P")
|
|
(cond
|
|
((and edit (git-rebase-looking-at-exec))
|
|
(let ((new-line (git-rebase-read-exec-line
|
|
(match-string-no-properties 2 (thing-at-point 'line))))
|
|
(inhibit-read-only t))
|
|
(delete-region (point-at-bol) (point-at-eol))
|
|
(if (not (equal "" new-line))
|
|
(insert "exec " new-line)
|
|
(delete-char -1)
|
|
(forward-line))
|
|
(move-beginning-of-line nil)))
|
|
((git-rebase-looking-at-killed-exec)
|
|
(save-excursion
|
|
(beginning-of-line)
|
|
(let ((buffer-read-only nil))
|
|
(delete-char 1))))
|
|
(t
|
|
(let ((inhibit-read-only t)
|
|
(line (git-rebase-read-exec-line)))
|
|
(unless (equal "" line)
|
|
(move-end-of-line nil)
|
|
(newline)
|
|
(insert (concat "exec " line))))
|
|
(move-beginning-of-line nil))))
|
|
|
|
(defun git-rebase-read-exec-line (&optional initial-line)
|
|
(read-shell-command "Execute: " initial-line))
|
|
|
|
(defun git-rebase-undo (&optional arg)
|
|
"A thin wrapper around `undo', which allows undoing in read-only buffers."
|
|
(interactive "P")
|
|
(let ((inhibit-read-only t))
|
|
(undo arg)))
|
|
|
|
(defun git-rebase-show-commit (&optional arg)
|
|
"Show the commit on the current line if any."
|
|
(interactive "P")
|
|
(save-excursion
|
|
(goto-char (point-at-bol))
|
|
(when (looking-at git-rebase-action-line-re)
|
|
(let ((commit (match-string 2)))
|
|
(if (fboundp 'magit-show-commit)
|
|
(magit-show-commit commit)
|
|
(shell-command (concat "git show " commit)))))))
|
|
|
|
(defun git-rebase-backward-line (&optional n)
|
|
"Move N lines backward (forward if N is negative).
|
|
Like `forward-line' but go into the opposite direction."
|
|
(interactive "p")
|
|
(forward-line (* n -1)))
|
|
|
|
;;; Mode
|
|
|
|
;;;###autoload
|
|
(define-derived-mode git-rebase-mode special-mode "Git Rebase"
|
|
"Major mode for editing of a Git rebase file.
|
|
|
|
Rebase files are generated when you run 'git rebase -i' or run
|
|
`magit-interactive-rebase'. They describe how Git should perform
|
|
the rebase. See the documentation for git-rebase (e.g., by
|
|
running 'man git-rebase' at the command line) for details."
|
|
(setq font-lock-defaults '(git-rebase-mode-font-lock-keywords t t))
|
|
(when git-rebase-remove-instructions
|
|
(let ((inhibit-read-only t))
|
|
(flush-lines "^\\($\\|#\\)"))))
|
|
|
|
(defvar git-rebase-mode-font-lock-keywords
|
|
`((,git-rebase-action-line-re
|
|
(1 font-lock-keyword-face)
|
|
(2 'git-rebase-hash)
|
|
(3 'git-rebase-description))
|
|
(,git-rebase-exec-line-re 1 font-lock-keyword-face)
|
|
("^#.*" 0 font-lock-comment-face)
|
|
(,git-rebase-dead-line-re 0 'git-rebase-killed-action t))
|
|
"Font lock keywords for Git-Rebase mode.")
|
|
|
|
(defun git-rebase-mode-show-keybindings ()
|
|
"Modify the \"Commands:\" section of the comment Git generates
|
|
at the bottom of the file so that in place of the one-letter
|
|
abbreviation for the command, it shows the command's keybinding.
|
|
By default, this is the same except for the \"pick\" command."
|
|
(save-excursion
|
|
(goto-char (point-min))
|
|
(while (search-forward-regexp "^# \\(.\\), \\([[:alpha:]]+\\) = " nil t)
|
|
(let ((start (match-beginning 1))
|
|
(end (match-end 1))
|
|
(command (intern (concat "git-rebase-" (match-string 2)))))
|
|
(when (fboundp command)
|
|
(let ((overlay (make-overlay start end)))
|
|
(overlay-put
|
|
overlay 'display
|
|
(key-description (where-is-internal command nil t)))))))))
|
|
|
|
(add-hook 'git-rebase-mode-hook 'git-rebase-mode-show-keybindings t)
|
|
|
|
(defun git-rebase-mode-disable-before-save-hook ()
|
|
(set (make-local-variable 'before-save-hook) nil))
|
|
|
|
(add-hook 'git-rebase-mode-hook 'git-rebase-mode-disable-before-save-hook)
|
|
|
|
;;;###autoload
|
|
(add-to-list 'auto-mode-alist
|
|
'("/git-rebase-todo\\'" . git-rebase-mode))
|
|
|
|
(provide 'git-rebase-mode)
|
|
;; Local Variables:
|
|
;; indent-tabs-mode: nil
|
|
;; End:
|
|
;;; git-rebase-mode.el ends here
|