using emacs to promote git health


Having a well structured commit history in a feature branch makes development more efficient. Sensibly named and placed markers allow you to quickly revisit points in the change history. The trouble is getting in the habit of methodically making commits throughout. Unlike saving, which is an action required immediately for your changes to take effect, making a commit is only useful in the eventuality that a future change breaks everything and you need to revert to a stable state. I’ve forced myself in to this habit by extending my favorite text editor to prompt me to commit when saving.

Advising Emacs functions

The behavior of existing functions can be modified in emacs by using the advice feature. In this case I’d like to tack on a commit prompt to the the “save-some-buffers” function. As an aside, I’ve assumed that the reader is somewhat familiar with emacs lisp when structuring this post (helpful links at the bottom to add to your compendium of esoterica).

 (advice-add 'save-some-buffers :around #'prompt-git-commit-on-save)

This line causes the function “prompt-git-commit-on-save” to be called in place of the “save-some-buffers” function (with the original function and original args as arguments). There are other symbols, like :before and :after, that could be used to combine the original and new functions in different ways, but, in this case, I require :around, because I’d like to execute some code before and after the the original function call.

(defun prompt-git-commit-on-save (orig-fun &rest args)
  (let ((modified-buffers (build-modified-buffer-list (buffer-list))))
    (apply orig-fun args)
    (dolist (repo (get-modified-repos modified-buffers))
      (message (add-changes repo))
      (message (make-commit
		repo (get-commit-msg repo)))


This is the decorating function. As you can see it requires “orig-fun” as an argument and will optionally take “args” (whatever the original arguments were). Here’s the breakdown: determine which buffers have been modified and let “modified-buffers” hold that value, execute the original function, and, finally, add the changes made and prompt for a commit for each repository that had a buffer saved to it.


The details

In this section I’ll explain the functionality and reasoning behind each of the component functions of the force-git extension.

(defun build-modified-buffer-list (current-buffers)
   ;; end recursion test
   ((not current-buffers) nil)

   ;; exists and modified?
   ((and (buffer-file-name (car current-buffers))
	 (buffer-modified-p (car current-buffers)))
    (cons (car current-buffers)
	  (build-modified-buffer-list (cdr current-buffers))))

   ;; fails test, recurse
   (t (build-modified-buffer-list (cdr current-buffers)))


This function is used both before and after the execution of the original function so that we can determine which buffers were actually saved. It takes as an argument a list of the current buffers (which is available via the emacs function “buffer-list”). The function checks that the buffer has a file associated with it using “buffer-file-name” (filtering out default buffers like *scratch* and *Messages*) and that it has been modified using “buffer-modified-p”. If the buffer passes these checks, we construct a new list of that buffer and the result of a recursive call to “build-modified-buffer-list”. Otherwise, just recurse.

(defun build-git-repo-list (modified-buffers)
   ;; end recursion test
   ((not modified-buffers) nil)

   ;; is git repo
   ((vc-git-root (buffer-file-name (car modified-buffers)))
      (buffer-file-name (car modified-buffers)))
      (cdr modified-buffers))))

   ;; fails test, recurse
   (t (build-git-repo-list (cdr modified-buffers)))


The “build-git-repo-list” uses same “keep” recursion pattern as used above to look through a list of buffers and return a list of their associated repos. The built-in “vc-git-root” is the meat of the function; returning the root of the repo if the file is in version control, otherwise nil. This builds a non-unique list, so the result must be de-duped.

(defun get-modified-repos (orig-modified-buffers)
    (cl-set-difference modified-buffers
		       (build-modified-buffer-list (buffer-list)))


This function orchestrates determining which files were saved and building the repo list from these saved buffers. The set difference between “modified-buffers” (which we got before calling save-some-buffers) and the the buffers currently in the “modified” state gives us all of the buffers that were saved. Of the files that are saved, we determine a list of their associated git repos and de-dupe it.

(defun add-changes (repo)
   (let ((repo-path (replace-regexp-in-string "~" "${HOME}" repo)))
     (format "git --git-dir=%s.git --work-tree=%s add -u"
	     repo-path repo-path))


(defun get-commit-msg (repo)
    (format "git --git-dir=%s.git rev-parse --abbrev-ref HEAD"
	    (replace-regexp-in-string "~" "${HOME}" repo))


(defun make-commit (repo msg)
  (if (not (equal msg ""))
       (format "git --git-dir=%s.git commit -m '%s'"
	       (replace-regexp-in-string "~" "${HOME}" repo)
    (format "message blank: no commit made to %s" repo)


The function “shell-command-to-string” is used execute git commands in these three functions; adding the changes, getting the current branch, and making a commit. Conveniently, the “shell-command-to-string” returns the result of the shell command as a string, which is propagated to the *Messages* buffer using the “message” function in the main function described at the top of the post. The built-in of note here is “read-from-minibuffer”. This prints whatever is passed as an argument and waits for user input. In this case, we prompt with the name of the branch in the repo to which a commit is going to be made.


M-x apropos
Introduction to emacs lisp
emacs advice documentation
recursion patterns
check out the full source for force-git here

you like emacs too? Join us!

Tags: ,


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s