Emacs helper for Pull Request descriptions
Using Emacs, you eventually need to learn Emacs Lisp (elisp) as everything is written in that language. Configuration is also in elisp, but usually requirements are minor enough not to need much learning. I’ve tried to learn it several times with low success, but recently I got to write a helper for myself that I think is worth sharing. This is just beginner stuff, and hopefully will help me remember how the bloody thing works :)
To no one’s surprise, I’m one of those people that use Emacs. And as I use Emacs, I obviously want to do everything from Emacs and never context switch away.
One of the nicest things I found in Emacs when I started using it was Magit, a git porcelain. A few years after, Magit’s maintainer created a companion package to work with git forges (e.g GitHub, GitLab…) called forge. At work we use GitHub, so I mostly use forge there.
Alert: The post meanders a bit and goes chronologically towards the end. You can skip to the last section if you don’t care about my (boring) voyage there.
What we want #
Forge let’s you create view and add comments to issues and pull-requests and at work we set out on a way to format the “cover letter” of a pull-request. This format is somewhat inspired on the actual cover letters for request-pull, the command GitHub took inspiration from, and required for sending patches to the Linux Kernel mailing list (creators of git). In this Pull Request description we want:
- An overall description of the whole Pull Request
- A list of the commits and descriptions
The goals for this system are to present the overall goal of the effort as it may be bigger than the sum of goals in every individual change, and the view of all individual changes at once.
At the very beginning I did it by hand. As most Pull Requests had only one commit and GitHub pre-populates the message with the commit contents when there’s only one commit, it was easy. At the time I was doing that via the Web interface, as forge had not been born. I dreaded the few Pull Requests where I had more than a couple commits at once.
The first approach: pure Git #
I soon looked for ways to ease/automate it. First step was to
gather all commits and messages in the proper order so I didn’t
have to copy them one by one. That’s actually what git log
is
for!
git log --date-order --reverse --format=**%s**%n%b develop..HEAD
With this I just had to copy/paste the output from the terminal to wherever was needed.
The second approach: All in Emacs #
When forge appeared, I felt like I could cry! No more web interface to adjust my text! Yes!
History disclaimer: prior to forge other projects existed that worked remarkably well and I used them. Those projects depended heavily on Magit and were to be merged with it, but the resulting mess was extremely complex, so the maintainers of all those projects sat together to create forge and split some more things from Magit itself into a much nicer bundle. Reality is usually messier than worth describing.
Now I could enter the text from Emacs itself, but I was still generating the text in a terminal and copying it over. There had to be a better way! The solution was to write some elisp:
(defun mbernabe/git-log-for-pr (branch)
"Gather commit messages diverging from target branch and format them for PR"
(interactive "sTarget branch: ")
(magit-git-command-topdir
(concat magit-git-executable " log --date-order --reverse --format=**%s**%n%b"
(format " %s..HEAD" branch))))
This function is marked interactive
so it can be called from the
command menu, and takes an argument from the user, which is the
branch we are asking to merge into. It then goes to the top
directory of the repository and uses the git binary detected by
Magit with a hand-built string of arguments.
This has some issues. First, I must have not read the Magit manual well (or at all) because I’m selecting the binary by hand when there are much better options. Second, I’m manually formatting and building the command string. Third, The output is written in the log buffer, so I still have to copy it over (inside Emacs). And finally, I have to write the branch used as default every time (and not all repositories use the same, and typos. Hilarity ensues).
But it worked, and it was inside Emacs, and I had learned next to no elisp but was dangerous enough to break things. Perfect!
The final approach: Doing things right^W better #
- Modified last code example to be split in several functions. This is how I have that code right now and I feel it’s easier to understand.
- Modified completion in last code example to ignore minibuffer-history. This is how I have that code right now and it solves several occasional and strange issues with completion where execution would fail calling the last string in the history. I do not understand why it does not work but this arrangement now works.
Of course the previous state was not good enough and something had to be done. The first goal was to insert the lines at point instead of having a separate buffer (which I had unsuccessfully tried when writing the previous version). This was eased by actually reading Magit’s manual and finding a section on functions for user extension (oops!).
Second goal was to provide completion, meaning to select a local branch among all possible branches instead of typing it from scratch every time. This became surprisingly easy once I realised the mechanism for it is generic and supports different backends (I use Ivy) so I didn’t need to write any weird incantation for it.
Third goal was to suggest the correct branch to the completion, so I can mostly accept the suggestion right away. I still like to select the branch in case I need something different (I can use the same thing to show differences between feature branches or releases) and because I don’t like hard-coded things.
Here is the improved (and current) version:
(defun mbernabe/git-log-for-pr (branch)
"Insert formatted commit messages between here and BRANCH
Gather commit messages between current branch's HEAD and target
BRANCH and format them for pull request. BRANCH defaults to git's
init.defaultBranch if defined, and magit defined standard branch
names. You may alter the default with:
git config --local init.defaultBranch develop
"
(interactive (list (mbernabe/git--branch-competing-read))
(magit-git-insert "log" "--date-order" "--reverse" "--format=**%s**%n%b"
(format "%s..HEAD" branch)))
(defun mbernabe/git--branch-completing-read ()
(completing-read "Target branch: " (mbernabe/git--list-branches)
nil t nil nil (magit-main-branch))
(defun mbernabe/git--list-branches ()
(mapcar (lambda (element) (string-trim-left element "refs/heads/"))
(magit-git-lines "for-each-ref" "refs/heads" "--format=%(refname)")))
As you can see the structure is improved. First, I use
magit-git-insert
to get the command output directly to where my
pointer is, and it’s also cleaner than the previous attempt. All
those arguments could be a list or other structures, which may be
useful to swap options or add optional ones, but for now I have no
need for them.
To select the branch I do a bit of git trickery. There is no
command to list all local branches nicely, but there is one
plumbing command to list all references with a HEAD (which are
branches). With magit-git-lines
I can get the output as a list
of lines (one per branch) and then I apply a regular expression to
leave just the branch name.
Once I get the list of branches I pass them to completing-read
,
which is the interface for completion. The manual explains how it
works but something it took me an embarrassingly long while to get
right was noticing the INITIAL
parameter has been deprecated and
the DEFAULT
should be used instead. Part of it is that the
manual’s own example uses the deprecated option and I could find
no examples doing it properly.
Conclusion #
It’s been a bit of ride for this process (my earliest notes running it in a shell are from Nov 2019) but I’m happy to have gotten to a point where I can understand how elisp works a bit better, and to be advancing to be less dangerous with it.
I still feel there’s too big a gap in available information between the complete newbie to Emacs and the hyper advanced guru, and I’m trying with this post to close that gap a bit as I find out what I need.
Of course, feel free to use these snippets and modify them, they are licensed like everything else in this blog under CC-4.0, and please post any comments, suggestions and improvements to my public inbox.