package gitx import ( "context" "fmt" "os" "os/exec" "path/filepath" "strings" "testing" ) func TestValidName(t *testing.T) { good := []string{"john", "j", "lisa-2", "a0", "x" + string(make([]byte, 0))} for _, s := range good { if !ValidName(s) { t.Errorf("ValidName(%q) = false, want true", s) } } bad := []string{ "", "-john", "John", "jo_hn", "j.hn", "../../etc", "j/n", "admin", "new", "signin", "signup", "keys", "tokens", "auth", "inbox", "runner", "raw", "search", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // 41 chars } for _, s := range bad { if ValidName(s) { t.Errorf("ValidName(%q) = true, want false", s) } } // A repository is a second path segment, so a name a route owns at the top shadows nothing. for _, s := range []string{"admin", "new", "signin", "runner", "raw", "api", "about"} { if !ValidRepoName(s) { t.Errorf("ValidRepoName(%q) = false, want a repository to be allowed that name", s) } } for _, s := range []string{"", "-john", "John", "jo_hn", "j.hn", "../../etc", "j/n", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"} { if ValidRepoName(s) { t.Errorf("ValidRepoName(%q) = true, want false", s) } } } func TestValidRef(t *testing.T) { good := []string{ "refs/heads/master", "refs/heads/main", "refs/proposals/new", "refs/proposals/7/rev/2", "refs/notes/threads/3", "refs/tags/v1.0.0", } for _, s := range good { if !ValidRef(s) { t.Errorf("ValidRef(%q) = false, want true", s) } } bad := []string{ "", "--upload-pack=/bin/sh", "refs/heads/--upload-pack=/bin/sh", "refs/heads/-x", "refs/-/x", "refs/heads/..", "refs/heads/a..b", "refs/heads/.hidden", "refs/heads/x.lock", "refs/heads/a b", "refs/heads/a\x00b", "refs/heads/a\nb", "refs/heads//x", "refs/heads/a~1", "refs/heads/a^", "refs/heads/a:b", "refs/heads/", } for _, s := range bad { if ValidRef(s) { t.Errorf("ValidRef(%q) = true, want false", s) } } } func TestValidRev(t *testing.T) { good := []string{ "master", "main", "trunk", "refs/heads/master", "refs/proposals/47", "v1.0.0", "a3f9c2", "8b1d44e", "HEAD", "HEAD~2", "master^", "feature/thing", } for _, s := range good { if !ValidRev(s) { t.Errorf("ValidRev(%q) = false, want true", s) } } bad := []string{ "", "-master", "--upload-pack=/bin/sh", "master..main", "master...main", "refs/heads/-x", "master; rm -rf /", "master rm", "a\x00b", "$(rm -rf /)", "`id`", "refs//x", "refs/.hidden/x", // reflog syntax reaches unreferenced objects, and has no business arriving from a url. "HEAD@{0}", "master@{yesterday}", } for _, s := range bad { if ValidRev(s) { t.Errorf("ValidRev(%q) = true, want false", s) } } } func TestValidPath(t *testing.T) { good := []string{"config.go", "irc/conn.go", ".barerepo/config", "a/b/c/d.txt", "README"} for _, s := range good { if !ValidPath(s) { t.Errorf("ValidPath(%q) = false, want true", s) } } bad := []string{"", "/etc/passwd", "../../etc/passwd", "a/../../b", "a//b", "./x", "-rf", "a\x00b"} for _, s := range bad { if ValidPath(s) { t.Errorf("ValidPath(%q) = true, want false", s) } } } // Batch must skip an object that is not there without losing its place in the stream. func TestBatchReadsManyObjectsAndAMissingOne(t *testing.T) { dir := t.TempDir() for _, args := range [][]string{ {"init", "-q", "--bare", "-b", "master", dir}, } { if out, err := exec.Command("git", args...).CombinedOutput(); err != nil { t.Fatalf("git %v: %v: %s", args, err, out) } } ctx := context.Background() want := map[string]string{} var specs []string for _, body := range []string{"one", "two\nlines", "three\x00with a nul"} { sha, err := RunStdin(ctx, dir, body, "hash-object", "-w", "--stdin") if err != nil { t.Fatal(err) } sha = strings.TrimSpace(sha) specs = append(specs, sha) want[sha] = body } // A spec git cannot resolve must not shift the answers that follow it. specs = append([]string{"0000000000000000000000000000000000000000"}, specs...) got, err := Batch(ctx, dir, specs) if err != nil { t.Fatal(err) } if len(got) != len(want) { t.Fatalf("asked for %d objects and one miss, got %d back", len(want), len(got)) } for sha, body := range want { obj := got[sha] if obj == nil { t.Fatalf("%s is missing from the batch", sha) } if obj.Body != body { t.Errorf("%s: body is %q, want %q", sha, obj.Body, body) } if obj.Type != "blob" { t.Errorf("%s: type is %q, want blob", sha, obj.Type) } } } // TreeEntries reads twenty binary bytes per entry, so a drifting read loses all the rest. func TestTreeEntriesReadsARealTree(t *testing.T) { dir := t.TempDir() if out, err := exec.Command("git", "init", "-q", "--bare", "-b", "master", dir).CombinedOutput(); err != nil { t.Fatalf("git init: %v: %s", err, out) } ctx := context.Background() var mktree strings.Builder want := map[string]string{} for _, name := range []string{"meta", "0a1b2c3d", "zz-last"} { sha, err := RunStdin(ctx, dir, "body of "+name, "hash-object", "-w", "--stdin") if err != nil { t.Fatal(err) } sha = strings.TrimSpace(sha) want[name] = sha fmt.Fprintf(&mktree, "100644 blob %s\t%s\n", sha, name) } tree, err := RunStdin(ctx, dir, mktree.String(), "mktree") if err != nil { t.Fatal(err) } obj, err := Batch(ctx, dir, []string{strings.TrimSpace(tree)}) if err != nil { t.Fatal(err) } got := TreeEntries(obj[strings.TrimSpace(tree)].Body) if len(got) != len(want) { t.Fatalf("tree has %d entries, want %d: %v", len(got), len(want), got) } for name, sha := range want { if got[name] != sha { t.Errorf("%s points at %q, want %q", name, got[name], sha) } } } // HEAD holds "ref: refs/heads/master", so a resolver that returns the file returns a name, not a hash. func TestResolveRefFollowsSymbolicRefs(t *testing.T) { dir := t.TempDir() sha := "a3f9c2db1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b" write := func(path, body string) { t.Helper() full := filepath.Join(dir, filepath.FromSlash(path)) if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(full, []byte(body), 0o644); err != nil { t.Fatal(err) } } write("refs/heads/master", sha+"\n") write("HEAD", "ref: refs/heads/master\n") for _, ref := range []string{"HEAD", "refs/heads/master"} { got, err := ResolveRef(dir, ref) if err != nil { t.Fatalf("ResolveRef(%s): %v", ref, err) } if got != sha { t.Errorf("ResolveRef(%s) = %q, want the hash %q", ref, got, sha) } } // A ref file pointing at itself is a file somebody wrote, and it must not spin. write("refs/heads/loop", "ref: refs/heads/loop\n") if _, err := ResolveRef(dir, "refs/heads/loop"); err == nil { t.Error("a self-referential ref resolved instead of erroring") } } // A repository with no refs is the empty page, and asking git that question cost a whole process. func TestAnyRefSeesLooseAndPackedRefs(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } dir := t.TempDir() mustRun(t, "", "git", "init", "-q", "--bare", "-b", "master", dir) if AnyRef(dir) { t.Fatal("a fresh bare repository reported a ref") } work := t.TempDir() mustRun(t, "", "git", "init", "-q", "-b", "master", work) if err := os.WriteFile(filepath.Join(work, "f"), []byte("hi\n"), 0o644); err != nil { t.Fatal(err) } mustRun(t, work, "git", "add", "-A") mustRun(t, work, "git", "-c", "user.email=t@x", "-c", "user.name=t", "commit", "-qm", "first") mustRun(t, work, "git", "push", "-q", dir, "master") if !AnyRef(dir) { t.Fatal("a pushed branch was not seen as a ref") } // git gc moves loose refs into packed-refs, and a repository is not empty because of that. mustRun(t, dir, "git", "pack-refs", "--all") if _, err := os.Stat(filepath.Join(dir, "refs", "heads", "master")); err == nil { t.Skip("this git kept the loose ref, so the packed path is untested here") } if !AnyRef(dir) { t.Fatal("a packed ref was not seen, so a full repository draws the empty page") } } func mustRun(t *testing.T, dir, name string, args ...string) { t.Helper() cmd := exec.Command(name, args...) cmd.Dir = dir if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("%s %v: %v\n%s", name, args, err, out) } }