package gitrepo import ( "errors" "os" "os/exec" "path/filepath" "testing" "time" "github.com/go-git/go-billy/v5/osfs" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" "gitweb/internal/store" ) // makeBareRepo builds a real bare repo at // via the git CLI // and returns the store-relative repo path "/". Skips the test if // git is unavailable. func makeBareRepo(t *testing.T, root, user, name string) string { t.Helper() if _, err := exec.LookPath("git"); err != nil { t.Skip("git not available") } work := t.TempDir() run := func(dir string, args ...string) { t.Helper() cmd := exec.Command("git", args...) cmd.Dir = dir cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=Test", "GIT_AUTHOR_EMAIL=test@example.com", "GIT_COMMITTER_NAME=Test", "GIT_COMMITTER_EMAIL=test@example.com", ) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } } run(work, "init", "-b", "main") if err := os.WriteFile(filepath.Join(work, "README.md"), []byte("# Demo\n\nhi\n"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(work, "main.go"), []byte("package main\n\nfunc main() {}\n"), 0o644); err != nil { t.Fatal(err) } run(work, "add", ".") run(work, "commit", "-m", "initial commit") run(work, "checkout", "-b", "feature/x") if err := os.WriteFile(filepath.Join(work, "main.go"), []byte("package main\n\nfunc main() { _ = 1 }\n"), 0o644); err != nil { t.Fatal(err) } run(work, "commit", "-am", "tweak main") run(work, "tag", "v1.0.0") run(work, "checkout", "main") bare := filepath.Join(root, user, name) if err := os.MkdirAll(filepath.Dir(bare), 0o755); err != nil { t.Fatal(err) } run(work, "clone", "--bare", work, bare) return user + "/" + name } // newTestCache builds a Cache over st rooted at a test tempdir. func newTestCache(t *testing.T, st store.Store, opts CacheOptions) *Cache { t.Helper() if opts.Dir == "" { opts.Dir = t.TempDir() } c, err := NewCache(st, opts) if err != nil { t.Fatalf("NewCache: %v", err) } return c } func TestMaterializeAndOpen(t *testing.T) { root := t.TempDir() repoPath := makeBareRepo(t, root, "u@d.com", "demo.git") st := store.NewLocalStore(root) c := newTestCache(t, st, CacheOptions{}) repo, release, err := c.Get(repoPath) if err != nil { t.Fatalf("Get: %v", err) } defer release() head, err := repo.Head() if err != nil { t.Fatalf("Head: %v", err) } if head.Name().Short() != "main" { t.Fatalf("head = %q, want main", head.Name().Short()) } // Log walks. iter, err := repo.Log(&git.LogOptions{From: head.Hash()}) if err != nil { t.Fatalf("Log: %v", err) } var n int err = iter.ForEach(func(*object.Commit) error { n++; return nil }) if err != nil { t.Fatalf("Log iter: %v", err) } if n == 0 { t.Fatal("expected at least one commit") } // Tree walks: README.md present at HEAD. commit, err := repo.CommitObject(head.Hash()) if err != nil { t.Fatalf("CommitObject: %v", err) } tree, err := commit.Tree() if err != nil { t.Fatalf("Tree: %v", err) } if _, err := tree.File("README.md"); err != nil { t.Fatalf("README.md not in tree: %v", err) } // Branch + tag are visible. if _, err := repo.Reference("refs/heads/feature/x", false); err != nil { t.Fatalf("feature/x branch missing: %v", err) } if _, err := repo.Tag("v1.0.0"); err != nil { t.Fatalf("v1.0.0 tag missing: %v", err) } } func TestMaterializeToDisk(t *testing.T) { root := t.TempDir() repoPath := makeBareRepo(t, root, "u@d.com", "demo.git") st := store.NewLocalStore(root) dir := t.TempDir() man, err := materialize(st, repoPath, osfs.New(dir), defaultMaxRepoBytes) if err != nil { t.Fatalf("materialize: %v", err) } // The bare-repo layout lands on the real filesystem. if _, err := os.Stat(filepath.Join(dir, "HEAD")); err != nil { t.Fatalf("HEAD not on disk: %v", err) } if fi, err := os.Stat(filepath.Join(dir, "objects")); err != nil || !fi.IsDir() { t.Fatalf("objects/ not a dir on disk: %v", err) } // Manifest entries match what is on disk. for name, info := range man { fi, err := os.Stat(filepath.Join(dir, filepath.FromSlash(name))) if err != nil { t.Fatalf("manifest file %s missing on disk: %v", name, err) } if fi.Size() != info.Size { t.Errorf("%s: size %d on disk, %d in manifest", name, fi.Size(), info.Size) } } } func TestMaterializeTooLarge(t *testing.T) { root := t.TempDir() repoPath := makeBareRepo(t, root, "u@d.com", "demo.git") st := store.NewLocalStore(root) _, err := materialize(st, repoPath, osfs.New(t.TempDir()), 10) if !errors.Is(err, ErrRepoTooLarge) { t.Fatalf("err = %v, want ErrRepoTooLarge", err) } } func TestCacheReusesAndInvalidates(t *testing.T) { root := t.TempDir() repoPath := makeBareRepo(t, root, "u@d.com", "demo.git") st := store.NewLocalStore(root) c := newTestCache(t, st, CacheOptions{}) r1, release1, err := c.Get(repoPath) if err != nil { t.Fatalf("Get: %v", err) } defer release1() r2, release2, err := c.Get(repoPath) if err != nil { t.Fatalf("Get: %v", err) } defer release2() if r1 != r2 { t.Fatal("expected cached repo to be reused (same pointer)") } // Bump the repo's mtime so LocalStore.Seq changes -> re-materialization. later := time.Now().Add(2 * time.Second) if err := os.Chtimes(filepath.Join(root, repoPath, "HEAD"), later, later); err != nil { t.Fatal(err) } r3, release3, err := c.Get(repoPath) if err != nil { t.Fatalf("Get: %v", err) } defer release3() if r3 == r1 { t.Fatal("expected re-materialization after edit (different pointer)") } }