I have spent much time working on Git Cherry Tree, and I wanted to share how things are made, how it ended up being where it is today.
There are many things that go into making a piece of software, and more perspectives to look from. We will cover some of the design decisions behind Git Cherry Tree, describe how a feature makes it into the client, cover day to day work, we will even talk about AI! And lastly we will cover how Git Cherry Tree has changed over time.
Working on a Git client is terrifying. Unlike other stuff I have worked on, where crashing is the worst that could happen, Git Cherry Tree works with people’s repos, and their work. Possibly years of it.
So: It must never ruin someone’s work. Ever.
I take much care to make sure that this piece of software is something you can really, truly rely on. A big part of how Git Cherry Tree is made, is about that.
Let’s start with some requirements, so we can keep those in mind as we go:
1. No losing work
This covers both bugs, misclicks, and surprises. Sometimes you get unexpected results, and should be able to go back. That’s undo. But also, of course, the client should never break your repo because of some bug. That’s much harder.
2. It should work well, even when something goes wrong
Things always go wrong, and when they do, the client should safely and gracefully degrade, not crash or make things worse. This also means not leaving the user confused as to what happened.
3. It should feel and be smooth, helping you work without friction
This drives a lot of the performance requirements too! Opening instantly, real time filtering, loading stuff without size limits, all that helps avoid breaking your flow. The client is something that helps you along, not something you fight.
The rest of this post is about how I try to meet these, and let’s start by looking at some design decisions in the client as a whole.
Having one writer
I split various things the client can do into read and write operations. A write operation can change the git repo on disk, while everything else does not. To avoid many headaches, I have only a single writer, with every command going through that. This ensures the client always works on a known repo state and there’s only one thing changing the contents on disk.
That is what I would like to say, but what about external processes?
The repo can change out from under us at any time! In fact, this happens constantly. If you save a file in an editor, the git client needs to rediscover this fact. It could have happened while the client was not even turned on! So every write operation first checks the repo state and that it’s safe to proceed. In extreme cases like a power cut or deleting half of the repo, you can’t avoid issues.
Fortunately, these extreme cases are rare, but even then, Git Cherry Tree tries to do something sensible.
Working in memory
It is much easier to be sensible if you can roll back an operation. But how do you make sure that the roll back itself doesn’t fail?
A good way is to do as much as you can in memory, without writing it to disk. Then, if something goes wrong, you didn’t save anything and can simply discard the partly done operation safely.
Git typically runs long operations like rebases by saving every intermediate state to disk. Git Cherry Tree does not. Instead it only changes disk at the end.
This also turns out to be a feature. By reducing the number of disk writes, you are more friendly towards external file watchers which rebuild your project every time something is saved.
But more than that - you can now do many things like pulling branches, even merging and rebasing them, without ever checking them out.
To me this is a quite neat example of setting out to make something robust first, and getting a cool feature out of it second.
Undo
Another way to do rollbacks, is to add undo. This helps against surprise changes. You expected a button to do one thing, and it did something else. Being able to easily undo, and then also redo things is important not just to fix a mistake, but also to let you click on stuff to see what it does. And some users click on every button without even reading it!
Unlike in a typical application, the state could change under us at any time. So it isn’t enough to add snapshots and revert to them.
Instead, Git Cherry Tree takes partial snapshots. This lets you only undo the changes an operation did, and external changes don’t get clobbered. This also remains declarative - i.e. always set to THIS - which avoids many edge case issues with inverse operations that don’t handle partial completion or complex states, and would require much more code specific to each action.
We also always take a snapshot just before doing something, including just before doing an undo. This means that you get a fresh state captured regardless of what might have changed under you (it always does - any file saved is state changing under you!). And in turn, that lets you undo the undo i.e. the redo button. That way, both undo and redo work as the “put me back” button which puts you back into the state before you just pressed it.
This type of undo appears to be unique to Git Cherry Tree. I’ve seen other clients do per action inverse commands (example: jj op revert), and I’ve seen wholesale restorations of state (some other git clients), but not this specific combination of features.
Lastly, Git Cherry Tree also takes snapshots of uncommitted changes: if you discard those, you can undo that as well. This is also a reason why we don’t simply use the reflog. There is more to undo than what Git’s reflog captures.
No crashing
Aside from being disruptive, crashing is bad for being able to rollback state, since you could crash mid operation.
Helpfully, the client is written in Rust. This language is pretty good for not crashing. All the code that could crash is sectioned off, and so by not writing that code (much), you can avoid crashing entirely. In theory.
In practice I try to get as close as possible to a crash proof application, and I think I come pretty close!
During the first couple of weeks of development, I improved the error handling and added a crash catcher, which makes would be fatal issues into notifications in the bottom right corner.
After that, the crashes over the entire life of the project fit on one hand:
- A buffer overflow in libgit2, where a false negative from libgit’s binary detection caused it to try and diff an image file.
- I caused a crash touching unsafe code in Iced (a UI library) while I was rewriting the UI
- One or two shader issues while working on custom shaders (those are not written in Rust)
All of the above were during development, and I don’t know of any that happened in production. Generally speaking, as long as Git Cherry Tree starts up, I can expect it to not crash.
So, does the crash catcher do useful work? You may be surprised to know that it doesn’t! I try to handle every error that I can, so fatal issues don’t show up. I sometimes think about removing it, but choose to keep it around for now as it handles a wider range of errors than me-not-making-any-mistakes.
Using libraries
I think it’s important to know your boundaries. There is some stuff I can’t do reliably enough, or might make mistakes. At these times I can rely on code written by others, which has had much more time and effort put into it than I ever could.
Let’s talk about a few dependencies I use and why.
A git backend is serious business. It may seem simple, but there are so many edge cases and such a variety of machines, that you would never be able to test it all, especially since it works with files on disk, which adds every operating system’s quirks into the mix. I use two or perhaps even four backends for git.
- Libgit2: This C library is used by every second git client out there, and is extremely battle tested. I use this by default when writing and reading the git repo.
- Gitoxide: A Rust based reimplementation of Git. Very cool, but newer, and so I use this for places where performance is required, or it has features libgit2 is missing, such as filters.
- Git itself: This is optional in Git Cherry Tree. If you have it installed, I use Gits credential manager to avoid storing and managing your keys for every git compatible server (Gitlab, etc).
- Git LFS: This is an extension to git, and is needed if you want to use the LFS features on your repo. Without this, Git Cherry Tree won’t touch LFS files to avoid breaking anything.
Aside from that, I use a few crates to handle connections to Github safely, and wgpu, which I use for rendering the UI. I do have other dependencies, but generally try to keep them down, using ones which solve a well defined problem with more rigour than I could bring, and have a track record of being reliable.
Lowering the blast radius
I also compartmentalize bits of code. I share some code between operations, but less than I could. I keep them separate on purpose, so that if there is a bug in one operation, it doesn’t affect another. This also makes them easier to reason about.
Separating entire systems out is also something that helps. I can more easily rewrite them, or need to reason about smaller parts of the codebase at once. Perhaps compartmentalisation is really just good software design.
Not using features
Another way to reduce blast radius is to not use your own code. I have a custom rev walker that walks about 40 times faster than libgit2, walking the million commits in the Linux repository in under two seconds. I don’t use it. Except for one place - a display only ahead/behind counter - where I need it to be fast, since you could be 60,000 commits behind. This way I can keep the write paths using the tried and tested libraries, even though I have fancy fast code right there.
Not adding features
An even better way to not have any bugs is to not have any code. Sadly, we must have at least some code to do what we need.
I keep track of line count very carefully. Everyone else does it wrong! You need to add the least amount of code, and delete as much code as you can, not the other way around! A 10k lines added commit is terrible, but a 10k lines deleted commit is great!
Also I’m hesitant to add more features. I would rather the client be guaranteed to not break anything than have tons of fancy features, so a lot of stuff is either deferred, or not on the list at all.
This also has the advantage of having a clearer interface, and being less confusing. I consider myself to be good with git, but still have issues with many interfaces where I don’t know what all these options do, and even if I know the term, I don’t know what will really happen if I press a button labelled “Pull”. For years I avoided this by using the trusted git CLI, where I can configure it to, for example, never merge if my local branch is diverged.
In Git Cherry Tree, I try to solve this by having fewer options, and clearly labelling the ones that exist. Lastly, undo/redo helps you try out the buttons and see what they do.
How a feature is made
Let’s go through a feature and see how it ends up in the client. My custom implementation of Git status has a long journey, so let’s use that as an example.
It starts with an idea. And the first thing is to do nothing!
Features need thought, and thought needs time. There is always plenty to work on, so I add the idea to my to do list, and work on something more important. And that gives me time to think about various questions:
Do we really need this right now? Is there anything more important? Will it cause issues? Is there a better version of this? Do we need this at all?
The feature can sit for weeks, months. Ideally, it gets cancelled. The more things I cancel, the fewer lines of code, fewer bugs, fewer issues later.
This almost happened here. I looked into making it faster in late 2025, but concluded it wasn’t worth it. Libgit2 was doing an alright job, and this was a speedup, not a bugfix.
However, I did have some performance issues which made the client less responsive on very large repos. After a fateful conversation with the fine folks at GitButler, and Byron, the maintainer of gitoxide, I decided it was time to look into this.
The next step is research and testing. I read the source for git status in both libgit2 and gitoxide, and had AI research every part of it on top of my own research. And run experiments too, which let me try far more implementations than I could have on my own. For the file tree traversal alone, I think I tried up to ten different ways of doing it, to see which was the fastest and the most robust.
For this feature I made a separate small repo with all sorts of test fixtures and throwaway code, separated from the code of Git Cherry Tree. There I could experiment as much as needed. I created small pieces of a solution. The best way to allocate memory, the best way to have threads write to one data structure. And so on.
These pieces then got assembled into a working system, in this case a git status. Git status in particular has many moving parts, and even more edge cases. I have a collection of 25-ish git repos on my PC, in all sorts of weird and misconfigured states:
- Unity and Unreal games
- Git LFS repos
- Huge public repos
- The linux kernel (also misconfigured with wrong case sensitivity in different folders!)
- Submodules, repos with conflicts, and more.
Real repos that get used can catch things tests cannot - for example, the packing and repacking of the Linux repo after months makes it slower than a fresh clone, and repacking doesn’t help. This is hard to diagnose, let alone reproduce in a test.
I got confidence in the feature by:
- Running it against all those repos.
- Making a few more test repos that cover adversarial test cases for that feature
- Showing it to people
- Adding it to the client, and using it for a few weeks
- Having some friends use it too
Lastly, I kept the old git status working in parallel with the new one. At first, I ran them both in the debug build for a few weeks. Then I used only the new status for display, but the old one was used by write operations. If they disagree, the write operations still rely on the old working code. This way I kept the performance gains where I needed them most, and write ops were a little slower for a while.
I kept the old git status around for almost 6 months before I did some last double checks and removed it. The decision to finally remove it was made when I checked that it hadn’t been causing issues, and by checking that every possible way in which it was used could not break later code.
I also wrote a post about the git status work (and a part two!) and published the code. And I had contributed an entire upstream PR to gitoxide, improving the performance of their own implementation, before removing the checks on my own git client!
Additionally, when working on committing and checking out files, I found a bug in gitoxide where in some cases the index would fail to get saved. I wrote a fix myself first, then created an issue which was later fixed upstream.
Day to day work
I think it’s important to use your own software. Fortunately, I made Git Cherry Tree to solve issues with my daily workflow, so I use it every day, all day.
I note any ux papercuts, issues, bugs I might come across, on my todo list so I don’t forget. I also often run the development version which is unoptimised and so much slower, which I use to make sure performance is ok - if it’s fast enough here, it’s usually a few times faster in the release build!
I have a few other people also using the dev build so they can find any issues and give feedback too.
A slow release cycle also helps. After I make a feature or a few features, I give it a few weeks to see if any issues surface. I worry about releasing something too early and having missed something, so this helps.
The AI Section
Alright, let’s talk about AI.
Most code in Git Cherry Tree has been typed by AI.
AI is better at typing than me, it doesn’t make small local mistakes, and puts much more effort into each line of code than I could. It can make more global mistakes and logic errors, and all of my efforts go into catching those. Perhaps this makes us a team.
I also don’t subscribe to this rosy idea that I am the gifted artist/master programmer with excellent taste, directing the AIs/mere tools to enact my creative vision, and so it shall remain in harmony forevermore.
I do not feel creative. Working on these systems needs discipline, drive, grit and rigour. I take the best prior art, trial individual components, assemble them into a solution based on known principles, and test it extensively. This seems more like systematic engineering, and the many small tasks making up the whole is something AI does increasingly well.
Trusting AI Code
I worry about code written by AI. I also worry about code written by me. Most of this blog post is about ways in which I try to get confidence that my code, or the AI’s code for that matter, is not filled with bugs.
Some code is easy to verify - UI can be checked just by looking at it.
Some code is very hard to verify. You write a (correct!) system which updates the index file. It seems to work. Days later, it turns out there is an undocumented behaviour in the library you are using, which causes the tree extension in the index file to not be updated, which corrupts your index, in such a way that it triggers when you revert commits, and no client I’ve checked nor the Git CLI can even show you that something is wrong, until you’ve ruined your history. I still, somehow caught and reported this, and now the behaviour is documented upstream.
This seems like it’s the same regardless of who typed the code. If I had typed it myself, I think I would have made the same mistake. And likely more besides.
When I wrote code by hand I used to make these elaborate diagrams (in miro!) with code snippets in boxes and arranging them together until I had confidence the design would work. Then I typed that up.
I also wrote enormous comments at the top of the file planning out what I would do in notes and pseudo code, and then typing that up. Now, I take those notes and ask the AI to type that up. The notes are also more detailed, since I asked an AI to review them, find issues and so on, so they are more like plans.
The result is faster, more rigorous and more reliable than when I did all that by hand. The amount of scrutiny applied to each line of code is several times greater, and I don’t spend time figuring out minor bugs like some typo.
Overall, I find that compared to typing code by hand, having AI lets me reach beyond my means in terms of expertise and ambition.
AI bug sweeps
A thing I do often is ask the AI to review the code base and find bugs, duplicated code, opportunities for simplification, (functions which could be inlined!) and so on. I used to run this every day, sometimes a few times a day. It came back with a few items. Maybe a third was actual issues, the rest was either fine, or was intended behaviour.
It was important to review the review. Lots of the bug reports were correctly working behaviour presented as an issue or edge case, when really that edge case behaviour was correct.
An interesting thing is that over time the AI runs out of issues. It starts to find fewer of them and more items are just moving code around or would create bugs instead. And then a new model comes out, and it is better at finding issues so it can find more of them again.
I found this to be helpful since it incrementally squeezed the codebase down, and prevented it from getting too fat as tends to happen with codebases where you write loads of features. I’ve observed the same thing happen with our (human written) codebase at work, though with AI you can write much more code, and so get there much faster.
I find that 30% of my time is thinking, 60% is cleanup, and just 10% is writing new code. The 60% cleanup time is what keeps the code squeezed, and I find velocity a year later is as good, or faster, than when I started. Most of the client is written like that.
Working with AI over time
How I worked with AI also changed over time. Early on I was needing to look at very small instructions and work in small blocks of code, while designing the larger systems and features entirely in my head.
Over time the scope of a task I can give to AI has increased, particularly in the last few months. It is now possible for the AI to do a bunch of research in lots of topics, and an initial implementation or some experiments in one go. This is helpful since I can check what the actual code will look like before deciding if it’s a good idea or not. After some rewrites and iterations it can quickly become a good feature.
I used to commit myself (we are making a git client after all), and squash and rearrange history as I go. I like to make small commits, and then squash them down into larger ones later. Recently though I ask the AI to make commits which helps run larger workflows at a time.
I read every diff, and do that inside Git Cherry Tree, which I find to be nicer than any code editor or AI harness. It got to the point where I look at the client more than I look at an IDE! In some sense it’s funny, I stopped looking at the code but I still look at the code.
There is this increasing amount of work that the AI does in one go. I have some sense of unease, developed over the last weeks and months, and I’m not sure where that is coming from. Perhaps it’s due to code I didn’t write. AI code to me feels like coworker written code - I have teammates at work who write stuff. I review it, read the diffs, somehow it works, but I don’t exactly know how it works. Some amount of that is in Git Cherry Tree, and I don’t like it.
But also it could be that I did all the nice code first which is strong and straightforward, and the sad code like all the edge cases and git LFS integration is the code I’ve been working on recently. So maybe there is that. I had a similar feeling when I needed to make my custom git status work with every last edge case.
Making something in one go
I find that nothing comes out perfect on the first go. Every feature needs lots of iteration, testing, number twiddling, sometimes reverting and doing something else. This is also true of AI code.
I did have an exception recently I wanted to share. A few weeks ago after getting my PR merged into gitoxide, I idly decided to plan out what a better git status would look like. I had been gathering thoughts about it over the last 6 months, and wanted to see how many weeks of work it might take. I gave AI my current code, notes on all my ideas, asking it to investigate each of several items on the list that I had questions about.
It did all that, and wrote up an implementation plan. But then without stopping, it also went and implemented it, all in one go. And the result appears to be a from scratch rewrite (of my from scratch rewrite) of git status on windows which is faster than anything else, including the stuff shipped in Git Cherry Tree today. Also it’s more correct, if the above wasn’t enough.
I have many thoughts about that. This was a surprising, worrying, and exciting moment. The status implementation is currently sitting in its own little test repo, and I will need to think about what to do with it. That is a story for another time, but I wanted to share this.
A finished piece of software
How Git Cherry Tree is written has changed over time. Early on, I was trying to prototype out the user experience and some drag and drop interactions, which were the main thesis for the client. And then I realized that I could create a very high quality piece of software, which is much faster than what is out there.
And over time, a lot of the codebase has crystallized and is not changed much anymore. This isn’t because I’m afraid to rewrite the code. In fact, I’ve done so much code rewriting, I think every system is in its third iteration or something. But at some point, I don’t know what I would rewrite the code to.
I ask myself, if I was to start again from scratch, how would I do it? And then I do that. And if the answer changes over time, then I add it to the list to rewrite that piece.
A lot is stable. The core code and architecture, working with the repo: I think it’s finished. Now I am working on peripheral systems: UI, UX, improvements to various things. I also think that this is a piece of software that I will finish. I quite like the idea of finishing your code.
Also: There are so many systems to improve, rewrite, and add. None of these changes are critical, but they should take Git Cherry Tree to another level. It wouldn’t do to spoil all the surprises, but there are things on my list I am rather excited to work on.
I feel like Git Cherry Tree is pretty much done. I feel like I could still work on this for many years.
And that, I suppose, is how Git Cherry Tree is made.
Enjoyed this? Get notified about Git Cherry Tree updates: