Pushing a whole stack of branches with a single Git command : Andrew Lock
by: Andrew Lock
blow post content copied from Andrew Lock | .NET Escapades
click here to view original post

In this post I show how you can push a whole stack of branches with a single command. This is particularly useful when you're working with stacked branches, and want to push all the branches in the stack at once. In this post I show the git aliases I have created that make doing this a single, simple command.
What are stacked branches?
I'm a big fan of creating small git commits. Especially when you're creating a big feature. I like to create a "story" with my commits, adding bits of a larger feature commit by commit. The idea is to make it as simple as possible for others to review by walking through a commit at a time.
As an extension to that, I often create separate PRs for each couple of commits in a feature. This, again, is to make it easier for people to review. GitHub's PR review pages really don't cope well with large PRs, even if you have "incremental" commits. Creating separate branches and PRs for each unit of functionality makes it easier for people to consume and follow the "story" of the commits.
To be clear, creating small commits and PRs is often harder than just creating a big PR at the end. Nevertheless, I argue it's worth the effort. Small PRs are easier to review, therefore you're likely to get better reviews than for big commits. Also, it's just polite, as it optimizes the reviewer's time over your own.
This approach, where you have lots of separate branches/PRs which build on top of one another, is called stacked branches/PRs. This makes sense when you think of the git graph of the branches: each branch is "stacked" on top of the previous one.
For example, in the following repo I have 6 commits as part of a feature, feature-xyz
, and have broken those down into 3 logical units, with a branch for each. I can then create a PR for each of those branches:
For the first PR, for branch andrew/feature-xyz/part-1
, I would create a PR requesting to merge to dev
(in this example). For the second PR, for branch andrew/feature-xyz/part-2
, I would create a PR requesting to merge to andrew/feature-xyz/part-1
, and for the part-3
branch the PR would request to merge into part-2
:
Each PR only includes the commits specific to that branch, which makes for a much nicer reviewing experience.
I firmly believe working with stacked branches for medium-large features is the best way to work if you're optimizing for reviewability and for remaining unblocked. However, there's no denying working with stacked branches requires more Git finesse than typical single-branch workflows.
Pushing a whole stack of branches with a single Git command
One of the pain points typically comes up shortly after espousing stacked branches to my teammates. The problem is how to handle the case where you have made changes to multiple branches in a stack and you want to push all the branches in the stack to a remote repository. The sad truth is that I didn't have a good way until recently. My solution was simply doing something like this:
git push origin --force-with-lease feature/part-1;
git push origin --force-with-lease feature/part-2;
git push origin --force-with-lease feature/part-3;
Pretty ugly, but I just lived with it 😅
Actually, I had an alias
pof
configured to make this slightly less ugly usinggit config --global alias.pof "push origin --force-with-lease"
. That way I could typegit pof <BRANCH>
which at least saves some key strokes.
Recently I was working with a particularly big stack of branches, and this finally irritated me enough to actually look into it further. This was also partially inspired by a discussion in the comments section of another of my posts about stacked branches.
As a toy example, imagine we have the following stack of branches:
The repository currently has the following features:
- The default branch is
main
and it tracks the default branch on the default remoteorigin
. - There is a stack of three branches that make up the
feature/
stack. - The
feature/*
stack has not yet been pushed to the upstream. - The
feature/part-3
branch at the top of the stack is currently checked out.
Our goal is to push feature/part-1
, feature/part-2
, and feature/part-3
to the remote repository as simply as possible. We'll start by running git stack
, which simply lists which branches are part of the stack and will be pushed:
$ git stack
feature/part-3
feature/part-2
feature/part-1
This lists all of the branches to be pushed. This looks correct, so we run git push-stack
:
$ git push-stack
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 20 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (5/5), 1.14 KiB | 1.14 MiB/s, done.
Total 5 (delta 4), reused 0 (delta 0), pack-reused 0 (from 0)
To C:\repos\temp\temp69
* [new branch] feature/part-3 -> feature/part-3
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To C:\repos\temp\temp69
* [new branch] feature/part-2 -> feature/part-2
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To C:\repos\temp\temp69
* [new branch] feature/part-1 -> feature/part-1
This pushes all the branches in our stack to our remote repository:
And voila, it works! This basic functionality is what I've wanted for a long time, but there's a few extra "features" available for the command too.
Pushing part of the stack with git push-stack
We'll look in detail at how git push-stack
is implemented shortly, but first we'll look at a couple of different ways you can use it.
The simplest approach, git push-stack
you've already seen, and is used when you already have the top-most branch of the stack checked out, feature/part-3
in the example above.
If you only wanted to push part of the stack, you have a couple of options
- Checkout the top-most branch of the stack that you wish to push, or
- Explicitly specify the top-most branch of the stack that you wish to push
For example, imagine you only want to push feature/part-1
and feature/part-2
, but not feature/part-3
. You have two options:
# Checkout feature/part-2
git checkout feature/part-2
# List the branches that would be pushed
git stack
# Prints:
# feature/part-2
# feature/part-1
Alternatively:
# Specify the top-most branch explicitly
git stack feature/part-2
# Prints:
# feature/part-2
# feature/part-1
Which of these options is the most useful depends on what you're doing: if you already have feature/part-2
checked out, then the first option is easier, otherwise use the second option.
I have used
git stack
here simply to show what would be pushed. Usinggit push-stack
instead would directly push these branches instead of just printing the branches to push.
Another aspect to acknowledge is that git stack
"intelligently" determines which branches to push based on the remote default branch, i.e. origin/main
. So for example, imagine we now have the following local repository:
In this scenario
- The
feature/part-1
branch was merged toorigin/main
(likely via a pull request) and the remote branch has been deleted. - The local branch
feature/part-1
has not yet been deleted. - The local default branch
main
has not yet been updated to trackorigin/main
.
If we run git stack
in this scenario then we get the expected results:
# We don't need to specify the head branch, but specified for clarity
git stack feature/part-3
# Prints:
# feature/part-3
# feature/part-2
We only push the branches which need to be, and we rely on the remote default branch origin/main
for that calculation, rather than the local default main
.
I'm sure there's more features we could add, but this has been sufficient for me for now. For the rest of the post, I'll describe how you can create the git push-stack
command.
Implementing git push-stack
The git-push stack
command consists of four steps:
- Calculate the default branch (
origin/main
). - Calculate the "merge base" of the stack i.e. the bottom of the stack.
- Calculate the branches between the merge base and the tip of the stack.
- Push all the branches calculated in the previous step.
To make things easy to test and more composable, I created a Git alias for each of these steps, so we'll walk through them one by one.
Assumptions
Before we start, it's worth highlighting a couple of assumptions in these aliases:
origin
is the default remote.- You cloned the local repository from a remote, or you have set the default remote branch.
These days, the default remote is almost always called origin
, but it could be called upstream
or anything else. In the aliases I provide here, I assume that you're using origin
. Tweaking these to assume origin
by default but allowing you to change them would not be difficult, but I haven't done it, for simplicity.
That's mostly because I dislike having more than one positional-parameter—would it be
git stack origin mybranch
orgit stack mybranch origin
🤔—and it's not at all easy to have named parameters in git aliases as far as I know. I'm happy to share updated scripts if this is something you do want.
The second point won't normally be a problem. If you cloned a repository from GitHub for example, then you're probably fine. However, if the remote does not set it for some reason, you can set the default branch using:
git remote set-head origin --auto
I discuss this more in the following section, so let's look at the various steps we need to create the git push-stack
command.
Calculating the default branch
When you create a new Git repository, whether locally or on GitHub, you need to specify the default branch. This was master
for a long time, but these days it's typically main
. Technically though it could be anything. Rather than make assumptions, I have a command that tries to determine the actual default branch used by the remote repository.
This isn't revelatory, a lot of people creating custom Git aliases create a similar alias. I most recently saw this approach in a post by Phil Haack.
When you clone a repository, Git automatically checks out the "default" branch (unless you explicitly specify a different branch). Git creates a symbolic-ref
in the local repository at refs/remotes/origin/HEAD
to point to the default branch of the remote. This ref is updated based on what the remote defines as its HEAD
—for GitHub, that’s the branch shown as "default" in the UI.
You can read the value of this reference with git symbolic-ref refs/remotes/origin/HEAD
which prints the remote reference:
$ git symbolic-ref refs/remotes/origin/HEAD
refs/remotes/origin/main
We only need the final main
part of the output, so we create an alias to extract that using sed
. I call the alias git default-branch
, and define it like this:
[alias]
default-branch = "!git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'"
If you run this in a repository it should print the remote you expect:
$ git default-branch
main
In some cases, you may get an error when you run this:
fatal: ref refs/remotes/origin/HEAD is not a symbolic ref
This indicates that the default branch for origin
hasn't been set. This generally won't happen if you cloned the repository from GitHub, but it could happen if you have only setup the repository locally, or for some configurations. To fix it, run:
git remote set-head origin --auto
This queries the remote and updates the local symbolic link to point to the correct branch. Now we know the default remote branch, we can calculate the merge-base
for the stack
Calculating the merge-base
for the stack
Per the documentation, the merge-base
between two commits is:
…the best common ancestor(s) between two commits to use in a three-way merge. One common ancestor is better than another common ancestor if the latter is an ancestor of the former. A common ancestor that does not have any better common ancestor is a best common ancestor, i.e. a merge base.
I think merge-base
is easiest to visualize, so considering this simple graph:
o---o---o---B---o---o---C
/
---o---1---o---o---o---o---o---A
The merge base of commits A
and B
is marked 1
. Similarly, the merge base of commits A
and C
is 1
. The merge base of B
and C
is is B
.
When we're calculating our stack, we don't want to require that you've already rebased your stack on top of origin/main
, so instead we need to calculate the merge-base
between the top-most branch of our stack and the default branch. Luckily Git already has the git merge-base
command that does this for us:
git merge-base <commit1> <commit2>
We define an alias called merge-base-origin
that runs the above command using the default-branch
and either the HEAD
(for the currently-checked branch) or a parameter specified by the caller, which prints out the commit. For example, if we had checked out branch C
and A
was origin/main
, then git merge-base-origin
would print out the SHA of commit 1
The Git configuration below shows the merge-base-origin
command, and how we embed a call to the git default-branch
alias in there:
[alias]
merge-base-origin ="!f() { git merge-base ${1-HEAD} origin/$(git default-branch); };f "
An interesting part of this command is the bash ${1-HEAD}
. This says:
- If there is a user-supplied parameter, place it here.
- If not, use
HEAD
.
That means we can do things like:
# Use HEAD and origin/main
$ git merge-base-origin
7257e92c016e017fc95e763302ac31c32d78c2b8
# Use feature/part-2 and origin/main
$ git merge-base-origin feature/part-2
7257e92c016e017fc95e763302ac31c32d78c2b8
The next step is the hard one: listing all the branches between the merge-base and our target branch.
Listing the branches
The next step, listing all the branches to push, is the tricky part, and I was heavily inspired by the discussion on my previous post! The general approach is to use git log
to list all the branches between two commits, but it requires quite a bit of playing with the format. I'll build it up bit-by-bit in this section to get to the final command.
We start by running git log --pretty=%D
and passing in the two commits we want to compare (manually set to HEAD
and main
at the moment for simplicity). I'm running these commands on the repository we saw previously:
The %D
format ensures that for each commit, we only print out the references pointed to:
$ git log --pretty=%D main..HEAD
HEAD -> feature/part-3, origin/feature/part-3
origin/feature/part-2, feature/part-2
origin/main, origin/HEAD, feature/part-1
OK, you can see the outline of what we need there. There's a lot of extra noise by way of the remote branches (origin/feature/part-3
etc) and the HEAD
, plus empty commits, but it's a good start.
We'll start by getting rid of the empty lines. We can do that using --simplify-by-decoration
:
$ git log --pretty=%D --simplify-by-decoration main..HEAD
HEAD -> feature/part-3, origin/feature/part-3
origin/feature/part-2, feature/part-2
origin/main, origin/HEAD, feature/part-1
Great. Now we need to get rid of the remote references and the HEAD
. We'll use --decorate-refs
for that, and specify only the refs we care about, local branches:
$git log --pretty=%D --simplify-by-decoration --decorate-refs=refs/heads main..HEAD
feature/part-3
feature/part-2
feature/part-1
Perfect! This almost looks perfect, but there's a slight issue that's not obviously apparent. We can see this if we create another branch:
# Just for clarity
git checkout feature/part-3
# Create a new branch on the same commit as feature/part-3
# but don't add any commits
git branch feature/part-4
If we run the above command again, we get:
$ git log --pretty=%D --simplify-by-decoration --decorate-refs=refs/heads main..HEAD
feature/part-4, feature/part-3
feature/part-2
feature/part-1
This shows the problem: the %D
pretty format places the two branches on the same line, separated by a comma. We want each branch to be listed on its own line, so we define our own pretty format instead, using %n
as the separator to place each branch on its own line.
$ git log --pretty=format:"%(decorate:prefix=,suffix=,tag=,separator=%n)" --simplify-by-decoration --decorate-refs=refs/heads main..HEAD
feature/part-4
feature/part-3
feature/part-2
feature/part-1
And there we have it, success! All that remains is to update the hardcoded main
and HEAD
to support providing a specific branch, and to calculate the merge-base
dynamically, and our alias is complete.
[alias]
stack = "!f() { \
BRANCH=${1-HEAD}; \
MERGE_BASE=$(git merge-base-origin $BRANCH); \
git log --decorate-refs=refs/heads --simplify-by-decoration --pretty=format:\"%(decorate:prefix=,suffix=,tag=,separator=%n)\" $MERGE_BASE..$BRANCH; \
};f "
For simplicity, I separated the BRANCH
and MERGE_BASE
variables out. As in the merge-base-orgin
alias, BRANCH
is defined as either HEAD
or the user-provided branch. MERGE_BASE
is the output of running git merge-base-origin
with the calculated $BRANCH
, and then finally, we run the git log
command.
With the git stack
alias complete, we can now run
$ git stack
feature/part-4
feature/part-3
feature/part-2
# or, for example
$ git stack feature/part-2
feature/part-2
All that remains is to implement the git push-stack
command.
Pushing all the branches
With the git stack
command implemented, git push-stack
simply needs to invoke git stack
and then call git push origin --force-with-lease
for each of the values returned. The easiest way to do this is with xargs
. We can simply pipe the output of git stack
to xargs
, and it will run the command for each of the provided branches:
git stack | xargs -I {} git push --force-with-lease origin {}
The -I {}
part means "replace {}
in the following command with the actual parameter", where the parameter is the value returned from git stack
, split by lines. So this runs git push
on each of the branches returned by git stack
.
The definition in git is almost as simple as this, I just defined the $BRANCH
variable again to allow passing a different value down to git stack
[alias]
push-stack = "!f() { \
BRANCH=${1-HEAD}; \
git stack $BRANCH | xargs -I {} git push --force-with-lease origin {}; \
};f "
That's the final piece of the puzzle, so now we can put it all together and look at our final set of aliases.
Putting it all together
The final set of aliases, as defined in my .gitconfig file is as follows:
[alias]
default-branch = "!git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'"
merge-base-origin ="!f() { git merge-base ${1-HEAD} origin/$(git default-branch); };f "
stack = "!f() { \
BRANCH=${1-HEAD}; \
MERGE_BASE=$(git merge-base-origin $BRANCH); \
git log --decorate-refs=refs/heads --simplify-by-decoration --pretty=format:\"%(decorate:prefix=,suffix=,tag=,separator=%n)\" $MERGE_BASE..$BRANCH; \
};f "
push-stack = "!f() { \
BRANCH=${1-HEAD}; \
git stack $BRANCH | xargs -I {} git push --force-with-lease origin {}; \
};f "
You can simply copy-paste those into your own Git config (e.g. by running git config --global --edit
to open your editor).
One thing you might wonder is why there are so many cases of
${1-HEAD}
duplicated throughout. This is primarily so that each of these aliases can be called independently.
Alternatively, you can run the following at the command line to add them automatically:
git config --global alias.default-branch "!git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'"
git config --global alias.merge-base-origin '!f() { git merge-base ${1-HEAD} origin/$(git default-branch); };f '
git config --global alias.stack '!f() { BRANCH=${1-HEAD}; MERGE_BASE=$(git merge-base-origin $BRANCH); git log --decorate-refs=refs/heads --simplify-by-decoration --pretty=format:\"%(decorate:prefix=,suffix=,tag=,separator=%n)\" $MERGE_BASE..$BRANCH; };f '
git config --global alias.push-stack '!f() { BRANCH=${1-HEAD}; git stack $BRANCH | xargs -I {} git push --force-with-lease origin {}; };f '
And there you have it: simple pushing of an entire Git stack of branches with a single command. If you've been handling this manually like I was for years, then I hope this helps! If not, then I'd be interested to see what scripts you're using: reply in the comments if you're happy sharing!
Summary
In this post I described stacked branches in Git and how they can simplify the review process for PRs. However, it can be a pain when you need to push a whole stack of PRs to a remote. In this post I showed how I created a Git alias that allows you to run git push-stack
to push an entire stack of branches in a single command. I showed how I built this alias out of multiple other aliases, such as git stack
, and added customisation options. I hope this makes managing stacks of branches easier for everyone!
May 20, 2025 at 02:30PM
Click here for more details...
=============================
The original post is available in Andrew Lock | .NET Escapades by Andrew Lock
this post has been published as it is through automation. Automation script brings all the top bloggers post under a single umbrella.
The purpose of this blog, Follow the top Salesforce bloggers and collect all blogs in a single place through automation.
============================

Post a Comment