Make Emacs write (part of) your git commit messages
I was fed up with the chores of writing consistent git commit messages, so a while ago I started developing a hook in Emacs which I used with Magit (actually git-commit-mode
) which uses some crude heuristics to fill out the COMMIT_EDITMSG
buffer for me. Here is what it does (|
stands for the cursor):
-
If only a single file modified, insert
<filename>: |
- If can figure out function name, insert
<filename> (<functionname>): |
- If can figure out function name, insert
-
If only a single file added, insert
Add <filename>|
-
If a
TODO
added toReadme.org
, insert; TODO <headline>|
-
If a
TODO
wasDONE
, insert; DONE <headline>|
-
If the files are
Readme.org
andReadme.org_archive
, and no new TODO's were added anywhere, insert; Archive DONE|
-
If the file is
.gitignore
, insert; Ignore |
-
If the file is
TAGS
, insert; Update TAGS|
I extend this when I find new cases where I repeatedly do the same thing. The code is below. It's probably a good idea to use it as a starting point and personalise it because this reflects how I like to write my commit messages (and I like pretending how they do it over at Emacs git repo). It is sloppy and probably buggy, but I don't think it can be destructive.
Final note: I can't figure out how to set this up so that after this takes effect, the buffer is marked as modified. I want to flip the modified bit so that in some cases I can just hit C-c C-c
and go. But I need to modify the buffer somehow to commit in some cases (I just type C-o
to open a new line in those cases). Here is the function:
(defun gk-git-commit-mode-hook ()
"Set up git commit buffer."
;; If a single file is modified, prefix the message w/ it.
(let ((modified-re "^# modified:")
(new-re "^# new file:")
(issue-re "^[+\\- ]\\*+ \\(TODO\\|DONE\\) ")
current-defun filename addp onlyp issuep)
(save-excursion
(with-current-buffer "COMMIT_EDITMSG"
(goto-char (point-min))
(re-search-forward "^# Changes to be committed:" nil t)
(forward-line)
(beginning-of-line)
(cond ((looking-at modified-re)
(re-search-forward ": " nil t)
(setf filename (thing-at-point 'filename t)))
((looking-at new-re)
(re-search-forward ": " nil t)
(setf filename (thing-at-point 'filename t)
addp t)))
(setq onlyp (progn
(forward-line)
(not (or (looking-at modified-re)
(looking-at new-re)))))
(when (and onlyp (equal filename "Readme.org"))
(goto-char (point-min))
(when-let* ((pos (re-search-forward issue-re nil t)))
(setq issuep (progn
(re-search-backward "\\*" nil t)
(buffer-substring (1+ (point))
(line-end-position))))))
;; Try to set ‘current-defun’.
(when onlyp
(save-excursion
(goto-char (point-min))
;; Error if not found, means verbose diffs
;; not enabled.
(re-search-forward "^diff --git")
(goto-char (line-beginning-position))
(let ((str (buffer-substring (point) (point-max)))
(default-directory (expand-file-name "..")))
(with-temp-buffer
(insert str)
(diff-mode)
(goto-char (point-min))
(setq current-defun (diff-current-defun))))))))
(if onlyp
(cond
((and issuep (not addp))
(goto-char (point-min))
(insert ";" issuep))
((equal filename "TAGS")
(goto-char (point-min))
(insert "; Update TAGS"))
((equal filename ".gitignore")
(goto-char (point-min))
(insert "; Ignore "))
(filename
(goto-char (point-min))
(if addp
(insert "Add " filename)
(insert
filename
(if (and current-defun)
(format " (%s)" current-defun)
"")
": "))))
(when (and (equal filename "Readme.org")
(save-excursion
(goto-char (point-min))
(re-search-forward (concat modified-re " +Readme.org_archive")
nil t))
(save-excursion
(goto-char (point-min))
(re-search-forward "\\-\\*+ DONE" nil t))
(not
(save-excursion
(goto-char (point-min))
(re-search-forward "\\+\\*[\\+\\-] TODO" nil t))))
(goto-char (point-min))
(insert "; Archive DONE")))))
(add-hook 'git-commit-mode-hook #'gk-git-commit-mode-hook)
Hope you find it useful.