package gitclient import ( "bytes" "context" "errors" "net/http/cgi" "net/http/httptest" "os" "os/exec" "path/filepath" "strings" "testing" ) func gitIn(t *testing.T, dir string, stdin []byte, args ...string) string { t.Helper() cmd := exec.Command("git", args...) cmd.Dir = dir cmd.Env = append(os.Environ(), "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t", "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t") cmd.Stdin = bytes.NewReader(stdin) out, err := cmd.Output() if err != nil { t.Fatalf("git %v: %v", args, err) } return strings.TrimSpace(string(out)) } func TestStockGit(t *testing.T) { gitPath, err := exec.LookPath("git") if err != nil { t.Skip("no git binary") } root := t.TempDir() repo := filepath.Join(root, "repo") git := func(args ...string) string { return gitIn(t, repo, nil, args...) } write := func(name, data string, mode os.FileMode) { p := filepath.Join(repo, name) if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(p, []byte(data), mode); err != nil { t.Fatal(err) } } gitIn(t, root, nil, "init", "-q", "-b", "main", "repo") git("config", "uploadpack.allowFilter", "true") git("config", "uploadpack.allowAnySHA1InWant", "true") write("README", "hi\n", 0o644) git("add", ".") git("commit", "-qm", "first") first := git("rev-parse", "HEAD") git("tag", "dup") write(".tangled/workflows/a.yml", "when: push\n", 0o644) write(".tangled/workflows/b.yml", "when: pr\n", 0o755) write(".tangled/workflows/sub/c.yml", "nested\n", 0o644) if err := os.Symlink("a.yml", filepath.Join(repo, ".tangled/workflows/link")); err != nil { t.Fatal(err) } git("add", ".") git("commit", "-qm", "second") second := git("rev-parse", "HEAD") git("branch", "dup") git("tag", "-am", "annotated", "v1") git("tag", "light") srv := httptest.NewServer(&cgi.Handler{ Path: gitPath, Args: []string{"http-backend"}, Env: []string{"GIT_PROJECT_ROOT=" + root, "GIT_HTTP_EXPORT_ALL=1", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null"}, }) defer srv.Close() ctx := context.Background() c := New(srv.Client(), srv.URL+"/repo", 4<<20) for ref, want := range map[string]string{ "main": second, "refs/heads/main": second, "dup": second, // branch before tag "refs/tags/dup": first, "v1": second, // peeled "light": second, } { got, err := c.Resolve(ctx, ref) if err != nil || string(got) != want { t.Errorf("Resolve(%s) = %s, %v; want %s", ref, got, err, want) } } if _, err := c.Resolve(ctx, "nope"); !errors.Is(err, ErrRefNotFound) { t.Errorf("Resolve(nope) = %v", err) } files, err := c.ReadDir(ctx, Oid(second), ".tangled/workflows") if err != nil { t.Fatal(err) } var got []string for _, f := range files { got = append(got, f.Mode+" "+f.Name+" "+string(f.Data)) } want := []string{"100644 a.yml when: push\n", "100755 b.yml when: pr\n", "120000 link a.yml", "40000 sub "} if strings.Join(got, "|") != strings.Join(want, "|") { t.Errorf("ReadDir = %q", got) } if _, err := c.ReadDir(ctx, Oid(first), ".tangled/workflows"); !errors.Is(err, ErrPathNotFound) { t.Errorf("ReadDir(first) = %v", err) } for _, dir := range []string{".tangled/nope", ".tangled/workflows/a.yml"} { if _, err := c.ReadDir(ctx, Oid(second), dir); !errors.Is(err, ErrPathNotFound) { t.Errorf("ReadDir(%s) = %v", dir, err) } } if _, err := c.ReadDir(ctx, Oid(second), ".tangled/workflows/a.yml"); !errors.Is(err, ErrNotDir) { t.Errorf("ReadDir(a.yml) = %v", err) } files, err = c.ReadTree(ctx, Oid(second), "/.tangled") if err != nil { t.Fatal(err) } got = nil for _, f := range files { got = append(got, f.Mode+" "+f.Name+" "+string(f.Data)) } want = []string{"40000 workflows ", "100644 workflows/a.yml when: push\n", "100755 workflows/b.yml when: pr\n", "120000 workflows/link a.yml", "40000 workflows/sub ", "100644 workflows/sub/c.yml nested\n"} if strings.Join(got, "|") != strings.Join(want, "|") { t.Errorf("ReadTree = %q", got) } if _, err := c.ReadTree(ctx, Oid(second), "nope"); !errors.Is(err, ErrPathNotFound) || errors.Is(err, ErrNotDir) { t.Errorf("ReadTree(nope) = %v", err) } if _, err := c.ReadTree(ctx, Oid(second), "README/x"); !errors.Is(err, ErrNotDir) { t.Errorf("ReadTree(README/x) = %v", err) } small := New(srv.Client(), srv.URL+"/repo", 64) if _, err := small.ReadDir(ctx, Oid(second), ".tangled/workflows"); !errors.Is(err, ErrTooLarge) { t.Errorf("small ReadDir = %v", err) } gone := New(srv.Client(), srv.URL+"/missing", 4<<20) if _, err := gone.Resolve(ctx, "main"); err == nil || errors.Is(err, ErrRefNotFound) { t.Errorf("Resolve on missing repo = %v", err) } // ofs and ref deltas straight from pack-objects base := strings.Repeat("line of text\n", 400) b1 := gitIn(t, repo, []byte(base), "hash-object", "-w", "--stdin") b2 := gitIn(t, repo, []byte(base+"tail\n"), "hash-object", "-w", "--stdin") for _, flag := range []string{"--delta-base-offset", "--window=10"} { cmd := exec.Command("git", "pack-objects", "--stdout", flag) cmd.Dir = repo cmd.Stdin = strings.NewReader(b1 + "\n" + b2 + "\n") pack, err := cmd.Output() if err != nil { t.Fatal(err) } objs, err := readPack(bytes.NewReader(pack), "sha1") if err != nil { t.Fatalf("%s: %v", flag, err) } if string(objs[Oid(b2)].Data) != base+"tail\n" || string(objs[Oid(b1)].Data) != base { t.Errorf("%s: bad objects", flag) } } } func TestApplyDelta(t *testing.T) { // src 5, dst 8: copy 5 bytes from 0, insert "xyz" out, err := applyDelta([]byte("hello"), []byte{5, 8, 0x90, 5, 3, 'x', 'y', 'z'}) if err != nil || string(out) != "helloxyz" { t.Fatalf("applyDelta = %q, %v", out, err) } if _, err := applyDelta([]byte("hello"), []byte{5, 8, 0x90, 6}); err == nil { t.Fatal("copy past base accepted") } } func TestPkt(t *testing.T) { var b bytes.Buffer writePkt(&b, "a\n") writeDelim(&b) writeFlush(&b) if b.String() != "0006a\n00010000" { t.Fatalf("framing %q", b.String()) } p := newPktReader(&b) for _, want := range []pktKind{pktData, pktDelim, pktFlush} { if kind, _, err := p.next(); err != nil || kind != want { t.Fatalf("next = %v, %v", kind, err) } } }