NIKA:\git-revise\> list
(Aug. 6, 2019): Added the "What
git-revise
is not" section.
At Mozilla I often end up building my changes in a patch stack, and used git
rebase -i
1 to make changes to commits in response to review
comments etc. Unfortunately, with a repository as large as
mozilla-central
2, 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.
source: xkcd |
Introducing git-revise
!
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.
It's fast
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 mozilla-central
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!
It's handy
git-revise
isn't just a faster git rebase -i
, it provides helpful commands,
flags, and tools which make common changes faster, and easier:
Fixup Fast
$ git add .
$ git revise HEAD~~
Running 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 -a
will stage your changes, so you don't have to! 5
Split Commits
$ git revise -c $COMMIT
Select changes to be included in part [1]:
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.
Running 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
part 2.
No more tinkering around with edit
during a rebase to split off that comment
you accidentally added to the wrong commit!
Interactive Mode
$ 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!
The index
Todo
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!
Autosquash Support
$ 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
created with 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.
It's non-destructive
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!
What 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 rebase
supports.
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 git revise
.
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
possible.
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
quick 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 rebase
.
Interested?
Awesome!
git-revise
is a MIT-licensed pure-Python 3.6+ package, and can be installed
with pip
:
$ python3 -m pip install --user git-revise
You can also check out the source on
GitHub, and read the
manpage online, or by
running 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
-
I use
git
withgit 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) ↩
-
The
-a
(or--all
) flag will impact the index (due to files being staged), unlike other commands. ↩