If you're not sure how to take advantage of Graphite but your colleagues are raving about it, here's what I wish I was told from the beginning when we started experimenting with Graphite at Semgrep. I propose a workflow that makes it easy to revise previous commits, reduces conflicts, and avoids having unrelated changes in the same PR.
A stack of what?
A stack of pull requests isn't really a stack of pull requests, it should be treated as a stack of changes. Even though technically each change in the stack ends up being a GitHub pull request as we know it, a stack of changes functionally is the equivalent of one large traditional GitHub pull request implementing a new feature in multiple steps.
Here's how to think of a stack:
- A Graphite stack should be thought of as a sequence of changes necessary to deliver a new feature.
- Each change should be a single Git commit that gets amended over and over again.
Recommended flow
Creating a stack
Set the current branch to your repository's main branch as you would before when creating a branch destined for a pull request:
$ git checkout main
Then make changes to your code until it's mergeable or until you want to back it up. It's ok if it's not finished.
Then, commit your code as the first set of changes:
$ gt create
Useful options include -a
to add all files, -m
to set a commit message. The branch name can be specified as an argument if you don't like the name assigned by Graphite.
Resuming work on the last change
Say you finished your day of work by committing a change with gt create
. The next day, you'll resume the work by making edits until you're happy with your code. Now, instead of adding a new commit, you're going to modify the last commit by adding your new changes. Graphite offers the modify
command to do this conveniently:
$ gt modify -a
That's it. The main idea is that we keep each meaningful change as one Git commit that can be revised later.
Adding another change
Now, assume you need to make another preliminary change before delivering your feature. As before, you modify your code as usual. You then commit it as one Git commit. This commit will also be technically its own Git branch later associated with its own pull request. We will treat it as a change that we're free to revise later. The command, like earlier, is gt create
:
$ gt create -a -m 'Another preliminary change'
Use gt rename
to change the Git branch name if you want.
Editing an earlier change
At this point, you may have figured out that Graphite is based heavily on editing the Git history. This is what we're doing here. From Git's perspective, a stack of changes is a branch of a branch of a branch... From Graphite's perspective, it's a stack or sequence of changes that are meant to be reviewed and revised later in any order. Inspect the stack and your position in the stack with:
$ gt ls
Navigate the stack i.e. change the current git branch/commit with gt up
and gt down
until the current branch is the one corresponding to the change you want to edit. For example, go to the previous change with:
$ gt down
Now, edit your code and commit it as described previously, not by introducing a new commit, but by amending the commit:
$ gt modify -a
With this command, Graphite modifies the current commit and updates the subsequent changes using git rebase
under the hood.
Pushing the work to save it
As a backup measure, it's a good idea to push your code to GitHub or equivalent once in a while. You would do this for all the changes in the stack at once with
$ gt submit
This will create a pull request for each change in the stack, sending you to Graphite's Web interface. However, these pull requests are not definitive. If you're not ready to send them to review, you can either save them but not press the Publish button or you can publish them but mark some of them as drafts. There are buttons for these things. The draft feature is the same as GitHub's draft feature.
Reviewing and merging the stack
A stack of changes translates to a sequence of pull requests that can be reviewed either on Graphite's Web app or on GitHub. On GitHub, there's a CI check that sort of prevents the reviewer from merging a PR if its dependent PRs corresponding to changes down the stack haven't been merged yet.
Synchronizing with the main branch
To import the latest changes of the main branch into your stack, use
$ gt sync
It's equivalent to a Git rebase for each of your changes. You'll have to resolve conflicts the usual way, by following the instructions. This is where it helps to have just one commit per logical change in the stack.
Running other Git operations
You can still use the usual Git commands but some are deprecated when working on a Graphite stack. git commit
can be fine but unnecessary since you'd usually use gt create
or gt modify
instead. git push
may be fine too but gt submit
will push all the branches corresponding to your stack at once. Rewriting history with git rebase
is probably best avoided if you're working on a Graphite stack. git merge
and git pull
should also be avoided since gt sync
will handle synchronization and conflict resolution for the whole stack.
Read-only operations such as git log
are of course fine and recommended in the same situations as with traditional Git/GitHub workflows.
Clean-up
Graphite will offer spontaneously to delete merged branches (unlike Git when merging a branch on GitHub with squash-and-merge).
Important take-aways
- Don't worry about planning your work ahead of time. You don't have to. Keep editing your code by amending the last commit with
gt modify
until your code works and you're comfortable moving on to something else... which will be a new change in the stack. You can always go back and edit earlier changes down the stack later. - Embrace editing the Git history.
gt ls
/gt up
/gt down
/gt modify
does it for you. - Minimize the number of commits to facilitate conflict resolution. This is achieved by the recommended flow above where you go extend previous commits.
The benefits that you should get out of this approach are:
- easier conflict resolution due to having fewer commits
- easier code reviews due to having one pull request per meaningful change
Open questions
Review multiple changes separately but merge them as one?
CI checks will need to pass for each change/commit/PR in the stack. Often, it makes sense for two changes to be reviewed separately even though they need each other for the software to work. Since merging only one of these commits would break the build and fail CI checks, is there a way to submit these two changes for review separately but require them to be merged as one?
Why is it called a stack?
In computer science, a stack or last-in-first-out (LIFO) container is a collection of elements where only the most recently added element is meant to be accessed. This is not exactly the meaning used by Graphite which can be confusing to some of us. Even to ordinary pancake enjoyers, the notion of a stack implies that modifying elements inside the stack is difficult and unusual*. Graphite makes it easy and pleasant to modify the items in the stack, making it feel less like a stack.
*a solution to add butter to each pancake before they get cold involves reversing the stack rather than trying to insert butter between pancakes.
Disclaimer
I'm still new to this. Expect some inaccuracies and oversights.
Top comments (0)