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:
- Drew Devault in The advantages of email-driven git workflow
- Joe Nelson in Mailing lists vs. Github
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 someonegit 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:
- Prepare some changes.
- Commit the changes.
- Use
git format-patch
to prepare a filefoo.patch
. - 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 gitsend-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 cd
ing 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.