12 votes

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 only a single file added, insert Add <filename>|

  • If a TODO added to Readme.org, insert ; TODO <headline>|

  • If a TODO was DONE, insert ; DONE <headline>|

  • If the files are Readme.org and Readme.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.