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)
}
}
}