;;; git-rebase-mode.el --- Major mode for editing git rebase files ;; Copyright (C) 2010-2015 The Magit Project Developers ;; Author: Phil Jackson ;; Maintainer: Jonas Bernoulli ;; 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 . ;;; 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-") 'git-rebase-move-line-up) (define-key map (kbd "M-") '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