Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
Go
at sl/gitmirror
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274package gitutil
import ( "bufio" "context" "fmt" "io" "net" "os" "os/exec" "path/filepath" "strings" "sync" "testing" "time")
const testSparseDir = "/.tangled/workflows"
func newGitRepo(t *testing.T) (string, func(args ...string) string) { t.Helper() // ignore the developer's git config t.Setenv("GIT_CONFIG_GLOBAL", os.DevNull) t.Setenv("GIT_CONFIG_SYSTEM", os.DevNull) t.Setenv("GIT_AUTHOR_NAME", "spindle") t.Setenv("GIT_AUTHOR_EMAIL", "spindle@tangled.org") t.Setenv("GIT_COMMITTER_NAME", "spindle") t.Setenv("GIT_COMMITTER_EMAIL", "spindle@tangled.org")
src := t.TempDir() run := func(args ...string) string { t.Helper() cmd := exec.Command("git", append([]string{"-C", src}, args...)...) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("git %v: %v: %s", args, err, out) } return strings.TrimSpace(string(out)) }
run("init", "-b", "main") return src, run}
func TestSparseSync(t *testing.T) { src, run := newGitRepo(t)
if err := os.MkdirAll(filepath.Join(src, ".tangled", "workflows"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(src, ".tangled", "workflows", "ci.yml"), []byte("when: []\n"), 0o644); err != nil { t.Fatal(err) } run("add", ".") run("commit", "-m", "workflows") rev := run("rev-parse", "HEAD")
path := filepath.Join(t.TempDir(), "repo") for _, stage := range []string{"clone", "fetch"} { if err := SparseSync(context.Background(), src, path, rev, testSparseDir); err != nil { t.Fatalf("%s: %v", stage, err) } if _, err := os.Stat(filepath.Join(path, ".tangled", "workflows", "ci.yml")); err != nil { t.Fatalf("%s: sparse file missing from the checkout: %v", stage, err) } }}
func TestSparseSync_DanglingRemoteHead(t *testing.T) { src, run := newGitRepo(t)
if err := os.MkdirAll(filepath.Join(src, ".tangled", "workflows"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(src, ".tangled", "workflows", "ci.yml"), []byte("when: []\n"), 0o644); err != nil { t.Fatal(err) } run("add", ".") run("commit", "-m", "workflows") sha := run("rev-parse", "HEAD")
// switch branch to master and point HEAD to unborn main run("branch", "-m", "main", "master") run("symbolic-ref", "HEAD", "refs/heads/main")
path := filepath.Join(t.TempDir(), "repo") // syncing by commit sha must succeed even when remote HEAD is dangling if err := SparseSync(context.Background(), src, path, sha, testSparseDir); err != nil { t.Fatalf("sync by sha failed on dangling HEAD remote: %v", err) } if _, err := os.Stat(filepath.Join(path, ".tangled", "workflows", "ci.yml")); err != nil { t.Fatalf("sparse file missing: %v", err) }
// syncing by ref name must also succeed path2 := filepath.Join(t.TempDir(), "repo2") if err := SparseSync(context.Background(), src, path2, "refs/heads/master", testSparseDir); err != nil { t.Fatalf("sync by ref failed on dangling HEAD remote: %v", err) }}
func TestRefContainsCommit(t *testing.T) { src, run := newGitRepo(t)
if err := os.WriteFile(filepath.Join(src, "f"), []byte("a"), 0o644); err != nil { t.Fatal(err) } run("add", ".") run("commit", "-m", "a") olderThanWindow := run("rev-parse", "HEAD")
// fill main past the ancestry window RefContainsCommit fetches for i := 0; i < maxAncestryDepth+5; i++ { if err := os.WriteFile(filepath.Join(src, "f"), []byte(fmt.Sprintf("f%d", i)), 0o644); err != nil { t.Fatal(err) } run("add", ".") run("commit", "-m", fmt.Sprintf("fill %d", i)) } tip := run("rev-parse", "HEAD") ancestor := run("rev-parse", "HEAD~1")
// a commit that exists in the repository but not on main run("checkout", "-b", "dev") if err := os.WriteFile(filepath.Join(src, "f"), []byte("c"), 0o644); err != nil { t.Fatal(err) } run("add", ".") run("commit", "-m", "c") offBranch := run("rev-parse", "HEAD") run("checkout", "main")
for _, tt := range []struct { name string sha string want bool }{ {"the ref tip", tip, true}, {"an ancestor of the tip", ancestor, true}, {"a commit older than the ancestry window", olderThanWindow, false}, {"a commit on another branch", offBranch, false}, {"a commit the repo never had", strings.Repeat("1", 40), false}, } { t.Run(tt.name, func(t *testing.T) { got, err := RefContainsCommit(context.Background(), src, "refs/heads/main", tt.sha) if got != tt.want { t.Fatalf("RefContainsCommit() = %t, err = %v, want %t", got, err, tt.want) } }) }}
// hangingKnot keeps the child process alive until the client closes its socketfunc hangingKnot(t *testing.T) (url string, asked, dropped <-chan struct{}) { t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } t.Cleanup(func() { ln.Close() })
askedCh := make(chan struct{}) droppedCh := make(chan struct{}) var askOnce, dropOnce sync.Once
go func() { for { conn, err := ln.Accept() if err != nil { return } go func() { defer conn.Close() br := bufio.NewReader(conn) for { line, err := br.ReadString('\n') if err != nil { return } if strings.TrimSpace(line) == "" { break } } io.WriteString(conn, "HTTP/1.1 200 OK\r\n"+ "Content-Type: application/x-git-upload-pack-advertisement\r\n"+ "Transfer-Encoding: chunked\r\n\r\n") askOnce.Do(func() { close(askedCh) }) // this ends only when the child process dies io.Copy(io.Discard, br) dropOnce.Do(func() { close(droppedCh) }) }() } }()
return "http://" + ln.Addr().String(), askedCh, droppedCh}
func TestRunCancelKillsRemoteHelper(t *testing.T) { knot, asked, dropped := hangingKnot(t) path := filepath.Join(t.TempDir(), "repo")
ctx, cancel := context.WithCancel(context.Background()) defer cancel()
done := make(chan error, 1) go func() { done <- Run(ctx, "clone", "--no-checkout", "--depth=1", knot+"/repo.git", path) }()
select { case <-asked: case err := <-done: t.Fatalf("clone returned before it reached the knot: %v", err) case <-time.After(30 * time.Second): t.Fatal("git never asked the knot for refs") }
cancel()
select { case err := <-done: if err == nil { t.Fatal("a cancelled clone reported success") } case <-time.After(15 * time.Second): t.Fatal("Run is still blocked after cancellation: a surviving helper holds the inherited stderr pipe open") }
select { case <-dropped: case <-time.After(15 * time.Second): t.Fatal("git-remote-http outlived the cancelled clone") }}
func TestSparseSyncCancelReleasesRepoLock(t *testing.T) { knot, asked, _ := hangingKnot(t) path := filepath.Join(t.TempDir(), "repo")
ctx, cancel := context.WithCancel(context.Background()) defer cancel()
done := make(chan error, 1) go func() { done <- SparseSync(ctx, knot+"/repo.git", path, "HEAD", testSparseDir) }()
select { case <-asked: case err := <-done: t.Fatalf("sync returned before it reached the knot: %v", err) case <-time.After(30 * time.Second): t.Fatal("git never asked the knot for refs") }
cancel()
select { case <-done: case <-time.After(15 * time.Second): t.Fatal("SparseSync is still blocked after cancellation") }
// another request must be able to use this repo dead, cancelDead := context.WithCancel(context.Background()) cancelDead()
next := make(chan error, 1) go func() { next <- SparseSync(dead, knot+"/repo.git", path, "HEAD", testSparseDir) }()
select { case <-next: case <-time.After(15 * time.Second): t.Fatal("a cancelled sync never released the repo lock") }}