feat(sync): replace hub with an internal branch engine

Fetch and prune the preferred remote, then reconcile every local branch
independently. Fast-forward branches that are behind, preserve divergent work,
and delete a branch only when its configured upstream is gone and its commits
are already contained by the remote default branch.

Keep dirty repositories eligible while protecting the active branch and refs
checked out in linked worktrees. Preserve the previous all-clean policy behind
--strict, expose structured per-branch results, add real bare-remote integration
tests, and retain the upstream MIT notice for the adapted hub sync behavior.
This commit is contained in:
2026-09-06 19:41:53 +01:00
parent 351eab3743
commit b80713b334
8 changed files with 703 additions and 170 deletions
+11 -102
View File
@@ -2,63 +2,14 @@ package main
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
func TestMain(m *testing.M) {
if os.Getenv("STACK_SYNC_TEST_HELPER") == "hub" {
marker := os.Getenv("STACK_SYNC_TEST_MARKER")
if err := os.WriteFile(marker, []byte(strings.Join(os.Args[1:], " ")), 0o644); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
os.Exit(0)
}
os.Exit(m.Run())
}
func installFakeHub(t *testing.T, dir string) string {
t.Helper()
source, err := os.Executable()
if err != nil {
t.Fatal(err)
}
name := "hub"
if runtime.GOOS == "windows" {
name += ".exe"
}
destination := filepath.Join(dir, name)
if err := os.Link(source, destination); err == nil {
return destination
}
input, err := os.Open(source)
if err != nil {
t.Fatal(err)
}
defer input.Close()
output, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o755)
if err != nil {
t.Fatal(err)
}
if _, err := io.Copy(output, input); err != nil {
_ = output.Close()
t.Fatal(err)
}
if err := output.Close(); err != nil {
t.Fatal(err)
}
return destination
}
func git(t *testing.T, dir string, args ...string) {
t.Helper()
cmd := exec.Command("git", append([]string{"-C", dir}, args...)...)
@@ -116,7 +67,7 @@ func TestNestedRepoDoesNotDirtyParent(t *testing.T) {
}
}
func TestDirtyFileBlocksRepository(t *testing.T) {
func TestDirtyFileIsReportedButRepositoryRemainsEligible(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, "repo")
makeRepo(t, path)
@@ -125,12 +76,12 @@ func TestDirtyFileBlocksRepository(t *testing.T) {
}
got := inspect(root, path, []string{path})
if got.Eligible || len(got.Dirty) != 1 {
t.Fatalf("dirty repository was not blocked: %+v", got)
if !got.Eligible || len(got.Dirty) != 1 {
t.Fatalf("dirty repository was not eligible with its changes reported: %+v", got)
}
}
func TestUntrackedFileAlongsideNestedRepoStillBlocksParent(t *testing.T) {
func TestUntrackedFileAlongsideNestedRepoIsStillReported(t *testing.T) {
root := t.TempDir()
parent := filepath.Join(root, "parent")
child := filepath.Join(parent, "child")
@@ -142,8 +93,8 @@ func TestUntrackedFileAlongsideNestedRepoStillBlocksParent(t *testing.T) {
paths, _ := discover(root, nil)
got := inspect(root, parent, paths)
if got.Eligible || len(got.Dirty) != 1 {
t.Fatalf("untracked file should block parent: %+v", got)
if !got.Eligible || len(got.Dirty) != 1 {
t.Fatalf("untracked file should be reported without blocking parent: %+v", got)
}
}
@@ -160,7 +111,7 @@ func TestSkippedDirectoriesAreNotSearched(t *testing.T) {
}
}
func TestSyncRechecksAndSkipsNewlyDirtyRepository(t *testing.T) {
func TestStrictSyncRechecksAndSkipsNewlyDirtyRepository(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, "repo")
makeRepo(t, path)
@@ -172,55 +123,13 @@ func TestSyncRechecksAndSkipsNewlyDirtyRepository(t *testing.T) {
t.Fatal(err)
}
bin := filepath.Join(root, "bin")
if err := os.Mkdir(bin, 0o755); err != nil {
t.Fatal(err)
}
marker := filepath.Join(root, "hub-was-called")
installFakeHub(t, bin)
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
t.Setenv("STACK_SYNC_TEST_HELPER", "hub")
t.Setenv("STACK_SYNC_TEST_MARKER", marker)
results := syncAll(root, []repo{planned}, 1, time.Minute, io.Discard, true)
results := syncAll(root, []repo{planned}, 1, time.Minute, io.Discard, true, true)
if len(results) != 1 || !results[0].Skipped || results[0].Success {
t.Fatalf("syncAll() = %+v", results)
}
if _, err := os.Stat(marker); !os.IsNotExist(err) {
t.Fatalf("hub ran despite the newly dirty worktree")
}
}
func TestSyncInvokesHubForCleanRepository(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, "repo")
makeRepo(t, path)
planned := inspect(root, path, []string{path})
bin := filepath.Join(root, "bin")
if err := os.Mkdir(bin, 0o755); err != nil {
t.Fatal(err)
}
marker := filepath.Join(root, "hub-was-called")
installFakeHub(t, bin)
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
t.Setenv("STACK_SYNC_TEST_HELPER", "hub")
t.Setenv("STACK_SYNC_TEST_MARKER", marker)
results := syncAll(root, []repo{planned}, 1, time.Minute, io.Discard, true)
if len(results) != 1 || !results[0].Success || results[0].Skipped {
t.Fatalf("syncAll() = %+v", results)
}
args, err := os.ReadFile(marker)
if err != nil {
t.Fatalf("hub was not called: %v", err)
}
if string(args) != "sync --color=never" {
t.Fatalf("hub arguments = %q", args)
}
}
func TestScanWithBlockedRepositoryIsAValidReport(t *testing.T) {
func TestScanReportsDirtyEligibleRepository(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, "repo")
makeRepo(t, path)
@@ -233,8 +142,8 @@ func TestScanWithBlockedRepositoryIsAValidReport(t *testing.T) {
if code != 0 {
t.Fatalf("run() code = %d, stderr = %q", code, stderr.String())
}
if !bytes.Contains(stdout.Bytes(), []byte("BLOCKED: dirty worktree")) {
t.Fatalf("scan did not report blocked repository:\n%s", stdout.String())
if !bytes.Contains(stdout.Bytes(), []byte("dirty worktree (1 changes), checked-out branch protected")) {
t.Fatalf("scan did not report dirty eligible repository:\n%s", stdout.String())
}
}