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:
+11
-102
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user