Git Stack Sync (gss) is a command-line tool, written in TypeScript, that simplifies working with stacked Git branches. Inspired by the workflows of tools like Graphite, it helps you create, manage, and submit dependent chains of branches without the usual hassle of manual rebasing and pull request management.
It works by maintaining a simple JSON configuration file in your local .git directory, allowing it to understand the relationships between your branches. When combined with the GitHub CLI (gh), gss can automate large parts of your development workflow.
- Efficiency: It leverages modern Git features like
git rebase --update-refsto perform complex stack rebases in a single, fast operation. - Automation: It automates the most tedious parts of stacked branching, such as rebasing an entire stack of branches (
sync) or intelligently updating children after an amendment (restack). - GitHub Integration: It seamlessly uses the
ghCLI to manage pull requests for your entire stack, creating dependencies and updating them as you restructure your branches. - Clarity: The
statuscommand gives you a comprehensive overview of your entire stack, showing which branches are out of sync, need pushing, or have merged pull requests.
gss treats a series of dependent branches as a "stack". When you run gss create <branch-name>, it creates a new branch and records its parent in its configuration file.
This simple parent-child metadata is the foundation for all of gss's powerful features. It allows the tool to traverse the stack, understand dependencies, and perform complex operations like rebasing the entire chain with a single command.
-
Dependencies: Make sure you have the following tools installed and available in your
$PATH:git(version 2.39+ is recommended for--update-refssupport)- Node.js
- GitHub CLI (
gh)
-
Install from npm: You can install
gssglobally from the npm registry.npm install -g git-stack-sync
Here is a detailed list of all available commands.
-
Creates a new branch as a child of the currently checked-out branch and switches to it. This is the primary way to extend a stack.
-
Inserts a new branch into the stack. By default, it inserts after the current branch. The
--beforeflag inserts it before the current branch.gsswill automatically rebase any descendant branches and update their associated pull requests on GitHub. -
Squashes the commits from one branch into another and deletes the squashed branch. By default, it squashes the current branch into its
parent. Use--into childto squash the child branch into the current one.gsswill prompt to close the pull request of the deleted branch. -
Manually marks an existing branch as a stacked branch. If
parent-branchis omitted, the repository's base branch (e.g.,main) is used. -
Removes
gssmetadata from the current branch. This is only allowed if the branch has no unique commits. The child branch will be reparented to the untracked branch's parent.
-
This is the workhorse command for keeping your stack up-to-date. It performs several key operations:
- Fetches the latest changes from
origin. - Updates your local base branch (e.g.,
main) to match the remote. - Checks the status of the pull request for every branch in your stack.
- If any PRs have been merged, it automatically removes those branches from the stack, re-parents their children, and prepares for cleanup.
- Rebases the remaining, unmerged branches onto the latest version of the base branch.
- Fetches the latest changes from
-
Intelligently updates your stack after you've modified its history (e.g., with
git commit --amendor an interactive rebase). It automatically detects the first branch that has diverged from its parent and rebases all of its descendants on top of it. -
A convenient shortcut. It adds all staged changes to the most recent commit (
git commit --amend --no-edit) and then automatically runsgss restackto update any descendant branches. -
Pushes all branches in the current stack to the remote (
origin). It uses--force-with-leaseto safely update remote branches after asyncorrestackhas changed their history. -
Resumes a
syncorrestackoperation after you have resolved a git rebase conflict.
-
Displays a detailed, colorful overview of the entire current stack, including parent-child relationships, sync status, and pull request status.
-
Finds and lists all stacks in your local repository.
-
Quickly navigate up (
up) or down (down) the branch stack.
-
Creates GitHub pull requests for all branches in the stack that don't have one yet. It automatically sets the base branch for each PR to be its parent in the stack, creating a dependent chain of PRs.
-
Opens the GitHub pull request for the current branch in your web browser.
# 1. Start on your base branch (e.g., main)
git checkout main
# 2. Create the first branch in your stack and add a commit
gss create feat-part-1
echo "Initial work" > file1.txt
git add . && git commit -m "feat: Implement part 1"
# 3. Create a second, dependent branch and add another commit
gss create feat-part-2
echo "More work" > file2.txt
git add . && git commit -m "feat: Implement part 2"
# 4. Create pull requests for the entire stack
gss submit
# β‘οΈ Creating PR for 'feat-part-1'...
# π’ Created PR #20 for 'feat-part-1': [https://github.com/mock/repo/pull/20](https://github.com/mock/repo/pull/20)
# β‘οΈ Creating PR for 'feat-part-2'...
# π’ Created PR #21 for 'feat-part-2': [https://github.com/mock/repo/pull/21](https://github.com/mock/repo/pull/21)
# π’ Stack submission complete.This is the most common and powerful workflow. Imagine your teammate reviewed and squash-merged the first PR (feat-part-1). Your local repository is now out of date.
# 1. Check the status. gss will fetch from the remote and detect changes.
gss status
# β‘οΈ Gathering stack status...
#
# β‘οΈ main (π‘ Behind by 1)
#
# β‘οΈ feat-part-1 (parent: main)
# ββ Status: π‘ Behind 'main' (1 commits)
# ββ PR: π£ #10: MERGED
#
# β‘οΈ feat-part-2 * (parent: feat-part-1)
# ββ Status: π’ Synced
# ββ PR: π’ #11: OPEN
#
# π‘ Warning: The stack contains merged branches or is behind the base branch.
# π‘ Next step: Run 'gss sync' to update the base and rebase the stack.
# 2. Run sync to automatically fix everything.
gss sync --yes
# β‘οΈ Syncing stack with 'main' and checking for merged branches...
# π’ Branch 'feat-part-1' has been merged.
# β‘οΈ Rebasing remaining stack onto 'main'...
# π’ Stack rebased successfully.
# β‘οΈ Finishing operation...
# π’ Deleted local branch 'feat-part-1'.
# π’ Operation complete.
# π‘ Next step: Run 'gss push' to update your remote branches.
# 3. Push the rebased branch to update its pull request.
gss push --yesThis workflow demonstrates how to insert a new feature or fix into the middle of an existing stack. This is common when a code review on feat-part-2 reveals a missing piece that should have been in its own PR, logically before feat-part-2.
# 1. You have a stack: main -> feat-part-1 -> feat-part-2
# A review on feat-part-2's PR suggests you extract some logic
# into its own branch/PR.
#
# First, check out the branch you want to insert *after*.
gss down
# π’ Checked out parent branch: feat-part-1
# 2. Use `gss insert` to create the new branch.
# `gss` will create the new branch and automatically rebase
# the old descendant (`feat-part-2`) on top of it.
gss insert feat-part-1-fixup
# β‘οΈ Preparing to insert 'feat-part-1-fixup' after 'feat-part-1'...
# π’ Created branch 'feat-part-1-fixup' on top of 'feat-part-1'.
# β‘οΈ Checking stack integrity to find point of divergence...
# π‘ Warning: Detected stack divergence at 'feat-part-1-fixup'. Restacking descendants...
# Will restack the following branches: feat-part-2
# π’ Restack complete.
# π’ Successfully inserted 'feat-part-1-fixup' into the stack.
# π‘ Next step: Add commits, then run 'gss submit' to create a PR.
# 3. You are now on the new branch. Add your changes and commit.
echo "A fix" > fix.txt
git add . && git commit -m "feat: Add fixup for part 1"
# 4. Your stack is now: main -> feat-part-1 -> feat-part-1-fixup -> feat-part-2
# Check the status to see the new structure.
gss status
# β‘οΈ feat-part-1-fixup * (parent: feat-part-1)
# ββ Status: βͺ Not on remote
# ββ PR: βͺ No PR submitted
#
# β‘οΈ feat-part-2 (parent: feat-part-1-fixup)
# ββ Status: π‘ Needs push (local history has changed)
# ββ PR: π’ #102: OPEN
#
# π‘ Warning: One or more local branches have changed.
# π‘ Next step: Run 'gss push' to update the remote.
# 5. Submit the new branch and push the changes.
gss submit
gss push --yesThis workflow covers the common scenario where you need to modify a commit on a parent branch that already has other branches stacked on top of it. gss makes this a safe and simple operation.
Imagine your stack is main -> feat-part-1 -> feat-part-2, and you need to make a small fix to the commit on feat-part-1.
# 1. You have a stack and need to edit an earlier branch.
# First, check out the branch you need to modify.
gss down
# π’ Checked out parent branch: feat-part-1
# 2. Make your code change and stage it using git.
echo "A small fix" >> file1.txt
git add file1.txt
# 3. Run `gss amend`.
# This command will amend your staged changes to the latest commit and
# then automatically trigger a `restack`. The restack operation detects
# that `feat-part-1` has changed and rebases its descendant, `feat-part-2`,
# on top of the new version.
gss amend --yes
# β‘οΈ Amending changes to the last commit on 'feat-part-1'...
# π’ Commit amended successfully.
# β‘οΈ Checking stack integrity to find point of divergence...
# π‘ Warning: Detected stack divergence at 'feat-part-1'. Restacking descendants...
# Will restack the following branches: feat-part-2
# π’ Restack complete.
# β‘οΈ Finishing operation...
# π’ Operation complete.
# π‘ Next step: Run 'gss push' to update your remote branches.
# 4. Your entire stack is now consistent. `feat-part-2` is correctly
# rebased on the amended `feat-part-1`.
# The final step is to update the PRs on GitHub.
gss push --yes