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:
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:
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:
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:
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:
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
:
We’ll need a key pair to encode/decode the file. If we don’t have one already, we can create these with GnuPG:
I chose the following parameters:
- RSA and RSA
- keysize: 3072
- expiration date: 1y
- my name
- my email address
- no comment
- and a passphrase
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:
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:
… but replace my_username
with your Github username, and my_github_token
with the token created previously. After that, call
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:
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
It’ll ask a couple of questions:
- We’d like to push an existing local repository to Github
- The path to the local repository is
.
- The repository name is the same at the local folder (
test
) - Add a description
- I set the visibility to
Private
- I also added it as a remote repository to the current local one
- And named the remote
origin
- I chose not to push commits, as I’ll do that in Magit
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.