2022-08-25 — 6 min read

Following the Github Flow with Emacs and Magit

Github Flow is a lightweight, branch-based git workflow. Let’s set up Emacs and Magit to use it from the terminal, without opening Github in the browser!

Installation

I’m using Arch Linux, so installing Emacs is a single command:

pacman -S emacs

The next step should be the installation of Prelude. Emacs + Prelude is what the default Emacs installation should be, in my opinion. Installation is easy:

curl -L https://github.com/bbatsov/prelude/raw/master/utils/installer.sh | sh

While I’m not at all a fan of this type of installation due to the security implications of running untrusted code, we can split it up into two steps, and inspect the downloaded shell script before running it.

The default Prelude installation also installs the Magit package, but we’ll need to install Forge to simplify the work with Github. In Emacs:

M-x package-install forge

To store the credentials we create in the next section, we’ll also need GnuPG (which is probably already installed).

There are some actions we can’t do with magit/forge yet. For example, we can’t yet create a new repository on Github (there’s an open issue for it). For this reason, we’ll also need to install Github CLI to fill in the gaps. Installation is a single command:

pacman -S github-cli

Configuration

Let’s configure what we just installed. I followed the instructions of the forge manual. I configured access to a Github account.

We have to set up the ghub package that forge uses to access Github or other forges. That means configuring the Github username:

git config --global github.user USERNAME

Then we create a personal access token on Github. We’ll need to provide a name for the token, an expiration date, and select the following scopes: repo, user, and read:org. Ghub will use this token to access our Github account, so we’ll have to save it somewhere it can access. This is what the auth-source library of Emacs is for. We could use the ~/.authinfo file for our secrets, but that is unencrypted, the passwords, and tokens would be stored in plain text. So we’ll configure Emacs to use the encrypted ~/.authinfo.gpg instead. Add to ~/.emacs.d/personal/custom.el:

(setq auth-sources '("~/.authinfo.gpg"))

We’ll need a key pair to encode/decode the file. If we don’t have one already, we can create these with GnuPG:

gpg --full-gen-key

I chose the following parameters:

GnuPG will add the generated keypair to its keyring, which we can access inside Emacs with the built-in EasyPG package. We can check whether it’s working with the following command:

M-x epa-list-keys

We should see the key we just created or the one we already had, along with other keys we might have in the gpg keyring.

After this short interlude, back to creating ~/.authinfo.gpg.

Open the file ~/.authinfo (without .gpg) in Emacs, if it doesn’t exist yet, create it. Add a line similar to the following:

machine api.github.com login my_username^forge password my_github_token

… but replace my_username with your Github username, and my_github_token with the token created previously. After that, call

M-x epa-encrypt-file

which will ask for a key to encrypt the file with. We should choose the one we just created (or the one we already had). Mark it with m, then press Enter while the cursor is over the [OK] button. Then the library will encrypt the file with the chosen key, save it as ~/.authinfo.gpg, and set its permission to 600 (-rw-------) so only the owner can access it. We should then delete the ~/.authinfo file which stores the token in plain text.

Let’s check whether the encryption actually happened. Open the ~/.authinfo.gpg file in your file manager, you should see some garbled characters, not the line we added to the unencrypted file previously. Now open the same file in Emacs: you should see a popup asking for the passphrase, then the decrypted file in a buffer. If we edit the file, Emacs would automatically re-encrypt it before saving. Neat.

Configuring Github CLI is most simple:

gh auth login

It’ll ask a couple of questions, open a browser window to authenticate with Github, and then we’re done.

With all of these in place, we can start coding!


Creating a local and a remote repository

We’ll create a simple test repository to try Github Flow with. Let’s create a test folder with a single file, first.txt in it. The file should contain a single line of text, I went with a really original one: First line.. Save the file, then open the Magit status buffer with C-x g. It should ask for the repository’s path, with the current file’s path as default, which we should accept, then it’ll ask whether to create a repository here – it should since we don’t have a repository yet.

The status window will show that we have one untracked file, first.txt. Let’s add it to the staging area by moving the cursor to the first.txt line inside the Untracked files section, and pressing s. It’ll move from Untracked files to Staged changes. (We could also have pressed s on the Untracked files line, in that case, Magit would add all of the untracked files to the staging area, not just a single one.) If we didn’t mean to stage this file, we could simply unstage it by pressing u on its line. For now, let’s keep it staged.

Create the first commit by pressing c c. This will open two new buffers, one is for writing the commit message, the other shows a diff with the changes we are about to commit. Let’s add a commit message (I went with Initial commit.), then press C-c C-c to create the commit. If we want to cancel the commit, we should press C-c C-k instead.

We now have a local repository with a single commit. Let’s create a matching remote repository next. We can’t yet do this in Magit, so we’ll need to use Github CLI which we installed previously. From the test folder, run the command

gh repo create

It’ll ask a couple of questions:

Back in Emacs, let’s push our commit to the Github repo. Press P p to push the current branch (main) to its push-remote. It’ll ask for the push-remote since it’s not set yet, choose origin. We’ll also need to initialize Forge’s database so we can use it with our repo. to do this, press f n. Now that everything is set up, let’s get started with Github Flow!

Create a branch

We currently have a basic repository with a single branch, main. This branch has a single commit, our initial commit.

We want to create a new feature in our repo (in this simple example, it’ll be an extra line in our file). First, we create an issue on Github to track the feature: press N c i, then add the issue title and text, then press C-c C-c. The new issue will appear both on the Github webpage, and in the Magit status buffer under the Issues heading. Next, we create a new branch for this feature, so as not to affect the main branch with our new work as long as it’s not finished. To create a new branch and then immediately check it out, press b c. We’ll set the new branch’s source to main, and name it 1-add-new-line-to-file. From now on, we’re on the 1-add-new-line-to-file branch, anything we do won’t affect the main branch.

Make changes

Let’s add an extra line to our file, first.txt, I’ll add Line from 1-add-new-line-to-file.. We can see in the Magit status buffer that we have unstaged changes (the line we just added). Let’s stage it and then commit it: press s in the status buffer on the file’s line, then press c c, write the commit message (Added first feature), then press C-c C-c. We can switch between the main and the 1-add-new-line-to-file branches by pressing b b: by checking out the main branch, we can verify that our change isn’t present in that branch, only in the 1-add-new-line-to-file branch. We should also push this to Github by pressing P p. In a real-world project, this would be followed by many more edits, commits, and pushes, but let’s say we’ve finished this feature.

Create a pull request

We can create a pull request by pressing N c p. We need to select the source branch (1-add-new-line-to-file) and the target branch (main), then write the title and body of the pull request. Since we want to close the issue we created previously when the pull request is merged, we should include Closes #1 or a similar keyword to link the pull request to the issue.

Merge the pull request

We should check out the main branch (b b), then merge our feature branch into it by pressing m m, and selecting the proper branch (1-add-new-line-to-file). Then we should push the changes (P p). It should automatically close the pull request and the issue as well.

Delete the feature branch

Lastly, we need to delete our feature branch by pressing b k and selecting the branch. The work on this branch is complete, and deleting it prevents us from accidentally using old branches. We should also delete the remote branch by pressing y, selecting the remote branch, and pressing k.

Final thoughts

In this post I described a very simplified version of the Github Flow: I didn’t touch issue comments, review comments, assignees, labels, milestones et cetera. Most of these also work with Magit/Forge, and if they are not supported, we could always use Github CLI.

Magit commands used in this post

Command Keystroke
Open status buffer C-x g
Stage changes s
Unstage changes u
Initiate commit c c
Create commit C-c C-c
Cancel commit C-c C-k
Push current branch to its push-remote P p
Forge pull - fetch topics & notifications f n
Create issue on forge (e.g. Github) N c i
Create branch and check it out b c
Checkout another branch b b
Create pull request N c p
Merge another branch into the current one m m
Delete a local branch b k
Delete a remote branch y [select branch] k

Thanks for reading! If you have any comments, additions, or corrections, feel free to reach me via e-mail.

Copyright © 2023 csm.hu
Contact