fix(sync): stop reporting success when no branch was actually synced

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>
This commit is contained in:
2026-09-28 19:57:42 +01:00
parent 4f6e9c9991
commit f4286edff2
7 changed files with 885 additions and 72 deletions
+27 -1
View File
@@ -20,6 +20,17 @@ Stack Sync never runs `git stash`, `git reset`, `git clean`, `git commit`, or an
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](THIRD_PARTY_NOTICES.md) 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. `hub` set its internal `remoteBranch` to an empty string and left `gone` false, 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 as `UNTRACKED` with 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 with `git 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:
@@ -78,7 +89,13 @@ PowerShell accepts the same options with a Windows path:
stack-sync tui --root C:\Users\you\Coding\jwtf
```
Every eligible repository starts selected. Dirty repositories remain selectable and are labelled `ready (dirty)`; their checked-out branches are protected if an update would affect them. 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.
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 (detached `HEAD`, 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.
@@ -102,6 +119,14 @@ 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:
```text
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:
```bash
@@ -113,6 +138,7 @@ Useful options:
```text
--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