socle

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
		}{
			{".gitignore", "blob", "100644", 7},
			{"README.md", "blob", "100644", int64(len(fixtureReadme))},
			{"docs", "tree", "040000", -1},
			{"src", "tree", "040000", -1},
			{"vendor", "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.5 KiB · d3c2758b67