This is a brief introduction to the tools needed to maintain a personal fork of ZMK (or QMK or really whatever). It covers:
- the initial setup
- updating your fork with the latest ZMK features
- merging PRs into your fork
- deleting PRs from your fork
- resetting your fork
- using your fork to compile with Github Actions
- appendix: glossary
Throughout I am using examples from ZMK but nothing is specific to ZMK (other than the compiling stuff). I am deliberately simplifying things, only covering what's absolutely essential for maintaining a personal ZMK fork for non-developers.1
First, create your personal fork of the official ZMK repository. To do so, navigate to https://github.com/zmkfirmware/zmk and click "fork" in the upper right corner. For now your fork lives online ("remotely") on Github at https://github.com/your_username/zmk
. For example mine is https://github.com/urob/zmk.
To work with your fork, next create a local "clone" of your fork on your computer. Navigate to the parent working directory in which you want to create the clone, then type:
git clone https://github.com/your_username/zmk --single-branch
The --single-branch
argument is optional. It tells git to only download the main
branch of your fork, which helps keeping the local clone clean.
Eventually, our fork will grow out of date. To keep it updated with the latest ZMK features, we first have to register the official ZMK repo with our local clone:
git remote add -t main upstream https://github.com/zmkfirmware/zmk
Here the -t main
argument is optional, it tells git to only register the "main" branch of the official ZMK repo. upstream
is an alias that we will use to interact with the official ZMK repo. It can be anything, but "upstream" is customary for the source repository.
FYI, when we created our local clone, git automatically created another alias, origin
, that points to our own ZMK fork on Github.
Now that we have registered the "upstream" ZMK repo, we can use it to update our own ZMK fork:
git fetch upstream
git rebase upstream/main
git push --force
The "fetch" command simply checks what's new with the upstream branch. The "rebase" command resets our local ZMK branch to the latest upstream state and then re-adds everything that we had done in our branch. Hopefully, everything goes smoothly and there are no merge conflicts. Finally, the "push" command pushes the changes we just made in our local repo back to our remote fork on Github (the --force
argument is needed here due to the way git rebase
works).
First, we have to locate and register the remote repository that contains the branch that we want to merge. We can use the same command that we used above to register the "upstream" ZMK repo. For example, for ftc's "mouse PR" branch:
git remote add -t mouse-ftc mouse-stuff https://github.com/ftc/zmk
Again, the -t mouse-ftc
is optional. Here I used it to only register the "mouse-ftc" branch (which is the one that contains the mouse PR). mouse-stuff
is again an arbitrary alias -- choose what makes sense to you.2
Once we registered the remote, we can merge it into our own local clone:
git fetch mouse-stuff
git merge mouse-stuff/mouse-ftc --squash
git commit -m "Mouse emulation support"
git push
The already familiar git fetch
now checks what's new with the "mouse-stuff" repository ("mouse-stuff" is the alias we chose above). git merge
does the actual merging into our local clone. The --squash
argument tells git to bundle all the commits in the source repo iinto a single one instead of adding them one-by-one. It's a matter of personal taste, but I like it, because it keeps my git history clean and allows for easy removal of a PR (see below). Next we commit all the merged changes with git commit
using a commit message of "Mouse emulation support" (can be anything). Finally, we push these local changes back to our fork on Github.
This process can be repeated to merge as many remote branches as you like. To give another example, to add the "fix-mod-morph" PR and the "positional-hold-tap-on-release" PR, we would first register the source remote:
git remote add urob https://github.com/urob/zmk
I haven't used the -t
argument this time, because I am planning to merge two branches from this repo. Once the remote is registered, we can merge the two branches and push the changes back to our remote fork with:
git fetch urob
git merge urob/fix-mod-morph --squash
git commit -m "Fix mod-morph"
git merge urob/positional-hold-tap-on-release --squash
git commit -m "On-release property for positional hold taps"
git push
Resolving merge conflicts is beyond the scope of this little introduction. While some merge conflicts are simple formatting issues, major merge conflicts require digging through the source code and understanding what's actually going on in order to resolve them correctly. Fortunately, in my experience, most PRs merge cleanly. And if not, there is often already someone who resolved the merge conflict (like ftc for the "mouse PR"), and one can just merge their branch instead.
Suppose at some point we decide that we no longer need the "Fix mod-morph" PR (perhaps because it has been merged into the official ZMK repo). If you followed my advice above, the entire PR is a single commit in our repo. To delete it, run:
git rebase upstream/main -i
This is almost the same command that we used above to update our repo with the latest ZMK features. As above, it first resets our local branch to the state of the upstream branch (or, more accurately, to the state it was in the last time we ran git fetch upstream
). But then, because we have specified the -i
argument, instead of re-applying all our local changes, it opens a text editor and lets us choose interactively what we want to do. The editor lists all our commits that we have made on top of upstream. It should look similar to this:
pick 2efdd3ce Mouse emulation support
pick d3eb8c3d Fix mod-morph
pick d3768f3a On-release property for positional hold taps
To delete one of those commits, replace "pick" by "d" and then save and exit the editor. For example, to delete the "Fix mod-morph" PR, we would do
pick 2efdd3ce Mouse emulation support
d d3eb8c3d Fix mod-morph
pick d3768f3a On-release property for positional hold taps
Once we are done, we can push back the changes to our remote fork on Github with:
git push --force
Suppose we have done something weird and screwed up our local clone. If we haven't pushed our changes back to our remote fork on Github, we can simply reset our local clone to the state of our remote fork on Github by running:
git reset --hard origin/main
Alternatively, we could reset our repo to the state of the official ZMK repo by running:
git fetch upstream
git reset --hard upstream/main
git push --force
If we only want to undo some of our commits, but have already pushed them to our remote, we can use the interactive rebase command from above to first delete the offending commits from our local repo:
git rebase upstream/main -i
Once we fixed our local repo, we can then push back the repaired repo to our remote fork on Github:
git push --force
Now that you have your shiny own personalized ZMK fork, you want to use it to build the firmware. You could just embrace the full experience, install the toolchain, and compile locally. But perhaps you want to save the full experience for another day. In this case, you can still use your own repo to build with Github Actions by replacing the west.yml
file in your zmk-config
repo (not the same as your new zmk
repo!) with this:
manifest:
remotes:
- name: some_name
url-base: https://github.com/yourusername
projects:
- name: zmk
remote: some_name
revision: main
import: app/west.yml
self:
path: config
That's it. You now know how to update your fork with the latest official features, how to merge PRs into your fork or delete them, and how to reset your repo. Happy building!
Okay, I get it, there's a lot of terms flying around. This is what some of them mean:
remote
: A repository that lives somewhere online. In the guide above we have used three types of remotes:upstream
: the original source repository (https://github.com/zmkfirmware/zmk/)origin
: your personal fork on Github (https://github.com/your_username/zmk/)- other remotes with branches that we did merge (https://github.com/some_dude/zmk/)
- our local clone: a local copy of
origin
Note that there is no automatic syncronization in git. To make our local clone aware of new stuff happening in any remote, we use git fetch
. To actually apply changes from a (fetched) remote to our local clone, we use git merge
and git rebase
(there's also git pull
and git cherry-pick
). And to push back changes from our local clone to origin
(or other remotes with write-access), we use git push
.
Footnotes
-
This means that I am leaving out many core concepts such as
git add
,git pull
,git branch
(andcheckout
),git stash
,git submodule
andgit subtree
,git cherry-pick
, resolving merge-conflicts, viewing diffs and logfiles, etc. The reason is that there are already tons of great introductions to git. The problem is that git is complex, which can make even the best introductions a bit daunting, especially for people who only want to combine a couple of PRs without developing their own code (if you develop your own code, you probably don't need help using git). Hence the idea for this "cookbook approach". ↩ -
Personally I like to use remote aliases based on the maintainers username. But for the purpose of this guide, I wanted to emphasize that it is the alias that it used when interacting with the repo, not the username. Hence, my choice of "mouse-stuff" as opposed to "ftc". ↩
It's a nice summary of the topic. I think it's worth adding a description how to manage contributing back into the upstream repos.
So for example if I have my repo that combines multiple branches from various upstream repos and I want a modification I made to go back as PR to one of those repos, how should it be done so I can isolate only that change without all the extra modifications from other upstream repos I've incorporated into mine.