(Aug. 6, 2019): Added the "What
git-reviseis not" section.
At Mozilla I often end up building my changes in a patch stack, and used
rebase -i1 to make changes to commits in response to review
comments etc. Unfortunately, with a repository as large as
git rebase -i has some downsides:
It's slow! Rebase operates directly on the worktree, so it performs a full checkout of each commit in the stack, and frequently refreshes worktree state. On large repositories (especially on NTFS) that can take a long time.
It triggers rebuilds! Because rebase touches the file tree, some build systems (like gecko's recursive-make backend) rebuild unnecessarially.
It's stateful! If the rebase fails, the repository is in a weird mid-rebase state, and in edge cases I've accidentally dropped commits due to other processes racing on the repository lock.
It's clunky! Common tasks (like splitting & rewording commits) require multiple steps and are unintuitive.
Naturally, I did the only reasonable thing: Build a brand-new tool.
git-revise is a history editing tool designed for the patch-stack workflow.
It's fast, non-destructive, and aims to provide a familiar, powerful, and easy
to use re-imagining of the patch stack workflow.
I would never claim to be a benchmarking expert 3, but
git-revise performs substantially better than rebase for small history editing
tasks 4. In a test applying a single-line change to a
commit 20 patches up the stack I saw a 15x speed improvement.
$ time bash -c 'git commit --fixup=$TARGET; EDITOR=true git rebase -i --autosquash $TARGET~' <snip> real 0m10.733s
$ time git revise $TARGET <snip> real 0m0.685s
git-revise accomplishes this using an in-memory rebase algorithm operating
directly on git's trees, meaning it never has to touch your index or working
directory, avoiding expensive disk I/O!
git-revise isn't just a faster
git rebase -i, it provides helpful commands,
flags, and tools which make common changes faster, and easier:
$ git add . $ git revise HEAD~~
git revise $COMMIT directly collects changes staged in the index, and
directly applies them to the specified commit. Conflicts are resolved
interactively, and a warning will be shown if the final state of the tree is
different from what you started with!
With an extra
-e, you can update the commit message at the same time, and
will stage your changes, so you don't have to! 5
$ git revise -c $COMMIT Select changes to be included in part : diff --git b/file.txt a/file.txt <snip> Apply this hunk to index [y,n,q,a,d,e,?]?
Sometimes, a commit needs to be split in two, perhaps because a change ended up
in the wrong commit. The
--cut flag (and
cut interactive command) provides a
fast way to split a commit in-place.
git revise --cut $COMMIT will start a
git add -p-style hunk
selector, allowing you to pick changes for part 1, and the rest will end up in
No more tinkering around with
edit during a rebase to split off that comment
you accidentally added to the wrong commit!
$ git revise -i
git-revise has a
git rebase -i-style interactive mode, but with some
quality-of-life improvements, on top of being fast:
Implicit Base Commit
If a base commit isn't provided,
--interactive will implicitly locate a safe
base commit to start from, walking up from
HEAD, and stopping at published &
merge commits. Often
git revise -i is all you need!
Staged changes in the index automatically appear in interactive mode, and can be moved around and treated like any other commit in range. No need to turn it into a commit with a dummy name before you pop open interactive mode & squash it into another commit!
Bulk Commit Rewording
$ git revise -ie
Ever wanted to update a bunch of commit messages at once? Perhaps they're all
missing the bug number? Well,
git revise -ie has you covered. It'll open a
special Interactive Mode where each command is prefixed with a
++, and the
full commit message is present after it.
Changes made to these commit messages will be applied before executing the TODOs, meaning you can edit them in bulk. I use this constantly to add bug numbers, elaborate on commit details, and add reviewer information to commit messages.
++ pick f5a02a16731a Bug ??? - My commit summary, r=? The full commit message body follows! ++ pick fef1aeddd6fb Bug ??? - Another commit, r=? Another commit's body!
$ git revise --autosquash
If you're used to
git rebase -i --autosquash, revise works with you. Running
git revise --autosquash will automatically reorder and apply fixup commits
git commit --fixup=$COMMIT and similar tools, and thanks to the
implicit base commit, you don't even need to specify it.
You can even pass the
-i flag if you want to edit the generated todo list
before running it.
git-revise doesn't touch either your working directory, or your index. This
means that if it's killed while running, your repository won't be changed, and
you can't end up in a mid-rebase state while using it.
Problems like conflicts are resolved interactively, while the command is
running, without changing the actual files you've been working on. And, as no
files are touched,
git-revise won't trigger any unnecessary rebuilds!
git-revise is not
(Section Added: Aug. 6, 2019)
git-revise does not aim to be a complete replacement for
git rebase -i. It
has a specific use-case in mind, namely incremental changes to a patch stack,
and excludes features which
In my personal workflow, I still reach for
git rebase [-i] when I need to
rebase my local commits due to new upstream changes, and I imagine there are
people with advanced workflows who cannot use
Working directory changes:
git-revise does not modify your working directory or index while it's running.
This is part of what allows it to be so fast. However, it also means that
certain rebase features, such as the
edit interactive command, are not
This also is why
git revise -i does not support removing commits from within a
patch series: doing so would require changing the state of your working
directory due to the now-missing commit. If you want to drop a commit you can
instead move it to the end of the list and mark it as
index. The commit will
disappear from history, but your index and working directory won't be changed. A
git reset --hard HEAD will update your index and working directory.
These restrictions may change in the future. Features like this have been requested, and it might be useful to allow opting-in to dropping commits on the floor or pausing mid-revise.
Merging through renames & copies:
git-revise uses a custom merge backend, which doesn't attempt to handle
file renames or copies. For changes which need to be merged or rebased
through file renames and copies,
git rebase is a better option.
Complex history rewriting:
git rebase supports rebasing complex commits, such as merges. In contrast,
git-revise does not currently aim to support these more advanced features of
git-revise is a MIT-licensed pure-Python 3.6+ package, and can be installed
$ python3 -m pip install --user git-revise
You can also check out the source on
GitHub, and read the
manpage online, or by
man git revise in your terminal.
I'll leave you with some handy links to resources to learn more about
git-revise, how it works, and how you can contribute!
- Repository: https://github.com/mystor/git-revise
- Bug Tracker: https://github.com/mystor/git-revise/issues
- Manpage: https://git-revise.readthedocs.io/en/latest/man.html
- Installing: https://git-revise.readthedocs.io/en/latest/install.html
- Contributing: https://git-revise.readthedocs.io/en/latest/contributing.html
git cinnabar, as I'm more comfortable with it, despite the official repos being mercurial. ↩
280268 files, according to
git ls-files | wc -l. ↩
I know my sample size of 1 sucks, though ^_^ ↩
On my system, at least. I'm running Fedora 30 on an X1 Carbon (Gen 6) ↩
--all) flag will impact the index (due to files being staged), unlike other commands. ↩