Skip to content

sync: Add failing test scripts for branch sync feature#1262

Open
ed-irl wants to merge 12 commits into
ed-irl/restack-options-paramfrom
ed-irl/branch-sync
Open

sync: Add failing test scripts for branch sync feature#1262
ed-irl wants to merge 12 commits into
ed-irl/restack-options-paramfrom
ed-irl/branch-sync

Conversation

@ed-irl

@ed-irl ed-irl commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

sync: Add failing test scripts for branch sync feature

Drafts the end-to-end test surface for the upcoming gs branch sync and
its multi-branch / repo-sync variants. Five script tests covering:

  • fast-forward when local matches LastPushed
  • rebase when both sides have new commits
  • conflict surfacing + gs rebase continue resumption
  • .gitattributes regenerate merge driver auto-resolution
  • repo sync orchestrating multiple tracked branches bottom-up

All five fail today because the sync commands and per-branch
LastPushedHash state do not yet exist. Subsequent commits add the
implementation needed to turn them green.

state: Track LastPushedHash per upstream

Add LastPushedHash to branchUpstreamState so each tracked branch
remembers the commit hash that was last successfully pushed to its
upstream. This is the missing piece needed by the forthcoming
'gs branch sync' command to distinguish:

  • "remote moved, local stayed" (fast-forward safe)
  • "local moved past last push, remote stayed" (will push next)
  • "both moved past last push" (diverged; needs rebase)

Plumbing:

  • branchUpstreamState gains LastPushedHash with json:"omitempty" so
    existing state files don't grow until a hash is recorded.
  • LookupResponse exposes it as UpstreamLastPushedHash (git.Hash).
  • UpsertRequest gains *git.Hash UpstreamLastPushedHash, preserving
    the existing-value semantics from other Upsert fields.
  • Setting UpstreamBranch to the same name preserves the prior
    LastPushedHash; clearing or renaming the upstream resets it.

Tests cover JSON round-trip (with and without the hash), the omit-when-
empty marshaling contract, and the Upsert preserve / advance / clear
flows.

submit: Record LastPushedHash on every push

Capture the pushed commit hash at both push sites in the submit handler
(initial publish and subsequent updates) and write it into per-branch
state alongside the upstream name. This populates the marker the
upcoming 'gs branch sync' command uses to distinguish a fast-forwardable
remote update from a true divergence.

If hash recording fails (peel or store error), the push has already
succeeded -- log a warning and continue rather than failing the submit.
The branch will pick up a fresh hash on the next push.

sync: Add 'gs branch sync' fast-forward primitive

Add a branchsync handler and the 'gs branch sync' CLI command that
pulls remote-side commits added to a tracked branch since the last
push. Fast-forward only in this commit:

  • if the remote is strictly ahead of LastPushedHash and local hasn't
    moved past LastPushedHash, advance the branch to the remote and
    record the new LastPushedHash;
  • if local is ahead of remote, report "ahead of remote" and exit;
  • if both diverge, report and exit; rebase support lands in a
    follow-up.

For the currently checked-out branch the handler runs
'git pull --rebase --autostash' so HEAD and the working tree advance
together; a no-commits-to-replay rebase degenerates into a clean
fast-forward. For other branches it does a plain SetRef update.

Wired into main.go so the command resolves through the existing Kong
provider tree. testdata/script/branch_sync_fast_forward.txt now passes.

repo sync: Pull remote-side commits into tracked branches

After the trunk pull and merged-branch handling, iterate every tracked
branch and run the branchsync handler against it. Fast-forwarded
branches trigger an upstack restack so children that were based on the
old hash get rebased onto the new tip; otherwise the next 'gs branch
restack' would still see the stale base.

Add --no-pull-branches to opt out (matches the safe default mental
model: someone with no autofix bots, no force-pushes from collaborators,
and no co-developers on a branch can skip the extra fetches).

testdata/script/repo_sync_pull_branches.txt now passes.

sync: Add --rebase mode for diverged branches

Extend 'gs branch sync' with --rebase. When local has commits past
LastPushedHash AND the remote has new commits (the typical case after
a local restack races a CI bot push), replay the remote-only commits
on top of local instead of skipping the branch.

Algorithm:

  1. Confirm LastPushedHash is in both L and R's history. If either
    side rewrote history below it, skip with a clear reason.
  2. Check out the branch, reset --hard to R so HEAD = R.
  3. 'git rebase --onto L LastPushedHash' replays p..R onto L.

Conflicts surface as a normal interrupted rebase; 'gs rebase --continue'
resumes the operation because git-spice's continuation already inspects
.git/rebase-merge/head-name and our rebase sets it to the branch name.

.gitattributes merge drivers DO apply during step 3 because git uses
three-way merges for every commit replay. A file mapped to
merge=regenerate has its conflict auto-resolved by re-running the
configured generator. git rerere is NOT force-enabled by this command;
honor user-set rerere.enabled if present.

Surfaced via SyncRequest{Branch, Mode} so multi-branch callers can
opt in per-branch; the Sync entry point now takes the request struct.

All five script tests pass: fast_forward, rebase, conflict (with
gs rebase --continue resumption), regenerate_driver (with merge driver
auto-resolution), and the repo_sync_pull_branches orchestrator.

sync: Add stack / upstack / downstack sync variants

Add three new CLI commands mirroring the existing stack / upstack /
downstack restack shape:

  • gs stack sync -- sync every branch in the current stack
  • gs upstack sync -- sync the current branch and those above it
  • gs downstack sync -- sync the current branch and those below it

Each accepts --branch to retarget and --rebase to integrate diverged
branches via the rebase path. All three share a syncBranches() helper
that iterates in dependency order and triggers an upstack restack for
any branch whose tip moved -- the same orchestration 'gs repo sync'
performs across all tracked branches.

repo sync: Make pull-branches policy configurable

Replace the binary --no-pull-branches flag with an enum-backed
spice.repoSync.pullBranches config key. Values:

off -- skip the per-branch pull entirely
fastForward -- (default) advance safe branches, skip diverged ones
rebase -- replay diverged branches' local commits onto the
remote (matches 'gs branch sync --rebase' semantics)

Default is fastForward, which solves the autofix-ci stale-info problem
without ever surfacing a conflict mid repo sync. Users with stacks
that have mature merge drivers can opt up to rebase via either the
flag or gs config set spice.repoSync.pullBranches rebase.

sync: Add docs, changelog entries, and regenerated CLI reference

  • Document the new pull-branches behavior in the syncing-with-upstream
    guide, including the per-scope (branch/upstack/downstack/stack)
    variants and how to opt up to rebase mode.
  • Four changelog entries:
    Added: submit records LastPushedHash on push.
    Added: gs branch sync + scope variants.
    Changed: gs repo sync now pulls every tracked branch by default.
    Fixed: shamhub bare-repo --git-dir on git 2.50+.
  • Regenerate doc/includes/cli-reference.md, cli-shorthands.md, the
    per-command testdata/help/*.txt fixtures, and gs.txt.

@ed-irl

ed-irl commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator Author

ed-irl and others added 12 commits June 28, 2026 07:03
Drafts the end-to-end test surface for the upcoming `gs branch sync` and
its multi-branch / repo-sync variants. Five script tests covering:

  - fast-forward when local matches LastPushed
  - rebase when both sides have new commits
  - conflict surfacing + gs rebase continue resumption
  - .gitattributes regenerate merge driver auto-resolution
  - repo sync orchestrating multiple tracked branches bottom-up

All five fail today because the sync commands and per-branch
LastPushedHash state do not yet exist. Subsequent commits add the
implementation needed to turn them green.
Add LastPushedHash to branchUpstreamState so each tracked branch
remembers the commit hash that was last successfully pushed to its
upstream. This is the missing piece needed by the forthcoming
'gs branch sync' command to distinguish:

  - "remote moved, local stayed" (fast-forward safe)
  - "local moved past last push, remote stayed" (will push next)
  - "both moved past last push" (diverged; needs rebase)

Plumbing:

  - branchUpstreamState gains LastPushedHash with json:"omitempty" so
    existing state files don't grow until a hash is recorded.
  - LookupResponse exposes it as UpstreamLastPushedHash (git.Hash).
  - UpsertRequest gains *git.Hash UpstreamLastPushedHash, preserving
    the existing-value semantics from other Upsert fields.
  - Setting UpstreamBranch to the same name preserves the prior
    LastPushedHash; clearing or renaming the upstream resets it.

Tests cover JSON round-trip (with and without the hash), the omit-when-
empty marshaling contract, and the Upsert preserve / advance / clear
flows.
Capture the pushed commit hash at both push sites in the submit handler
(initial publish and subsequent updates) and write it into per-branch
state alongside the upstream name. This populates the marker the
upcoming 'gs branch sync' command uses to distinguish a fast-forwardable
remote update from a true divergence.

If hash recording fails (peel or store error), the push has already
succeeded -- log a warning and continue rather than failing the submit.
The branch will pick up a fresh hash on the next push.
Add a branchsync handler and the 'gs branch sync' CLI command that
pulls remote-side commits added to a tracked branch since the last
push. Fast-forward only in this commit:

  - if the remote is strictly ahead of LastPushedHash and local hasn't
    moved past LastPushedHash, advance the branch to the remote and
    record the new LastPushedHash;
  - if local is ahead of remote, report "ahead of remote" and exit;
  - if both diverge, report and exit; rebase support lands in a
    follow-up.

For the currently checked-out branch the handler runs
'git pull --rebase --autostash' so HEAD and the working tree advance
together; a no-commits-to-replay rebase degenerates into a clean
fast-forward. For other branches it does a plain SetRef update.

Wired into main.go so the command resolves through the existing Kong
provider tree. testdata/script/branch_sync_fast_forward.txt now passes.
After the trunk pull and merged-branch handling, iterate every tracked
branch and run the branchsync handler against it. Fast-forwarded
branches trigger an upstack restack so children that were based on the
old hash get rebased onto the new tip; otherwise the next 'gs branch
restack' would still see the stale base.

Add --no-pull-branches to opt out (matches the safe default mental
model: someone with no autofix bots, no force-pushes from collaborators,
and no co-developers on a branch can skip the extra fetches).

testdata/script/repo_sync_pull_branches.txt now passes.
Extend 'gs branch sync' with --rebase. When local has commits past
LastPushedHash AND the remote has new commits (the typical case after
a local restack races a CI bot push), replay the remote-only commits
on top of local instead of skipping the branch.

Algorithm:

  1. Confirm LastPushedHash is in both L and R's history. If either
     side rewrote history below it, skip with a clear reason.
  2. Check out the branch, reset --hard to R so HEAD = R.
  3. 'git rebase --onto L LastPushedHash' replays p..R onto L.

Conflicts surface as a normal interrupted rebase; 'gs rebase --continue'
resumes the operation because git-spice's continuation already inspects
.git/rebase-merge/head-name and our rebase sets it to the branch name.

.gitattributes merge drivers DO apply during step 3 because git uses
three-way merges for every commit replay. A file mapped to
merge=regenerate has its conflict auto-resolved by re-running the
configured generator. git rerere is NOT force-enabled by this command;
honor user-set rerere.enabled if present.

Surfaced via SyncRequest{Branch, Mode} so multi-branch callers can
opt in per-branch; the Sync entry point now takes the request struct.

All five script tests pass: fast_forward, rebase, conflict (with
gs rebase --continue resumption), regenerate_driver (with merge driver
auto-resolution), and the repo_sync_pull_branches orchestrator.
Add three new CLI commands mirroring the existing stack / upstack /
downstack restack shape:

  - gs stack sync     -- sync every branch in the current stack
  - gs upstack sync   -- sync the current branch and those above it
  - gs downstack sync -- sync the current branch and those below it

Each accepts --branch to retarget and --rebase to integrate diverged
branches via the rebase path. All three share a syncBranches() helper
that iterates in dependency order and triggers an upstack restack for
any branch whose tip moved -- the same orchestration 'gs repo sync'
performs across all tracked branches.
Replace the binary --no-pull-branches flag with an enum-backed
spice.repoSync.pullBranches config key. Values:

  off         -- skip the per-branch pull entirely
  fastForward -- (default) advance safe branches, skip diverged ones
  rebase      -- replay diverged branches' local commits onto the
                 remote (matches 'gs branch sync --rebase' semantics)

Default is fastForward, which solves the autofix-ci stale-info problem
without ever surfacing a conflict mid repo sync. Users with stacks
that have mature merge drivers can opt up to rebase via either the
flag or `gs config set spice.repoSync.pullBranches rebase`.
- Document the new pull-branches behavior in the syncing-with-upstream
  guide, including the per-scope (branch/upstack/downstack/stack)
  variants and how to opt up to rebase mode.
- Four changelog entries:
    Added: submit records LastPushedHash on push.
    Added: gs branch sync + scope variants.
    Changed: gs repo sync now pulls every tracked branch by default.
    Fixed: shamhub bare-repo --git-dir on git 2.50+.
- Regenerate doc/includes/cli-reference.md, cli-shorthands.md, the
  per-command testdata/help/*.txt fixtures, and gs.txt.
@abhinav abhinav changed the base branch from ed-irl/shamhub-bare-repo-discovery to ed-irl/restack-options-param June 28, 2026 14:04
@abhinav abhinav force-pushed the ed-irl/branch-sync branch from 1725dbc to 7363b3a Compare June 28, 2026 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant