Working with Stacked PRs using git-branchless, git-autofixup, and git-pr

I usually work with stacked PRs. It is a great way to organize my work. I was using sapling and it works really well. I can view stacked commits with sl smart logs, edit many files and make changes to many commits at the same time with sl absorb, have the stack automatically rebase after each change, and push these commits as multiple stacked GitHub PRs with a single sl pr command. It’s great! I was very happy with sapling. You can read more about it in my last article.

But the honeymoon ended when I wanted to push a stack with 17 commits to GitHub. I reached GitHub’s rate limit after creating 10 PRs and got a temporary ban:

$ sl pr 2>&1 | go run ~/ws/conn/be/go/scripts/slpr
pushing 17 to
created new pull request:
created new pull request:
abort: error creating pull request for f05689dca505b3ad1b526a220d7a15f46b4a9511: {
 "message": "You have exceeded a secondary rate limit and have been temporarily blocked from content creation. Please retry your request again later.",
 "documentation_url": ""

This left my sapling repository in a bad state. The local commits and the GitHub PRs no longer match. Even after the ban lift, I could not push the remaining commits to GitHub anymore:

$ sl pr
pushing 8 to
abort: `git --git-dir /Users/i/ws/conn/be/.sl/store/git push --force 447c5d073cbadd4bcc251bf8bcd46d9ec4f728bd:refs/heads/pr3320 3f0d1e3103e5246e29806f44b87f4e9289749202:refs/heads/pr3320 7403ecce2590066177a23923bbd509598fe32781:refs/heads/pr3321 8750722250395b8d7e5a2e624a5c65a42ee817e0:refs/heads/pr3322 b543123ebadcca0b1de95293fd551752e1fa0c43:refs/heads/pr3323 263fde607355872bb47305168bf1907d673b0249:refs/heads/pr3324 5b590681dbe1394b7cae9a7d1e9f823205da3cc7:refs/heads/pr3325 17137b286aa5376615a77e58f3ef71bf02a3398f:refs/heads/pr3326` failed with exit code 1: stdout:
stderr: error: dst ref refs/heads/pr3320 receives from more than one src
error: failed to push some refs to ''

The problem is that I do not know the internals of sapling enough to be able to fix it. I had to switch back to git and manually pushed the remaining commits to GitHub. So I decided to bring the sapling’s workflow to git with git-branchless, git-autofixup, and writing my own command, git-pr. Together, they work really well with my stacked PRs workflow. And I can use my Git knowledge to fix problems if they happen.

To see how working with stacked PRs in sapling and in git-branchless/git-autofixup/git-pr, let’s come back with the last example from developing the user sign-up feature: user inputs their email address, password, and we send them a nice welcome email. From the backend perspective, we need to create a new user, verify that the user doesn’t exist, and send the email. We need to touch the implementation of the cache package, add an email package, then finally implement the sign-up logic. This can be represented as a stack of PRs:

The stacked PRs workflow with sapling

Here are my most use commands while working with sapling:

1. View the stacked commits and PRs

$ sl
o bf31e38d1 Today at 04:14 remote/main
│ @  00f1749f6  30 minutes ago  oliver
│ │  implement user signup
│ │
│ o  e0dbbc80e  50 minutes ago  oliver
│ │  implement email package
│ │
│ o  4f6928029  Yesterday at   oliver
├─╯  update cache package

We have a stack with 3 commits.

2. Edit a commit message with sl metaedit

sl metaedit 4f6928029

Update the commit message for the cache commit. And automatically rebase all the commits above.

3. Make changes to multiple commits with sl absorb

sl goto 00f1749f6              # checkout the sign up code
vim features/signup/signup.go  # make changes to signup package
vim lib/email/email.go         # make changes to email package
sl absorb                      # magic 👻

With a single command sl absorb, the signup.go changes will be amended to the signup commit, the email.go changes will be amended to the email commit, and the commits will be automatically rebase onto each other.

4. Rebase a stack of commits with sl rebase -s

sl rebase -s 4f6928029 -d remote/main

The stack will be rebased onto remote/main.

4. Undo mistakes with sl undo

sl undo

5. Push all commits and create stacked PRs with sl pr

$ sl pr
pushing 3 to
created new pull request:
created new pull request:
created new pull request:

Push all the commits the remote repository and associate one PR for each commit.

Back to git world

Now let’s see how can we achieve similar results with git. We will need to install a few things: github-cli, git-branchless, git-autofixup, and git-pr.

  • Install github-cli with brew install gh then run gh login.
  • Install git-branchless with cargo install --locked git-branchless then run git branchless init in your repository.
  • Install git-autofixup by downloading the binary and put it in your $PATH.
  • Install git-pr and put it in your $PATH:
    git clone
    cd git-pr
    go install .
    export PATH=$PATH:~/bin/go  # put git-pr in your $PATH

View stacked commits with “git sl” (git-branchless)

After running git branchless init in your repository, you will have access to a few useful commands to manage stacked commits. Let’s start with git sl to view the stack:

$ git sl
◇ bf31e38 3d (main) update something on main (#3102)
◯ 4f69280 1d update cache package
◯ e0dbbc8 50m implement email package
● 00f1749 30m (oliver/signup) implement user signup

Notice that I still create a branch oliver/signup to point to the last commit in the stack.

Restack commits with “git restack” (git-branchless)

Let’s make some changes to the cache commit:

$ git checkout 4f69280    # check out the cache commit
$ git commit --amend      # make some change to the commit message
$ git sl
◇ bf31e38 3d (main) update something on main
┃ ✕ 4f69280 1d (rewritten as a9ca4ac) update cache package
┃ ┃
┃ ◯ e0dbbc8 50m implement email package
┃ ┃
┃ ◯ 00f1749 30m (oliver/signup) implement user signup
● a9ca4ac 1m update cache package 

Now run git restack to fix the stack:

$ git restack
Attempting rebase in-memory...
◇ bf31e38 3d (main) update something on main (#3102)
◯ a9ca4ac 2m update cache package
◯ b6d516d 0s implement email package
● 8a40bd4 0s (oliver/signup) implement user signup

All the commits after the cache commit will be rebased on the previous one, with the branch name pointing to the last commit. Compared to sapling, these commands achieve a similar effect as sl metaedit or sl amend.

Make changes and automatically restack with “git amend” and “git reword” (git-branchless)

The above can be simplified by using git amend and git-branchless will automatically rebase subsequent commits. You can also edit a commit message (and automatically rebase) with git reword:

git amend
git reword a9ca4ac

By using these commands provided by git-branchless, we do not need to work with branches anymore. And git-branchless with take care of all the rebasing for us.

Make changes to multiple commits with “git autofixup”

Let’s make some changes:

git checkout oliver/signup     # checkout the sign up code
vim features/signup/signup.go  # make changes to signup package
vim lib/email/email.go         # make changes to email package

Now, add the changes as fixup! commits. You will notice that there are 2 new commits with the fixup! prefix. They are associated with the corresponding original commits and will be used to update them later.

$ git add .
$ git autofixup origin/main
$ git sl
◇ bf31e38 3d (main) update something on main (#3102)
◯ a9ca4ac 4m update cache package
◯ b6d516d 2m implement email package
◯ 8a40bd4 2m implement user signup
◯ 5f4da9b 1m fixup! implement user signup
● c606fe9 1m (oliver/signup) fixup! implement email package

Finally, run git rebase --interactive --autosquash to absorb the changes into the original commits:

$ git rebase --interactive --autosquash origin/main
$ git sl
◇ bf31e38 3d (main) update something on main (#3102)
◯ a9ca4ac 6m update cache package
◯ 5bdd29e 1m implement email package
◯ aa025d1 1m implement user signup

Together, these commands achieve the similar effect of sl absorb.

Undo mistake with “git undo” (git-branchless)

git undo

Push all commits and create stacked PRs with git pr

When everything is ready, let’s run git pr to push all the commits to GitHub. A PR will be created for each commit. They will be stacked onto each other:

git pr

Here’s how a PR in the stack will look like:

  • A review link, which reviewers can click to access the corresponding commit and add comments.
  • A list of all PRs for that stack.

Bonus: Some useful aliases

Here are a list of my frequently used aliases:

alias g='git'
alias gg='git sl'             # smart log
alias gs='git status'
alias gp='git push -u'
alias ga='git add'
alias gaa='git add -A && git amend'
alias gcm='git commit'
alias gco='git checkout'
alias gcp='git cherry-pick'

# checkout the main branch
alias gmain='git checkout "$(git_main)"'

# checkout the latest commit from the main branch
alias gm='gco $(gg | grep "main)" | grep -oE "[a-f0-9]{7}") ; gg'

# commit the current changes as fixup commits
alias gfix='git autofixup origin/main'

# rebase the current branch on the main branch, and squash fixup commits
alias grom='git rebase -i --autosquash "$(git_remote_main)"'

# pull, rebase, and update submodules
alias gu='git pull --rebase --prune $@ && git submodule update --init --recursive'

# checkout the main branch, pull, rebase, and checkout the main commit
alias gsync="gmain && gu && git sync && gm"


🎉 That’s it! Now we can work with stacked PRs easily within the git world, compatible with other familiar git commands. And when something goes wrong, at least we can still be able to fix it with our git knowledge and plenty of available tools.


I'm Oliver Nguyen. A software maker working mostly in Go and JavaScript. I enjoy learning and seeing a better version of myself each day. Occasionally spin off new open source projects. Share knowledge and thoughts during my journey. Connect with me on , , , and .

Back Back