Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
229 lines (201 sloc) 7.95 KB
;;; gitsum.el --- basic darcsum feelalike for Git
;; Copyright (C) 2008 Christian Neukirchen <>
;; Licensed under the same terms as Emacs.
;; Repository:
;; git://
;; Patches to:
;; Version: 0.2
;; 04feb2008 +chris+
(eval-when-compile (require 'cl))
(defcustom gitsum-reuse-buffer t
"Whether `gitsum' should try to reuse an existing buffer
if there is already one that displays the same directory."
:group 'git
:type 'boolean)
(defun utf8-shell-command-on-region (&rest args)
(let ((coding-system-for-read 'utf-8-unix)
(coding-system-for-write 'utf-8-unix))
(apply 'shell-command-on-region args)))
(easy-mmode-defmap gitsum-diff-mode-shared-map
'(("A" . gitsum-amend)
("c" . gitsum-commit)
("g" . gitsum-refresh)
("k" . gitsum-kill-dwim)
("P" . gitsum-push)
("R" . gitsum-revert)
("s" . gitsum-switch-to-git-status)
("q" . gitsum-kill-buffer)
("u" . gitsum-undo))
"Basic keymap for `gitsum-diff-mode', bound to various prefix keys.")
(define-derived-mode gitsum-diff-mode diff-mode "gitsum"
"Git summary mode is for preparing patches to a Git repository.
This mode is meant to be activated by `M-x gitsum' or pressing `s' in git-status.
;; magic...
(lexical-let ((ro-bind (cons 'buffer-read-only gitsum-diff-mode-shared-map)))
(add-to-list 'minor-mode-overriding-map-alist ro-bind))
(if (functionp 'diff-auto-refine-mode)
(diff-auto-refine-mode nil))
(setq buffer-read-only t))
(define-key gitsum-diff-mode-map (kbd "C-c C-c") 'gitsum-commit)
(define-key gitsum-diff-mode-map (kbd "C-/") 'gitsum-undo)
(define-key gitsum-diff-mode-map (kbd "C-_") 'gitsum-undo)
;; When git.el is loaded, hack into keymap.
(when (boundp 'git-status-mode-map)
(define-key git-status-mode-map "s" 'gitsum-switch-from-git-status))
;; Undo doesn't work in read-only buffers else.
(defun gitsum-undo ()
"Undo some previous changes.
Repeat this command to undo more changes.
A numeric argument serves as a repeat count."
(let ((inhibit-read-only t))
(defun gitsum-refresh (&optional arguments)
"Regenerate the patch based on the current state of the index."
(let ((inhibit-read-only t))
(insert "# Directory: " default-directory "\n")
(insert "# Use n and p to navigate and k to kill a hunk. u is undo, g will refresh.\n")
(insert "# Edit the patch as you please and press 'c' to commit.\n\n")
(let ((diff (shell-command-to-string (concat "git diff " arguments))))
(if (zerop (length diff))
(insert "## No changes. ##")
(insert diff)
(goto-char (point-min))
(delete-matching-lines "^index \\|^diff --git ")))
(set-buffer-modified-p nil)
(goto-char (point-min))
(forward-line 4)))
(defun gitsum-kill-dwim ()
"Kill the current hunk or file depending on point."
(let ((inhibit-read-only t))
(if (looking-at "^---\\|^\\+\\+\\+")
(when (or (looking-at "^--- ")
(let ((here (point)))
(forward-line -2)
(when (looking-at "^--- ")
(delete-region here (point)))))))))
(defun gitsum-commit ()
"Commit the patch as-is, asking for a commit message."
(when (zerop (utf8-shell-command-on-region (point-min) (point-max)
"git apply --check --cached"))
(let ((buffer (get-buffer-create "*gitsum-commit*"))
(dir default-directory))
(utf8-shell-command-on-region (point-min) (point-max) "(cat; git diff --cached) | git apply --stat" buffer)
(with-current-buffer buffer
(setq default-directory dir)
(goto-char (point-min))
(insert "\n")
(while (re-search-forward "^" nil t)
(replace-match "# " nil nil))
(forward-line 0)
(forward-char -1)
(delete-region (point) (point-max))
(goto-char (point-min)))
(log-edit 'gitsum-do-commit nil nil buffer))))
(defun gitsum-amend ()
"Amend the last commit."
(let ((last (substring (shell-command-to-string
"git log -1 --pretty=oneline --abbrev-commit")
0 -1)))
(when (y-or-n-p (concat "Are you sure you want to amend to " last "? "))
(utf8-shell-command-on-region (point-min) (point-max) "git apply --cached")
(shell-command "git commit --amend -C HEAD")
(defun gitsum-push ()
"Push the current repository."
(let ((args (read-string "Shell command: " "git push ")))
(let ((buffer (get-buffer-create " *gitsum-push*")))
(switch-to-buffer buffer)
(insert "Running " args "...\n\n")
(start-process-shell-command "gitsum-push" buffer args))))
(defun gitsum-revert ()
"Revert the active patches in the working directory."
(let ((count (count-matches "^@@" (point-min) (point-max))))
(if (not (yes-or-no-p
(format "Are you sure you want to revert these %d hunk(s)? "
(message "Revert canceled.")
(utf8-shell-command-on-region (point-min) (point-max) "git apply --reverse")
(defun gitsum-do-commit ()
"Perform the actual commit using the current buffer as log message."
(with-current-buffer log-edit-parent-buffer
(utf8-shell-command-on-region (point-min) (point-max)
"git apply --cached"))
(utf8-shell-command-on-region (point-min) (point-max)
"git commit -F- --cleanup=strip")
(with-current-buffer log-edit-parent-buffer
(defun gitsum-kill-buffer ()
"Kill the current buffer if it has no manual changes."
(if (buffer-modified-p)
(message "Patch was modified, use C-x k to kill.")
(kill-buffer nil)))
(defun gitsum-switch-to-git-status ()
"Switch to git-status."
(git-status default-directory))
(defun gitsum-switch-from-git-status ()
"Switch to gitsum, resticting diff to marked files if any."
(let ((marked (git-get-filenames
(ewoc-collect git-status
(lambda (info) (git-fileinfo->marked info))))))
(when marked
(gitsum-refresh (mapconcat 'identity marked " ")))))
(defun gitsum-find-buffer (dir)
"Find the gitsum buffer handling a specified directory."
(let ((list (buffer-list))
(fulldir (expand-file-name dir))
(while (and list (not found))
(let ((buffer (car list)))
(with-current-buffer buffer
(when (and list-buffers-directory
(string-equal fulldir
(expand-file-name list-buffers-directory))
(eq major-mode 'gitsum-diff-mode))
(setq found buffer))))
(setq list (cdr list)))
(defun gitsum-get-top-dir ()
"Get the top directory of your Git repository."
(unless (zerop (prog1
(call-process "git" nil t nil "rev-parse" "--git-dir")
(kill-backward-chars 1)))
(error (buffer-string)))
;; --git-dir returns the absolute path to .git. We want the
;; directory above that (hopefully, maybe).
(expand-file-name ".." (buffer-string))))
(defun gitsum ()
"Entry point into gitsum-diff-mode."
(let* ((dir (gitsum-get-top-dir))
(buffer (or (and gitsum-reuse-buffer (gitsum-find-buffer dir))
(generate-new-buffer "*gitsum*"))))
(switch-to-buffer buffer)
(cd-absolute dir)
;; viper compatible
(eval-after-load "viper"
'(add-to-list 'viper-emacs-state-mode-list 'gitsum-diff-mode))
(provide 'gitsum)