git_test.go
package main
import (
"bytes"
"context"
"errors"
"fmt"
"path/filepath"
"reflect"
"testing"
"time"
)
// fixtureTime is the pinned author/committer moment from fixtureEnv.
var fixtureTime = time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
func TestOpenRepo(t *testing.T) {
t.Parallel()
t.Run("worktree", func(t *testing.T) {
dir := buildFixtureRepo(t)
r, err := openRepo("fixturama", repoConfig{Path: dir})
if err != nil {
t.Fatal(err)
}
if want := filepath.Join(dir, ".git"); r.gitDir != want {
t.Errorf("gitDir = %q, want %q", r.gitDir, want)
}
})
t.Run("bare", func(t *testing.T) {
src := buildFixtureRepo(t)
parent := t.TempDir()
gitFixture(t, parent, "clone", "-q", "--bare", src, "fixture.git")
bare := filepath.Join(parent, "fixture.git")
r, err := openRepo("fixturama", repoConfig{Path: bare})
if err != nil {
t.Fatal(err)
}
if r.gitDir != bare {
t.Errorf("gitDir = %q, want %q", r.gitDir, bare)
}
// The clone carries the same objects, so the same sha serves.
if sha, err := r.resolveCommit("main"); err != nil || sha != fixtureShaMain {
t.Errorf("resolveCommit(main) = %q, %v; want %s", sha, err, fixtureShaMain)
}
})
t.Run("absent and non-repo paths skip with warnings", func(t *testing.T) {
dir := buildFixtureRepo(t)
cfg := &config{Repos: map[string]repoConfig{
"fixturama": {Path: dir},
"ghost": {Path: filepath.Join(dir, "no-such-dir")},
"hollow": {Path: t.TempDir()},
}}
repos, warnings := openAll(cfg)
if len(repos) != 1 || repos["fixturama"] == nil {
t.Errorf("expected exactly fixturama to open, got %v", repos)
}
if len(warnings) != 2 {
t.Errorf("expected 2 warnings, got %q", warnings)
}
})
}
func TestResolveCommit(t *testing.T) {
t.Parallel()
r := fixtureRepo(t)
for _, c := range []struct {
ref string
sha string
want error // nil means success
}{
{"main", fixtureShaMain, nil},
{"HEAD", fixtureShaMain, nil},
{"v1.0", fixtureShaMain, nil}, // the annotated tag peels to the commit
{"dev", fixtureShaDev, nil},
{fixtureShaRoot, fixtureShaRoot, nil}, // a full sha is a fine ref
{"bogus", "", errNotFound},
{"main..dev", "", errInvalid},
{"-option", "", errInvalid},
} {
sha, err := r.resolveCommit(c.ref)
if c.want == nil {
if err != nil || sha != c.sha {
t.Errorf("resolveCommit(%q) = %q, %v; want %s", c.ref, sha, err, c.sha)
}
} else if !errors.Is(err, c.want) {
t.Errorf("resolveCommit(%q) error = %v; want %v", c.ref, err, c.want)
}
}
}
func TestDefaultBranch(t *testing.T) {
t.Parallel()
r := fixtureRepo(t)
if got := r.defaultBranch(); got != "main" {
t.Errorf("defaultBranch = %q, want main", got)
}
}
func TestRefs(t *testing.T) {
t.Parallel()
r := fixtureRepo(t)
branches, tags, err := r.refs()
if err != nil {
t.Fatal(err)
}
wantBranches := []refInfo{{"dev", fixtureShaDev}, {"main", fixtureShaMain}}
wantTags := []refInfo{{"v1.0", fixtureShaMain}}
if !reflect.DeepEqual(branches, wantBranches) {
t.Errorf("branches = %v, want %v", branches, wantBranches)
}
if !reflect.DeepEqual(tags, wantTags) {
t.Errorf("tags = %v, want %v", tags, wantTags)
}
}
func TestEntryType(t *testing.T) {
t.Parallel()
r := fixtureRepo(t)
for _, c := range []struct {
path string
typ string
want error
}{
{"", "tree", nil},
{"README.md", "blob", nil},
{"docs", "tree", nil},
{"docs/with space.txt", "blob", nil},
{"absent", "", errNotFound},
{"../escape", "", errInvalid},
} {
typ, err := r.entryType(fixtureShaMain, c.path)
if c.want == nil {
if err != nil || typ != c.typ {
t.Errorf("entryType(%q) = %q, %v; want %q", c.path, typ, err, c.typ)
}
} else if !errors.Is(err, c.want) {
t.Errorf("entryType(%q) error = %v; want %v", c.path, err, c.want)
}
}
}
func TestLsTree(t *testing.T) {
t.Parallel()
r := fixtureRepo(t)
t.Run("root", func(t *testing.T) {
entries, err := r.lsTree(fixtureShaMain, "")
if err != nil {
t.Fatal(err)
}
// Names and shape only; the blob hashes are already covered by catBlob reading content.
want := []struct {
name string
typ string
mode string
size int64
}{
{"README.md", "blob", "100644", int64(len(fixtureReadme))},
{"docs", "tree", "040000", -1},
{"src", "tree", "040000", -1},
}
if len(entries) != len(want) {
t.Fatalf("got %d entries (%v), want %d", len(entries), entries, len(want))
}
for i, w := range want {
e := entries[i]
if e.name != w.name || e.typ != w.typ || e.mode != w.mode || e.size != w.size {
t.Errorf("entry %d = %+v, want %+v", i, e, w)
}
}
})
t.Run("subdir", func(t *testing.T) {
entries, err := r.lsTree(fixtureShaMain, "docs")
if err != nil {
t.Fatal(err)
}
if len(entries) != 2 || entries[0].name != "notes.md" || entries[1].name != "with space.txt" {
t.Errorf("docs entries = %+v", entries)
}
})
t.Run("missing dir", func(t *testing.T) {
if _, err := r.lsTree(fixtureShaMain, "absent"); !errors.Is(err, errNotFound) {
t.Errorf("error = %v, want errNotFound", err)
}
})
t.Run("file is not a dir", func(t *testing.T) {
if _, err := r.lsTree(fixtureShaMain, "README.md"); !errors.Is(err, errNotFound) {
t.Errorf("error = %v, want errNotFound", err)
}
})
}
func TestBlobs(t *testing.T) {
t.Parallel()
r := fixtureRepo(t)
size, err := r.blobSize(fixtureShaMain, "README.md")
if err != nil || size != int64(len(fixtureReadme)) {
t.Errorf("blobSize = %d, %v; want %d", size, err, len(fixtureReadme))
}
var buf bytes.Buffer
if err := r.catBlob(fixtureShaMain, "README.md", &buf); err != nil {
t.Fatal(err)
}
if buf.String() != fixtureReadme {
t.Errorf("catBlob = %q, want %q", buf.String(), fixtureReadme)
}
// The dev branch rewrote the README: the same path at another sha is other bytes.
buf.Reset()
if err := r.catBlob(fixtureShaDev, "README.md", &buf); err != nil {
t.Fatal(err)
}
if buf.String() == fixtureReadme {
t.Error("catBlob at dev's sha should differ from main's README")
}
if err := r.catBlob(fixtureShaMain, "absent", &buf); !errors.Is(err, errNotFound) {
t.Errorf("catBlob(absent) error = %v, want errNotFound", err)
}
}
func TestCommits(t *testing.T) {
t.Parallel()
r := fixtureRepo(t)
got, err := r.commits("main", 100)
if err != nil {
t.Fatal(err)
}
want := []commitInfo{
{fixtureShaMain, "Ada Fixture", fixtureTime, "add docs and src"},
{fixtureShaRoot, "Ada Fixture", fixtureTime, "inaugurate with a readme"},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("commits(main) = %v, want %v", got, want)
}
if got, err := r.commits("dev", 100); err != nil || len(got) != 3 {
t.Errorf("commits(dev) = %d commits, %v; want 3", len(got), err)
}
if got, err := r.commits("main", 1); err != nil || len(got) != 1 {
t.Errorf("commits(main, 1) = %d commits, %v; want 1", len(got), err)
}
if _, err := r.commits("bogus", 1); !errors.Is(err, errNotFound) {
t.Errorf("commits(bogus) error = %v, want errNotFound", err)
}
}
func TestMatchRepo(t *testing.T) {
t.Parallel()
repos := map[string]*repo{
"parnass": {name: "parnass"},
"work/infra/deploy": {name: "work/infra/deploy"},
}
for _, c := range []struct {
urlPath string
name string // "" means no match expected
rest string
}{
{"parnass", "parnass", ""},
{"parnass/", "parnass", ""},
{"parnass/tree/main/docs", "parnass", "tree/main/docs"},
{"parnass.git", "parnass", ""},
{"parnass.git/info/refs", "parnass", "info/refs"},
{"work/infra/deploy/log/main", "work/infra/deploy", "log/main"},
{"work/infra", "", ""},
{"parn", "", ""},
{"parnassus/tree", "", ""},
{"", "", ""},
} {
r, rest := matchRepo(repos, c.urlPath)
switch {
case c.name == "" && r != nil:
t.Errorf("matchRepo(%q) = %q, want no match", c.urlPath, r.name)
case c.name != "" && (r == nil || r.name != c.name || rest != c.rest):
t.Errorf("matchRepo(%q) = %v, %q; want %q, %q", c.urlPath, r, rest, c.name, c.rest)
}
}
}
func TestUploadPack(t *testing.T) {
t.Parallel()
r := fixtureRepo(t)
ctx := context.Background()
t.Run("advertise v0", func(t *testing.T) {
var buf bytes.Buffer
if err := r.uploadPackAdvertise(ctx, &buf, ""); err != nil {
t.Fatal(err)
}
for _, want := range []string{fixtureShaMain, "refs/heads/main", "refs/tags/v1.0"} {
if !bytes.Contains(buf.Bytes(), []byte(want)) {
t.Errorf("advertisement lacks %q:\n%s", want, buf.String())
}
}
})
t.Run("advertise v2", func(t *testing.T) {
var buf bytes.Buffer
if err := r.uploadPackAdvertise(ctx, &buf, "version=2"); err != nil {
t.Fatal(err)
}
if !bytes.Contains(buf.Bytes(), []byte("version 2")) {
t.Errorf("expected a v2 capability advertisement:\n%s", buf.String())
}
})
t.Run("rpc fetch", func(t *testing.T) {
// A minimal protocol-v0 fetch request: want the tip, no haves, done.
// The reply should be a NAK followed by a packfile.
var req bytes.Buffer
pktline(&req, "want "+fixtureShaMain+"\n")
req.WriteString("0000")
pktline(&req, "done\n")
var resp bytes.Buffer
if err := r.uploadPackRPC(ctx, &req, &resp, ""); err != nil {
t.Fatal(err)
}
if !bytes.Contains(resp.Bytes(), []byte("NAK")) || !bytes.Contains(resp.Bytes(), []byte("PACK")) {
t.Errorf("expected NAK and PACK in the response, got %d bytes starting %q", resp.Len(), firstBytes(resp.Bytes(), 40))
}
})
}
// pktline writes one git pkt-line: a 4-hex-digit length prefix (including itself), then the payload.
func pktline(buf *bytes.Buffer, payload string) {
fmt.Fprintf(buf, "%04x%s", len(payload)+4, payload)
}
func firstBytes(b []byte, n int) []byte {
if len(b) < n {
return b
}
return b[:n]
}
git_test.go · 9.4 KiB · 01cc598cda