How We Built Undo
Table of Contents
Tower 4.0 on the Mac introduced a special feature: undo is now available in our Git client! Seeing as we're far from the first application implementing undo, you may ask yourself what's so special about being able to hit CMD+Z in Tower? Given that actions in Tower can perform quite complicated Git operations under the hood, there's more to the feature than meets the eye. A lot of work went into making undo feel intuitive while keeping the user's work and data safe. In this article, we'll give you a look behind the curtains of Tower development, describing how the undo feature was conceived and implemented.
The Idea
As mentioned, the idea of an undo feature is not novel, and the idea did lay on our roadmap for quite some time. Before moving to a subscription-based model, we tried to align big features with major releases, and undo was originally considered for Tower 3, but did not make it in. Nowadays, bigger features don't depend on major releases, but the undo feature was still a bit unusual for us in terms of scope and the time it took to implement. We generally try to add things in shorter sprints but development of the undo feature was spread over a long period of time. We also wanted to deliver a complete, polished feature, not release it piece-by-piece.
For some actions in Git, the expected outcome after undoing is obvious, and reversing the action is easy. For others, the outcome is less clear and implementation is complicated. From the very beginning, work on this feature consisted of implementing the obvious cases, trying them out in real-world scenarios and seeing if they felt and worked as expected, while iterating on the more complicated cases to make sure they were robust and felt as natural and intuitive as the simple ones.
Starting Work on Undo
Work on the undo feature proceeded intermittently — while the first commits for the feature happened back in March 2017, the bulk of the development came later, while other work took priority in the meantime. Alex, our CTO, and Heiko, our Mac developer, began with a proof-of-concept, implementing undo for some actions where the desired state after undoing is clear and simple — undoing a "delete branch" action, for example. The implementation of Git operations as discrete actions in Tower allowed us to work on undo one case at a time, implementing and tweaking it for one action without affecting another. The feature was actually shipped with Tower, but disabled for end-users through a developer flag. This allowed us to use and test the feature internally in our own daily work for a long time.
Being able to test the feature in our daily work was useful for a couple of reasons. For one, we wanted to find out whether the feature really was useful — would we actually use it? Also, we had to find the sweet spot for each action: what felt right, what felt wrong, and was there anything we expected to be able to undo, but weren't? As mentioned, in some cases deciding on the correct outcome and implementing undo was relatively simple, in others, decidedly less so.
As work proceeded on other aspects of Tower, undo was taking shape as well. It generally seems to be the case that one underestimates the complexity of upcoming work, and so it was in this case: while we knew the feature would come with its challenges, perhaps we didn't realize exactly how challenging it would be. In the words of Hofstadter's Law, "It always takes longer than you expect, even when you take into account Hofstadter's Law".
Challenges
There's a wide gap between implementing a feature as a gimmick and actually making it feel natural, intuitive and solid. In Tower, actions happen asynchronously relative to the UI, so right away, we had the challenge of making sure the undo feature could handle this — for example, repeatedly undoing and redoing some action could not result in something breaking or data being lost.
In order to undo some more complicated actions, we had to save additional data for each step undertaken by the user, so that we would have all the information necessary to get back to the previous state in case the user decided to undo. An example of this occurs with some operations involving the working tree. The working tree is complicated, with many different potential states for files: files can be modified, staged, they can have merge conflicts and so on.
Rebases and Merge Conflicts
As a concrete example, let's say you're rebasing a branch on another branch. As your commits are being replayed on the target branch, during one of them, you get merge conflicts in multiple files. At this point, Git (and Tower) will stop to let you resolve the merge conflicts. Here, there are several interesting use cases for undo. A user might resolve several of the files with merge conflicts, only to find that one of the files was not resolved in the correct way, and then decide to undo to get back to resolving that file. In addition, the user might resolve the merge conflicts for that commit and either abort or continue the rebase, only to want to undo and go back to the previous rebase step.
The solution for both of these scenarios involved implementing snapshots for the working tree. As the user acts on the repository, these snapshots capture the state of the working tree along each step and allow Tower to back up when undoing. In the case of undoing a resolved merge conflict for a single file, undoing essentially just means going back to a previous snapshot. When continuing or aborting the rebase and then undoing, there's not really a way in Git to get back to the previous state, Here, Tower has a mechanism for replaying the steps previously taken while rebasing (using the snapshots), to get back to the state before undoing. There were a few options for how this could have been implemented — we could have re-used the commits created as the user performed the rebase sequence, and restored the state down to the actual commit hashes after undo, but that would have meant tracking the state of the rebase kept by Git in internal files. This is something we avoid, as this is an implementation detail and can change — Tower only uses the same Git commands that are available to end-users. The solution used, with snapshots and replaying of these, doesn't result in the same commit hashes, but the content of those commits is the same.
There's another scenario to keep in mind when restoring some previous state: it's possible for a user to do an action in Tower, then work with the repository on the command line, then go back to Tower and try to undo the last action done here. This may be a somewhat unlikely scenario, but still may not result in breakage or data being lost. In the kind of workflow described above, this is handled by the fact that the snapshots are implemented using patches — if there are conflicting changes, the patches simply won't apply.
Bringing the Feature into the Real World
As mentioned, during work on this and other features, Alex and Heiko were able to test and develop the undo feature, making sure it performed as expected. Undo is (hopefully) not a feature you use every ten minutes in Tower, but when you do reach for it, it is highly important that it feels natural and robust. For this purpose, real-world testing and iteration was key — the critical parts of the feature are unit-tested, but it's hard to write tests for how intuitive a feature feels to use.
What we found was that the feature did come in handy — Alex recalls a lightbulb moment of mistagging a commit and realizing that, instead of going to find the tag, deleting it, and creating a new one, he could just hit CMD+Z. As the snapshots for the working tree were implemented and used in the more complex flows, these, too, fell into place.
Beta And Release
In December 2019, the feature hit real-world testers in the Tower beta version. This went fairly undramatically — some bugs were found that actually turned out to originate in other parts of the code, but were exposed by undo. In the beginning of February 2020, the feature was released in Tower 4.0 — to the delight of many users, judging by the feedback. Seeing as undo is such a simple operation from the user's point of view, but can have such wide-varying effects on the underlying Git repository, the way it all comes together makes it quite satisfying to use.
We've found it to be useful in our day-to-day work to be able to quickly go back from merging the wrong branch, making an erroneous commit or deleting the wrong file, for example. There are many other situations - have a look at the full list of actions you can undo, if you haven't already. Of course, we use Tower internally, and developing Tower involves a great deal of messing around with Git repositories to test it out — in these scenarios, the feature is very handy indeed! All in all, we are very happy with the feature and glad that our users seem to enjoy it too!
The Bigger Picture
In our work on Tower, we constantly strive to make our users more productive, providing access to the powerful features of Git in the easiest, most intuitive way possible. In addition, one of the benefits of a graphical user interface like Tower's is discoverability — you may come across a feature that you didn't know existed. The command-line is very powerful and has its benefits, but discoverability is not necessarily one of them.
The undo feature provides a perfect example of how this vision is implemented. Depending on the context, the simple act of undoing can result in a wide variety of operations on the underlying Git repository. The feature is useful for beginners as well as for Git veterans, and it's as discoverable as can be. You can use it without stopping to consider what goes on behind the scenes, and hopefully you will nevertheless find that it does what you expect, every time!
Epilogue
There is a nice coda to the story of undo. When Alex and Heiko started looking at enabling partial stashing in Tower, they figured it should be easy, as Git supports partial stashing since 2.13.2. However, it turned out that the behavior of Git in this case was not quite what we expected and hoped for. Nevertheless, the work on snapshots for the undo feature allowed us to work around this issue, implementing a feature which might not have happened if it wasn't for the earlier work on undo! Partial stashing was released in Tower 4.3.
Join Over 100,000 Developers & Designers
Be the first to know about new content from the Tower blog as well as giveaways and freebies via email.