-
Notifications
You must be signed in to change notification settings - Fork 31
Working on multiple dependent unmerged PRs at once
This is (amongst other things) is an alternative to the approach described here.
Stacked git is an external tool (on the command line it's called stg
) that works with Git to provide an alternative interface for work in progress. The idea is that you are working on one branch (e.g., develop
) which is the "official" state of the repo, while our changes in progress are a series of patches on top of the branch. We can easily edit, reorder, unapply, etc., each of these patches.
stg
stores its data inside the git repository, and must be initialized before use. This is done per branch, so start by having develop
checked out and run:
$ stg init
In contrast to normal git development we typically want to create the patch we're going to be working on before we make any changes (normally we make changes then commit
, here we essentially go the other way).
# -m is optional, it'll open your $EDITOR otherwise
$ stg new name-of-patch -m "Here's the eventual commit message"
After we've made some changes that we want to record we run refresh
:
$ stg refresh
# or equivalently:
$ stg refresh --patch name-of-patch
refresh
is fairly flexible, if you haven't git add
ed any files it'll record all tracked files that have changes, and if you've git add
ed all changed tracked files it'll record those. If you have a mix it will do nothing and suggest the --index
option, which makes it only record git add
ed files. Finally, it can also take a bunch of filenames as arguments, at which point it will record only those.
Note the --patch
argument, by default refresh
will record changes in the top-most applied patch, but it can record changes in other patches as well.
Once we have a few patches we can start manipulating the stack of patches. We can examine the stack with stg series
. Most normal commands you'd expect on a stack exist, e.g., push
and pop
, but also some conveniences, e.g., goto
and float
.
As an example, this is the output of me running stg series
at the time of writing:
$ stg series
+ comment-out-pi
> nest-proto-boot
- tokEq
- profile-boot-lib
- sfunc-tactics
comment-out-pi
is applied, nest-proto-boot
is applied and top-most, and tokEq
, profile-boot-lib
, and sfunc-tactics
are unapplied.
Eventually we will have a sequence of patches that we want to form a PR. This part of the workflow is the most inconvenient; I believe the tool is intended for a workflow where you submit patches, as opposed to PRs. This is the sequence of commands I tend to use:
$ stg pop --all
$ git checkout -b new-pr-branch
$ stg init
$ stg pick develop:patch-1..patch-n # This copies a series of patches from another branch and applies them
$ git push fork # This is my fork on GitHub, origin is the main repository
$ git checkout develop # I do all development on the patches on develop
If I end up needing to make changes I make them in the patches on develop
, then I update the branch:
$ git checkout new-pr-branch
$ stg pop -a # Pop all patches
$ stg branch --cleanup --force # Delete the patches and remove stg metadata
$ git reset --hard origin/develop # This tends to do nothing, but sometimes I've pulled develop in the meanwhile
$ stg init # Then I repeat the normal procedure
$ stg pick develop:patch-1..patch-n
$ git push fork --force # I need a force here since the commits are different
$ git checkout develop
I have little doubt there is a better way than this, so feel free to share if you figure something out.
stg
has a command pulls from upstream and updates your stack of patches to be based on the new position of the branch:
$ stg pull --merged
This will essentially pop all your patches, pull from upstream, then reapply your patches. --merged
tells stg
to try to detect if any of the patches in the series were merged upstream, in which case they become empty in your stack. At this point you can remove the empty patches:
$ stg clean
This is the part that solves the issue of working on multiple dependent PRs in sequence; the tool detects when they've been merged automatically, and you don't have to
When reordering patches or pulling upstream changes or the like, merge conflicts can appear. When this happens you have two options:
# Fix the merge conflict:
$ stg refresh # ...once you're happy with the resolution
$ stg goto patch-name # Go to the patch you actually want to be at, I don't
# know of an equivalent of `git rebase --continue` or the like
# Abort whatever you were doing:
$ stg undo # This works if your working directory is clean
# or
$ stg undo --hard # This works otherwise (e.g., when in a merge conflict)
stg undo
in general is pretty handy, thus far it's been able to undo every change I've wanted it to.
Inevitably when developing I end up in a situation where I'm part-way through a change, then I realize that there's something else that needs to be done before I can finish the current thing. In this case I just save my changes thus far, pop the patch, then do that "something else", then push the patch again:
$ stg refresh
$ stg pop
$ stg new something-else
# Do "something else"
$ stg refresh
$ stg push
At this point I'm back where I was, but the other thing is now done, and it ended up in a different commit, without particularly complicated juggling of branches or stashing.
One of the patches in the stg series
example earlier in this document (profile-boot-lib
) is a change that is useful (it adds profiling in a fairly pervasive way to boot
and compiled programs), but it's incompatible with the repository at large (it has an opam
dependency that can't be installed at the same time as something else) and so can't really be merged. This way I can store it, with a name, without otherwise cluttering up my branches or logs, and without needing to rebase things all the time (I still need to solve conflicts if needed when I apply it, but that's the only time).
The unapplied part of the stack becomes a pretty convenient named stash of changes.
stg undo
I believe works by using the log that stg
keeps. We can examine that log with stg log
, and revert to a previous state with stg reset
. I have yet to need the latter, but it's nice to know that all the intermediate refresh
es are stored somewhere, and I imagine it'll be helpful at some point.