hub sync silently dropped any local branch whose upstream it could not resolve: it cleared its internal remote branch while leaving the "gone" flag false, so neither the update nor the delete path ran. A run could then print "Already up to date" having done nothing, which is why a workspace-wide sync looked like it worked when it did not. - report unmatched branches as UNTRACKED with the reason, so "Already up to date" is only said when it is true - resolve the remote default branch via `git ls-remote --symref` when refs/remotes/<remote>/HEAD is missing, so merged branches whose upstream was deleted are still cleaned up - make --create-missing opt-in, keeping hub's default of never creating a local branch that does not already exist - summarise runs as changed/unchanged with per-action counts, and list deletions and failures in full instead of leaving them in scrollback - restore per-command flag help, which the custom Usage had dropped - add an in-place CLI progress line, drawn only to a terminal so piped and --json output stay free of cursor control - group the TUI selection screen by outcome so blocked and dirty repositories are visible before a run starts 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
Stack Sync
stack-sync safely fetches and synchronizes local branches across every Git repository in a workspace. It is designed for directory trees such as JezzWTF where the root may not be a repository and a parent repository may contain deliberately untracked, nested repositories.
It provides both a full-screen Bubble Tea control deck for interactive work and a conventional CLI/JSON interface for SSH sessions and automation. Both interfaces use the same discovery and safety engine.
Safety model
Before a repository is eligible, Stack Sync verifies that it:
- is on a branch (not a detached
HEAD); - has at least one remote;
- has no merge, rebase, cherry-pick, revert, or bisect in progress.
Modified, staged, deleted, conflicted, and untracked files are reported but do not block the repository by default. If the checked-out branch would otherwise be fast-forwarded or deleted, Stack Sync protects that branch and continues synchronizing safe inactive branches. Branches checked out in another linked worktree are protected too. Use --strict when every repository must be completely clean before anything is synchronized.
Untracked paths that are themselves discovered nested Git repositories are excluded from the parent repository's dirty report. All other worktree changes remain visible in the CLI, JSON, and TUI.
Stack Sync never runs git stash, git reset, git clean, git commit, or any other command that saves or discards work. A repository is checked again immediately before synchronization, and the worktree is checked again before changing its checked-out branch.
The branch behavior is derived from the MIT-licensed hub sync implementation and is now built directly into Stack Sync. It fetches and prunes the main remote, fast-forwards outdated branches, warns about divergent/unpushed branches, and removes a branch only when its configured upstream was deleted and the branch is already merged into the remote default branch. It prefers a remote named upstream, github, or origin; a sole differently named remote is also accepted. See third-party notices for attribution.
Branch handling
hub sync only ever moved branches that already existed locally, and a branch whose upstream it could not resolve was dropped from the report entirely. Stack Sync keeps hub's safety model but stops the silent behaviour:
- Unmatched branches are reported, not hidden. A local branch with no configured upstream and no same-named branch on the remote cannot be fast-forwarded.
hubset its internalremoteBranchto an empty string and leftgonefalse, so neither of its branches ran and the branch vanished without a word; the run then reported "Already up to date" while having done nothing. Such a branch is now reported asUNTRACKEDwith the reason. This makes the outcome visible, but it does not fast-forward the branch — there is no upstream to fast-forward from. - The remote default branch is resolved properly. Which branch a deleted-and-merged branch had been merged into was inferred from
refs/remotes/<remote>/HEAD, a local convenience symref that is missing from plenty of real clones. When it is absent Stack Sync now asks the remote directly withgit ls-remote --symref, so merged branches are still cleaned up instead of being kept with a vague warning.
By default, like hub, Stack Sync never creates a local branch that does not already exist. Pass --create-missing to additionally create local branches for branches that exist only on the remote; they are created with tracking configured and reported as CREATED, and this never touches the worktree, moves an existing branch, or checks anything out.
Branch actions reported per repository are UPDATED, DELETED, CREATED, PROTECTED, WARNING, and UNTRACKED.
Requirements
Stack Sync supports Linux and Windows. It requires:
- Go 1.24 or newer to build;
- Git on
PATH.
There is no separate hub dependency.
Build and install
go install is the simplest cross-platform installation method. From the repository root, run:
go install .
Go automatically installs stack-sync on Linux and stack-sync.exe on Windows into your Go binary directory. Make sure that directory is on PATH (normally $HOME/go/bin on Linux and %USERPROFILE%\go\bin on Windows).
To build a binary in the repository instead, use the command for your platform.
Linux
cd stack-sync
go test ./...
go build -o stack-sync .
install -Dm755 stack-sync ~/.local/bin/stack-sync
Windows (PowerShell)
Set-Location stack-sync
go test ./...
go build -o stack-sync.exe .
New-Item -ItemType Directory -Force "$env:LOCALAPPDATA\Programs\stack-sync" | Out-Null
Copy-Item .\stack-sync.exe "$env:LOCALAPPDATA\Programs\stack-sync\stack-sync.exe"
Add %LOCALAPPDATA%\Programs\stack-sync to your user PATH if it is not already present. The .exe suffix is important when choosing an explicit output name on Windows; go build -o stack-sync . creates an extensionless binary there.
Usage
Interactive TUI
Launch the control deck for the workspace:
stack-sync tui --root ~/Coding/jwtf
PowerShell accepts the same options with a Windows path:
stack-sync tui --root C:\Users\you\Coding\jwtf
Every eligible repository starts selected. The list is grouped by what will happen if you start a run now, so problems are visible before you commit to anything:
BLOCKED— not eligible, and cannot be selected. The reason (detachedHEAD, merge in progress, no remotes) is shown for the focused row.DIRTY— eligible, but the checked-out branch will be protected rather than fast-forwarded.READY— fully eligible.
Move with the arrow keys or j/k, toggle the focused repository with Space, select all with a, clear the selection with n, and refresh with r. Press s or Enter to review the branch-deletion warning, then y to begin. Blocked repositories cannot be selected.
During synchronization, the TUI shows overall progress, elapsed time, active repositories, live success/failure/skip totals, and the latest completed result. The final view lists failures and protected or divergent branches, prioritizes and focuses the first failure automatically, and retains per-repository branch details. Press f to cycle through every repository that needs attention.
CLI and automation
Scan the current workspace. This is read-only and is the default command:
stack-sync scan --root ~/Coding/jwtf
# equivalent:
stack-sync --root ~/Coding/jwtf
stack-sync scan --root C:\Users\you\Coding\jwtf
Review the same plan, confirm it, and sync every eligible repository:
stack-sync sync --root ~/Coding/jwtf
While a run is in progress, an updating line keeps the workspace totals on screen so you can see what is happening without waiting for the final summary:
17/40 · 2 active · 9 changed · 8 updated · 1 deleted · 2 protected · 1 warning · 1 untracked
That line is drawn only when stdout is a terminal. Redirected output and --json never contain cursor control characters, and --json is the right interface for scripts.
For automation, suppress the prompt and optionally require the entire workspace to be clean. In strict mode, either a blocked repository or any worktree change aborts the whole run:
stack-sync sync --root ~/Coding/jwtf --yes --strict
Useful options:
--jobs 4 maximum concurrent inspections or syncs
--timeout 5m per-repository fetch and sync timeout
--create-missing create local branches that exist only on the remote
--exclude temp skip a directory name anywhere in the tree
--exclude Org/old skip a root-relative path
--json emit structured output
Both / and \ are accepted in Windows paths. Quote a root or exclusion containing spaces, for example --root "C:\Users\you\Source Repositories".
Dependency caches, build outputs, and tool-managed directories such as node_modules, target, .claude, and .codex are skipped during discovery by default. These exclusions only affect repository discovery; they never make real changes inside a discovered repository disappear from its dirty-worktree check.
Exit codes are 0 for a successful scan or sync (including protected branches and divergence warnings), 1 for an operational/sync failure, 2 for invalid or unconfirmed non-interactive use, and 3 when strict mode refuses the plan or a previously eligible repository fails its immediate pre-sync safety check.