Antoine Kalmbach's blog

Working with Git and patches in Emacs

Git was initially designed around emails. Modern Git forges have co-opted the term pull request to mean using web-based applications for collaborating using Git. This has become the de facto method of collaboration in the Git world. Before that happened, people actually used plain emails when working with decentralized version control system. There was no notion of requiring a web application for collaboration, you simply mailed your changes to someone and they merged them if they wished.

This document demonstrates the way I use email and Git using Emacs.

Why Git and email?

While working with Git these days you cannot but notice the mainstream collaboration model is completely unlike what this document details. You are expected to use web applications like Github or its copies (Gitlab, etc., see above). Lots of people have written about the pros and cons of each, to highlight a few:

My own reason for preferring email is that it promotes discussion. Many forges do not actively promote a discussion forum, which is why things like gitter.im exist. Other alternatives is to have an IRC channel, but those are usually too informal to focus on development.

Development forums are essential

With an email-based workflow the place for collaboration supports both discussion and actual development work and review. A project mailing list offers an environment for discussing the project and for sending and reviewing patches.

When patches are mixed with discussion, it makes it easy for people to follow the current zeitgeist of the project. Changes have more context this way. Contrast this to a forge like GitHub where, if the project doesn’t really state anything, you’re suppose to conduct your discussion in issues before making a pull request. But that can lead to confusion: issues usually are synonymous with bug reports. It would be nicer if issues were, well, issues. In an email-based collaboration model, discussion and development happen in the same place, it’s a vibrant bazaar of development.

Email? Seriously?

Ok, email and git isn’t for everybody. That’s fine. The folks at GitHub rightly understood that if one were to make sending pull requests easier it would increase the popularity of Git. The point isn’t in elitism or setting high barriers of entry. I don’t think the point is in filtering beginners either, since I wouldn’t want to be hostile to beginners.

I suppose advocating Git and email in 2020 is in one sense some kind of activism for simpler solutions from a more elegant time. The argument is also interesting from a technological perspective: git send-email is actually a uniform, standard way of collaborating in Git.

As a result, git send-email is actually quite relevant still despite forges having twisted the meaning of pull request. That is why in every forge a pull request is slightly different. Some forges call them merge requests or something similar, and the user experience for making them varies ever so slightly.

There is no standard pull request any more

There is certain nonobvious elegance in the email-based workflow: it actually standardizes on two tools: git send-email and git am. This standardization means that everyone, regardless of their mail client, will be working with a uniform language for collaboration.

Now, many will say that web applications offer a better user experience than emails. That is, first and foremost, a matter of opinion. While web applications offer more graphical bells and whistles, the problem is that in the modern age of a multitude of Git forges every one of them will have slight differences in the language they use and implement pull requests in. So if you regularly collaborate on many platforms the slight idiosyncracies of each forge will get in the way. Not so with git send-email, it’s the same for everyone!

Barriers of entry

Some forges embrace email based collaboration natively: sourcehut is a newish forge from 2018 which is still in alpha as of October 2020, and it actively promotes an email-based workflow. In fact, the people behind sourcehut are behind the tutorial at git-send-email.io.

Even sourcehut recognizes that setting up sendemail in git can be onerous and a lot of people don’t want to work with email. So they built a webpage to handle that part where you can interactively prepare a patch email as if you were using git send-email. It’s still email at the bottom, that’s the cool part!

So as a result, don’t think of git and email as some sort of activism mixed with retrocomputing or text-based interfaces, rather, the goal is to

  • rely on a standard way of collaboration, and
  • natively mix discussion and development in the same environment.

Because git send-email encourages the use of mailing lists, you no longer need an official IRC channel or subreddit or gitter room or whatever. And forges like sourcehut have made it easy to separate patches in the web interface for mailing lists, as well as having CI for mailing lists (seriously, how cool is that?).

Git and email in 5 minutes

Email-based workflows in Git are based on two tools:

  • git send-email for sending changes to someone
  • git am for applying changes from someone

git send-email is essentially a wrapper around git format-patch. This wrapper calls git format-patch and mails it to the email address in the --to parameter. So

$ git send-email --to="foo@bar.com@" HEAD^

This will call git format-patch HEAD^ and send it to foo@bar.com. Git includes a Perl-based email client that it uses. This mail includes your commit message and the diff of the changes. When foo@bar.com wants to apply your patch, they save the mail and use git am on it.

$ git am patch.mbox

Now the question is, how do you use git send-email and git am from Emacs so that you don’t have to exit to the shell?

Sending patches from Emacs

Those daring enough who have configured Emacs as their email client probably think, ok, what’s the big deal, let me just call git format-patch and then mail the patch myself. The workflow would be thus:

  1. Prepare some changes.
  2. Commit the changes.
  3. Use git format-patch to prepare a file foo.patch.
  4. Send foo.patch as an email to the intended recipient, e.g. by opening it and then calling M-x message-mode which would open it in a major mode for sending emails.

While this makes sense, you’ll still have to edit the intended recipient manually, ensure your subject is formatted properly, and so on. For basic one-off patches this might be sufficient, but you risk deviating from the standard git send-email format. The following is displayed on git-send-email.io, the git & email tutorial:

Warning! Some people think that they can get away with sending patches through some means other than git send-email, but you can’t. Your patches will be broken and a nuisance to the maintainers whose inbox they land in. Follow the golden rule: just use git send-email.

So it’s safe to say we ought to stick to git send-email. But we can use it interactively from within Emacs.

Calling git send-email from Emacs

Calling any shell command is pretty easy in Emacs, just type M-! (or M-x shell-command) and type git send-email -1. This will open up a shell command buffer and git send-email will prompt you for the recipient and so on. You can stop this process at any time using C-c C-c if you’re note happy with it. Here’s an example patch generated using git send-email:

From: Antoine Kalmbach <ane@iki.fi>
Date: Mon, 19 Oct 2020 21:47:50 +0300
Subject: [PATCH] Added a line

This should improve the README experience by quite a bit.
---
Not sure about the final wording.
 README.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/README.md b/README.md
index f9e3d61..2e4f197 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,5 @@
 # Hello there!
 
 Blah blah blah.
+
+Let's add this line here.

Sometimes it makes sense to annotate your patch with a message that explains what the patch does, but not in a way that it would end up into the commit history. In the example above, that’s the bit under the three dashes ---.

To add such an annotation, git send-email specifies a --annotate flag that will preview your patch in your editor specified by the $EDITOR environment variable. If you have your EDITOR set correctly to e.g. emacsclient, this will work just fine provided you’re running an Emacs server.

Setting Emacs as $EDITOR

If you try --annotate without EDITOR set, git send-email will most likely complain that your terminal has no editor specified, and quit. We’re executing a shell command, so we want to tell the terminal that we’re doing this from the editor. There’s an easy way to do this: Emacs has a with-editor-shell-command which will then set EDITOR to the Emacs process. Just call M-x with-editor-shell-command and type git send-email -1 &. The ampersand tells the shell to execute the command asynchronously, letting us interact with the process while it is running. (If it was executing synchronously we’d have no way of suspending the command execution to spawn an editor buffer.) You can also do directly M-x with-editor-async-shell-command and you can drop the &.

You can also call git format-patch to produce the patch and then send it using git send-email which also accepts patch files. That way you can inspect the patch file before sending it, but this is mostly unnecessary since if you pass --annotate to git send-email it will preview the file anyway. I recommend calling

$ git config --global sendemail.annotate yes

And git send-email will always preview the patch before sending it.

Ok now that we’ve successfully sent a patch, how do we turn this around and apply patches sent to us?

Applying email patches in Emacs

Let’s assume a patch has landed in your email. You need to be able to save this patch in either a mbox or maildir format. If you’re not sure how to do this, you can just copy the mail verbatim into a text file and then pass that to git am. git am will detect the format and use the From: field to set the committer and Subject: and message body will go into the commit message.

How to do this in Emacs? For this, we don’t have to just rely only on shell commands anymore. We can use either

  • the wonderful Magit package with its interface for git am
  • opening the patch and sending (piping) the whole buffer to git am

Using Magit, this is as simple as pressing w w (magit-am-apply-patches) or w m (magit-am-apply-maildir). Just find the patch email and you’re set.

Without Magit you can do this just by opening the patch and piping its output to git am. That is as easy as doing M-| git am. But that requires you to be in the same directory as the repository itself. So you’d have to M-x cd into the directory before applying. But all in all, there are several ways for doing this directly from Emacs, and of course as a last resort you can just open the Emacs shell M-x eshell and do it from thecommand line.

Making our own M-x git-am

If you’re not too keen on using Magit or cding to the directory every time, I wrote this handy little Emacs Lisp to provide a nice M-x git-am command:

(defun git-am (repository)
  "Run git-am in REPOSITORY with the current buffer.

If REPOSITORY is nil, prompt for a directory.  If a region is
selected, it will pipe the buffer to git am.  If the current
buffer mode is Rmail, Gnus article, or diff mode, it will call
`git am' with the contents of the current buffer.  

If the current buffer isn't appropriate and there is no region selected,
call git am after prompting for a patch/mbox/maildir. 

If the current buffer is a `vc-dir-mode', assume we are in
the repository and we want to look for a patch somewhere else.

If Magit is available and we're in a `magit-status-mode' buffer,
the same treatment as `vc-dir-mode' is given.

With prefix argument ignore the fact we're in a version control
buffer and prompt for the repository anyway."
  (interactive
   (list (if (and (not current-prefix-arg)
                  (memq major-mode '(vc-dir-mode magit-status-mode))
                  (eq 'Git (vc-responsible-backend default-directory)))
             (vc-git-root default-directory)
           (read-directory-name "Repository: "))))
  (when (not repository)
    (error "No repository given."))
  (when (not (eq 'Git (vc-responsible-backend repository)))
    (error "Not a git repository: %s" repository))
  (let ((default-directory (expand-file-name repository)))
    (cond ((mark)
           (shell-command-on-region (region-beginning) (region-end) "git am"))

          ;; Use the whole mail buffer when viewing a patch email or patch file.
          ((memq major-mode '(rmail-mode gnus-article-mode diff-mode))
           (shell-command-on-region (point-min) (point-max) "git am"))
          
          (t (shell-command
              (format "git am %s"
                      (read-file-name "Apply patch (mbox/patch/maildir): " nil nil t)))))))

This command works both in version control status buffers, dired buffers, patch files and patch emails. If the user is looking at some version control status buffer, it assumes the repository is the one we’re in, unless the prefix argument is given by pressing C-u M-x git-am. If we’re in a mail buffer or looking at a diff-mode file (*.patch), we’re going to apply this patch to some unknown repository. And if none of this is true we’re going to prompt for the patch as well.

Emacs has for a couple of major versions supported the idea of version control as projects. That is, a version controlled directory tree is a project, i.e. a collection of related files. This facility leverages ignore mechanisms like .gitignore by providing an interactive file selection interface but with ignored files filtered out from the search. Whenever you interact with such a project, Emacs registers this repository into its list of known projects.

Since these projects map to git repositories, I wrote a wrapper for git-am that will present repositories using project-prompt-project-dir:

(defun project-git-am (&optional use-current-project)
  "Run `git-am' in a project directory.

With a prefix argument run in the current project if found, otherwise
prompt for a project directory.

See the Info node `(emacs) Projects' for what a project is."
  (interactive "P")
  (let ((root (if (and use-current-project (project-current))
                  (project-root (project-current))
                (project-prompt-project-dir))))
    (git-am root)))

Looks like this requires installing project.el from GNU ELPA, or using Emacs 28 from Git, which includes this version of project.el.

Who knows, maybe all of this will end up in its own package one day! Even without this Elisp code you can easily use git am quite readily in Emacs with the tools already available. If nothing else, this code shows how easily you can do powerful customizations for simple shell commands in Emacs!

Conclusion

Git + Email = ❤. It’s nice to see a resurgence of Git and email being boosted by sites like sourcehut. I think the first time I used Git and email must have been in maybe 2008? I don’t remember what it was for, but I do remember mailing it to the author directly from alpine. Not long after that GitHub came and made everything different.

Previous: Divergence: A website is born Next: A Guile test runner with an exit code