package gitexec
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func run(t *testing.T, dir, name string, args ...string) {
t.Helper()
cmd := exec.Command(name, 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("%s %v: %v\n%s", name, args, err, out)
}
}
func TestReadFileAtHEADAndListTags(t *testing.T) {
tmp := t.TempDir()
barePath := filepath.Join(tmp, "repo.git")
if err := InitBareRepo(barePath); err != nil {
t.Fatalf("init bare repo: %v", err)
}
work := filepath.Join(tmp, "work")
if err := os.Mkdir(work, 0755); err != nil {
t.Fatal(err)
}
run(t, work, "git", "init", "-q", "-b", "main")
if err := os.WriteFile(filepath.Join(work, "README.md"), []byte("# Hello\n"), 0644); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(filepath.Join(work, "cmd"), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(work, "cmd", "main.go"), []byte("package main\n"), 0644); err != nil {
t.Fatal(err)
}
run(t, work, "git", "add", "README.md", "cmd/main.go")
run(t, work, "git", "commit", "-q", "-m", "initial")
run(t, work, "git", "tag", "v1.0.0")
run(t, work, "git", "remote", "add", "origin", barePath)
run(t, work, "git", "push", "-q", "origin", "main", "--tags")
content, found, err := ReadFileAtHEAD(barePath, "README.md")
if err != nil {
t.Fatalf("ReadFileAtHEAD: %v", err)
}
if !found {
t.Fatal("README.md should be found at HEAD")
}
if content != "# Hello\n" {
t.Fatalf("content = %q, want %q", content, "# Hello\n")
}
_, found, err = ReadFileAtHEAD(barePath, "LICENSE")
if err != nil {
t.Fatalf("ReadFileAtHEAD(LICENSE): %v", err)
}
if found {
t.Fatal("LICENSE should not be found (never committed)")
}
tags, err := ListTags(barePath)
if err != nil {
t.Fatalf("ListTags: %v", err)
}
if len(tags) != 1 || tags[0] != "v1.0.0" {
t.Fatalf("tags = %v, want [v1.0.0]", tags)
}
root, found, err := ListTree(barePath, "")
if err != nil {
t.Fatalf("ListTree root: %v", err)
}
if !found {
t.Fatal("root tree should be found")
}
if len(root) != 2 || root[0].Name != "cmd" || root[0].Type != "tree" || root[1].Name != "README.md" || root[1].Type != "blob" {
t.Fatalf("root tree = %+v, want [cmd(tree) README.md(blob)]", root)
}
sub, found, err := ListTree(barePath, "cmd")
if err != nil {
t.Fatalf("ListTree cmd: %v", err)
}
if !found || len(sub) != 1 || sub[0].Name != "main.go" || sub[0].Type != "blob" {
t.Fatalf("cmd tree = %+v found=%v, want [main.go(blob)]", sub, found)
}
_, found, err = ListTree(barePath, "nope")
if err != nil {
t.Fatalf("ListTree nonexistent path: %v", err)
}
if found {
t.Fatal("nonexistent path should not be found")
}
}
func TestReadFileAtHEADEmptyRepo(t *testing.T) {
tmp := t.TempDir()
barePath := filepath.Join(tmp, "repo.git")
if err := InitBareRepo(barePath); err != nil {
t.Fatalf("init bare repo: %v", err)
}
_, found, err := ReadFileAtHEAD(barePath, "README.md")
if err != nil {
t.Fatalf("ReadFileAtHEAD on empty repo: %v", err)
}
if found {
t.Fatal("empty repo should never report a file as found")
}
tags, err := ListTags(barePath)
if err != nil {
t.Fatalf("ListTags on empty repo: %v", err)
}
if len(tags) != 0 {
t.Fatalf("tags = %v, want none", tags)
}
_, found, err = ListTree(barePath, "")
if err != nil {
t.Fatalf("ListTree on empty repo: %v", err)
}
if found {
t.Fatal("empty repo should never report a tree as found")
}
}
// TestPushToMismatchedDefaultBranch reproduces a real bug: InitBareRepo
// points HEAD at refs/heads/main, but nothing stops a client from pushing
// under a different branch name (an older git installation still defaults
// to "master", or someone names their trunk something else entirely). HEAD
// then never resolves even though the repo plainly has content — this is
// the exact scenario that made every freshly pushed repo 404 in gitfed-web.
// TestDefaultBranchNameDanglingHeadPrefersMainOrMaster reproduces the real
// case found live: a bare repo whose HEAD points at "main" (InitBareRepo's
// default) but which only ever had "master" pushed to it — with more than
// one branch present so the old single-branch fallback in resolveDefaultRef
// didn't apply and the repo looked empty when browsing it.
func TestDefaultBranchNameDanglingHeadPrefersMainOrMaster(t *testing.T) {
tmp := t.TempDir()
barePath := filepath.Join(tmp, "repo.git")
if err := InitBareRepo(barePath); err != nil {
t.Fatalf("init bare repo: %v", err)
}
work := filepath.Join(tmp, "work")
if err := os.Mkdir(work, 0755); err != nil {
t.Fatal(err)
}
run(t, work, "git", "init", "-q", "-b", "master") // never "main"
if err := os.WriteFile(filepath.Join(work, "f"), []byte("x"), 0644); err != nil {
t.Fatal(err)
}
run(t, work, "git", "add", "f")
run(t, work, "git", "commit", "-q", "-m", "initial")
run(t, work, "git", "remote", "add", "origin", barePath)
run(t, work, "git", "push", "-q", "origin", "master")
// A second branch, so the old "exactly one branch" fallback doesn't
// mask what's being tested here.
run(t, work, "git", "checkout", "-q", "-b", "some-feature")
run(t, work, "git", "push", "-q", "origin", "some-feature")
if exec.Command("git", "--git-dir="+barePath, "rev-parse", "--verify", "-q", "HEAD").Run() == nil {
t.Fatal("test setup invalid: HEAD unexpectedly resolves, mismatch not reproduced")
}
branch, ok := DefaultBranchName(barePath)
if !ok || branch != "master" {
t.Fatalf("DefaultBranchName: got %q ok=%v, want \"master\" (preferred over the arbitrary \"some-feature\")", branch, ok)
}
root, found, err := ListTree(barePath, "")
if err != nil {
t.Fatalf("ListTree: %v", err)
}
if !found || len(root) != 1 || root[0].Name != "f" {
t.Fatalf("ListTree: found=%v root=%+v, want [f] — the repo must not look empty", found, root)
}
}
// TestDefaultBranchNameDanglingHeadNoConventionalName covers the remaining
// case: HEAD dangling, several branches, and none named "main" or
// "master" — an arbitrary but deterministic pick beats reporting the repo
// as empty.
func TestDefaultBranchNameDanglingHeadNoConventionalName(t *testing.T) {
tmp := t.TempDir()
barePath := filepath.Join(tmp, "repo.git")
if err := InitBareRepo(barePath); err != nil {
t.Fatalf("init bare repo: %v", err)
}
work := filepath.Join(tmp, "work")
if err := os.Mkdir(work, 0755); err != nil {
t.Fatal(err)
}
run(t, work, "git", "init", "-q", "-b", "zeta")
if err := os.WriteFile(filepath.Join(work, "f"), []byte("x"), 0644); err != nil {
t.Fatal(err)
}
run(t, work, "git", "add", "f")
run(t, work, "git", "commit", "-q", "-m", "initial")
run(t, work, "git", "remote", "add", "origin", barePath)
run(t, work, "git", "push", "-q", "origin", "zeta")
run(t, work, "git", "checkout", "-q", "-b", "alpha")
run(t, work, "git", "push", "-q", "origin", "alpha")
branch, ok := DefaultBranchName(barePath)
if !ok || branch != "alpha" {
t.Fatalf("DefaultBranchName: got %q ok=%v, want \"alpha\" (alphabetically first of alpha/zeta)", branch, ok)
}
}
func TestPushToMismatchedDefaultBranch(t *testing.T) {
tmp := t.TempDir()
barePath := filepath.Join(tmp, "repo.git")
if err := InitBareRepo(barePath); err != nil {
t.Fatalf("init bare repo: %v", err)
}
work := filepath.Join(tmp, "work")
if err := os.Mkdir(work, 0755); err != nil {
t.Fatal(err)
}
run(t, work, "git", "init", "-q", "-b", "trunk") // deliberately not "main"
if err := os.WriteFile(filepath.Join(work, "README.md"), []byte("# Hello\n"), 0644); err != nil {
t.Fatal(err)
}
run(t, work, "git", "add", "README.md")
run(t, work, "git", "commit", "-q", "-m", "initial")
run(t, work, "git", "remote", "add", "origin", barePath)
run(t, work, "git", "push", "-q", "origin", "trunk")
// Sanity check this test actually exercises the mismatch: HEAD must
// NOT resolve on its own, or this isn't testing the fallback at all.
if exec.Command("git", "--git-dir="+barePath, "rev-parse", "--verify", "-q", "HEAD").Run() == nil {
t.Fatal("test setup invalid: HEAD unexpectedly resolves, mismatch not reproduced")
}
content, found, err := ReadFileAtHEAD(barePath, "README.md")
if err != nil {
t.Fatalf("ReadFileAtHEAD: %v", err)
}
if !found || content != "# Hello\n" {
t.Fatalf("ReadFileAtHEAD: found=%v content=%q, want found with content", found, content)
}
root, found, err := ListTree(barePath, "")
if err != nil {
t.Fatalf("ListTree: %v", err)
}
if !found || len(root) != 1 || root[0].Name != "README.md" {
t.Fatalf("ListTree: found=%v root=%+v, want [README.md]", found, root)
}
branch, ok := DefaultBranchName(barePath)
if !ok || branch != "trunk" {
t.Fatalf("DefaultBranchName: got %q ok=%v, want \"trunk\"", branch, ok)
}
}
// TestCloneMirrorFixesDanglingHead reproduces the real bug found on
// production: a mirrored source whose own HEAD points at a branch that
// doesn't actually exist among what got mirrored (e.g. renamed master to
// main upstream, HEAD metadata never caught up) produces a bare repo git
// upload-pack refuses to advertise a symref for, breaking `git clone` for
// every downstream client even though the content is perfectly fine.
// CloneMirror must repoint HEAD at the one real branch, not just leave it
// dangling for resolveDefaultRef's read-time fallback to paper over.
func TestCloneMirrorFixesDanglingHead(t *testing.T) {
tmp := t.TempDir()
sourcePath := filepath.Join(tmp, "source.git")
if err := InitBareRepo(sourcePath); err != nil {
t.Fatalf("init source bare repo: %v", err)
}
work := filepath.Join(tmp, "work")
if err := os.Mkdir(work, 0755); err != nil {
t.Fatal(err)
}
run(t, work, "git", "init", "-q", "-b", "trunk") // deliberately not "main"
if err := os.WriteFile(filepath.Join(work, "README.md"), []byte("# Hello\n"), 0644); err != nil {
t.Fatal(err)
}
run(t, work, "git", "add", "README.md")
run(t, work, "git", "commit", "-q", "-m", "initial")
run(t, work, "git", "remote", "add", "origin", sourcePath)
run(t, work, "git", "push", "-q", "origin", "trunk")
// Sanity check the source itself has the dangling-HEAD problem, or this
// test isn't exercising the fix at all.
if exec.Command("git", "--git-dir="+sourcePath, "rev-parse", "--verify", "-q", "HEAD").Run() == nil {
t.Fatal("test setup invalid: source HEAD unexpectedly resolves, mismatch not reproduced")
}
mirrorPath := filepath.Join(tmp, "mirror.git")
if err := CloneMirror(context.Background(), mirrorPath, sourcePath); err != nil {
t.Fatalf("CloneMirror: %v", err)
}
if err := exec.Command("git", "--git-dir="+mirrorPath, "rev-parse", "--verify", "-q", "HEAD").Run(); err != nil {
t.Fatal("mirrored repo's HEAD still doesn't resolve — a real git client's `git clone` against this repo would fail")
}
branch, ok := DefaultBranchName(mirrorPath)
if !ok || branch != "trunk" {
t.Fatalf("DefaultBranchName: got %q ok=%v, want \"trunk\"", branch, ok)
}
}
func TestCountCommits(t *testing.T) {
tmp := t.TempDir()
barePath := filepath.Join(tmp, "repo.git")
if err := InitBareRepo(barePath); err != nil {
t.Fatalf("init bare repo: %v", err)
}
count, found, err := CountCommits(barePath)
if err != nil {
t.Fatalf("CountCommits on empty repo: %v", err)
}
if found {
t.Fatalf("empty repo should report found=false, got count=%d", count)
}
work := filepath.Join(tmp, "work")
if err := os.Mkdir(work, 0755); err != nil {
t.Fatal(err)
}
run(t, work, "git", "init", "-q", "-b", "main")
for i := 0; i < 3; i++ {
if err := os.WriteFile(filepath.Join(work, "f"), []byte{byte('a' + i)}, 0644); err != nil {
t.Fatal(err)
}
run(t, work, "git", "add", "f")
run(t, work, "git", "commit", "-q", "-m", "commit")
}
run(t, work, "git", "remote", "add", "origin", barePath)
run(t, work, "git", "push", "-q", "origin", "main")
count, found, err = CountCommits(barePath)
if err != nil {
t.Fatalf("CountCommits: %v", err)
}
if !found || count != 3 {
t.Fatalf("CountCommits: found=%v count=%d, want found=true count=3", found, count)
}
}
// TestListTreeAtRefAndReadFileAtRef reproduces the actual use case: a repo
// with two branches whose content genuinely differs, confirming
// ListTreeAtRef/ReadFileAtRef read the requested branch — not just
// whatever the default branch happens to be (the bug it'd be easy to
// introduce by accidentally calling resolveDefaultRef internally again).
func TestListTreeAtRefAndReadFileAtRef(t *testing.T) {
tmp := t.TempDir()
barePath := filepath.Join(tmp, "repo.git")
if err := InitBareRepo(barePath); err != nil {
t.Fatalf("init bare repo: %v", err)
}
work := filepath.Join(tmp, "work")
if err := os.Mkdir(work, 0755); err != nil {
t.Fatal(err)
}
run(t, work, "git", "init", "-q", "-b", "main")
if err := os.WriteFile(filepath.Join(work, "shared.txt"), []byte("on main\n"), 0644); err != nil {
t.Fatal(err)
}
run(t, work, "git", "add", "shared.txt")
run(t, work, "git", "commit", "-q", "-m", "main commit")
run(t, work, "git", "remote", "add", "origin", barePath)
run(t, work, "git", "push", "-q", "origin", "main")
run(t, work, "git", "checkout", "-q", "-b", "feature")
if err := os.WriteFile(filepath.Join(work, "shared.txt"), []byte("on feature\n"), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(work, "only-on-feature.txt"), []byte("x"), 0644); err != nil {
t.Fatal(err)
}
run(t, work, "git", "add", "shared.txt", "only-on-feature.txt")
run(t, work, "git", "commit", "-q", "-m", "feature commit")
run(t, work, "git", "push", "-q", "origin", "feature")
// ListTreeAtRef on "feature" sees the file that only exists there.
entries, found, err := ListTreeAtRef(barePath, "feature", "")
if err != nil {
t.Fatalf("ListTreeAtRef(feature): %v", err)
}
if !found {
t.Fatal("expected found=true for feature branch root")
}
var sawFeatureFile bool
for _, e := range entries {
if e.Name == "only-on-feature.txt" {
sawFeatureFile = true
}
}
if !sawFeatureFile {
t.Fatalf("ListTreeAtRef(feature) = %+v, expected only-on-feature.txt", entries)
}
// ListTree (default branch, "main") must NOT see it.
entries, _, err = ListTree(barePath, "")
if err != nil {
t.Fatalf("ListTree: %v", err)
}
for _, e := range entries {
if e.Name == "only-on-feature.txt" {
t.Fatalf("ListTree (default branch) unexpectedly saw feature-only file: %+v", entries)
}
}
// ReadFileAtRef reads the version of shared.txt specific to each branch.
content, found, err := ReadFileAtRef(barePath, "feature", "shared.txt")
if err != nil || !found {
t.Fatalf("ReadFileAtRef(feature): found=%v err=%v", found, err)
}
if content != "on feature\n" {
t.Fatalf("ReadFileAtRef(feature) = %q, want %q", content, "on feature\n")
}
content, found, err = ReadFileAtRef(barePath, "main", "shared.txt")
if err != nil || !found {
t.Fatalf("ReadFileAtRef(main): found=%v err=%v", found, err)
}
if content != "on main\n" {
t.Fatalf("ReadFileAtRef(main) = %q, want %q", content, "on main\n")
}
}
func TestArchive(t *testing.T) {
tmp := t.TempDir()
barePath := filepath.Join(tmp, "repo.git")
if err := InitBareRepo(barePath); err != nil {
t.Fatalf("init bare repo: %v", err)
}
work := filepath.Join(tmp, "work")
if err := os.Mkdir(work, 0755); err != nil {
t.Fatal(err)
}
run(t, work, "git", "init", "-q", "-b", "main")
if err := os.WriteFile(filepath.Join(work, "hello.txt"), []byte("hi\n"), 0644); err != nil {
t.Fatal(err)
}
run(t, work, "git", "add", "hello.txt")
run(t, work, "git", "commit", "-q", "-m", "initial")
run(t, work, "git", "tag", "v1.0.0")
run(t, work, "git", "remote", "add", "origin", barePath)
run(t, work, "git", "push", "-q", "origin", "main", "--tags")
for _, format := range []string{"tar.gz", "zip"} {
t.Run(format, func(t *testing.T) {
for _, ref := range []string{"main", "v1.0.0"} {
data, err := Archive(barePath, ref, format)
if err != nil {
t.Fatalf("Archive(%s, %s): %v", ref, format, err)
}
if len(data) == 0 {
t.Fatalf("Archive(%s, %s) returned no data", ref, format)
}
// Real magic-byte check, not just "non-empty" — gzip
// starts 0x1f 0x8b, zip starts "PK".
switch format {
case "tar.gz":
if len(data) < 2 || data[0] != 0x1f || data[1] != 0x8b {
t.Errorf("Archive(%s, tar.gz) doesn't look like gzip: % x", ref, data[:min(4, len(data))])
}
case "zip":
if len(data) < 2 || data[0] != 'P' || data[1] != 'K' {
t.Errorf("Archive(%s, zip) doesn't look like a zip: % x", ref, data[:min(4, len(data))])
}
}
}
})
}
}
func TestArchiveUnsupportedFormat(t *testing.T) {
tmp := t.TempDir()
barePath := filepath.Join(tmp, "repo.git")
if err := InitBareRepo(barePath); err != nil {
t.Fatalf("init bare repo: %v", err)
}
if _, err := Archive(barePath, "main", "tar.bz2"); err == nil {
t.Fatal("expected an error for an unsupported format")
}
}
func TestCountContributors(t *testing.T) {
tmp := t.TempDir()
barePath := filepath.Join(tmp, "repo.git")
if err := InitBareRepo(barePath); err != nil {
t.Fatalf("init bare repo: %v", err)
}
count, found, err := CountContributors(barePath)
if err != nil {
t.Fatalf("CountContributors on empty repo: %v", err)
}
if found {
t.Fatalf("empty repo should report found=false, got count=%d", count)
}
work := filepath.Join(tmp, "work")
if err := os.Mkdir(work, 0755); err != nil {
t.Fatal(err)
}
run(t, work, "git", "init", "-q", "-b", "main")
commitAs := func(name, email, file string) {
if err := os.WriteFile(filepath.Join(work, file), []byte("x"), 0644); err != nil {
t.Fatal(err)
}
cmd := exec.Command("git", "add", file)
cmd.Dir = work
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git add: %v\n%s", err, out)
}
cmd = exec.Command("git", "commit", "-q", "-m", "commit by "+name,
"--author="+name+" <"+email+">")
cmd.Dir = work
cmd.Env = append(os.Environ(),
"GIT_AUTHOR_NAME="+name, "GIT_AUTHOR_EMAIL="+email,
"GIT_COMMITTER_NAME="+name, "GIT_COMMITTER_EMAIL="+email,
)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git commit: %v\n%s", err, out)
}
}
commitAs("Alice", "alice@example.com", "a.txt")
commitAs("Bob", "bob@example.com", "b.txt")
commitAs("Alice", "alice@example.com", "a2.txt") // same author again
run(t, work, "git", "remote", "add", "origin", barePath)
run(t, work, "git", "push", "-q", "origin", "main")
count, found, err = CountContributors(barePath)
if err != nil {
t.Fatalf("CountContributors: %v", err)
}
if !found || count != 2 {
t.Fatalf("CountContributors: found=%v count=%d, want found=true count=2 (Alice x2, Bob x1)", found, count)
}
}
func TestDominantLanguage(t *testing.T) {
tmp := t.TempDir()
barePath := filepath.Join(tmp, "repo.git")
if err := InitBareRepo(barePath); err != nil {
t.Fatalf("init bare repo: %v", err)
}
lang, found, err := DominantLanguage(barePath)
if err != nil {
t.Fatalf("DominantLanguage on empty repo: %v", err)
}
if found {
t.Fatalf("empty repo should report found=false, got lang=%q", lang)
}
work := filepath.Join(tmp, "work")
if err := os.Mkdir(work, 0755); err != nil {
t.Fatal(err)
}
run(t, work, "git", "init", "-q", "-b", "main")
files := []string{"main.go", "util.go", "helper.go", "README.md", "script.py"}
for _, f := range files {
if err := os.WriteFile(filepath.Join(work, f), []byte("x"), 0644); err != nil {
t.Fatal(err)
}
}
run(t, work, "git", "add", ".")
run(t, work, "git", "commit", "-q", "-m", "initial")
run(t, work, "git", "remote", "add", "origin", barePath)
run(t, work, "git", "push", "-q", "origin", "main")
lang, found, err = DominantLanguage(barePath)
if err != nil {
t.Fatalf("DominantLanguage: %v", err)
}
if !found || lang != "Go" {
t.Fatalf("DominantLanguage: found=%v lang=%q, want found=true lang=\"Go\" (3 .go files vs 1 .py, README.md unrecognized)", found, lang)
}
}
func TestDominantLanguageNoRecognizedFiles(t *testing.T) {
tmp := t.TempDir()
barePath := filepath.Join(tmp, "repo.git")
if err := InitBareRepo(barePath); err != nil {
t.Fatalf("init bare repo: %v", err)
}
work := filepath.Join(tmp, "work")
if err := os.Mkdir(work, 0755); err != nil {
t.Fatal(err)
}
run(t, work, "git", "init", "-q", "-b", "main")
if err := os.WriteFile(filepath.Join(work, "README.md"), []byte("x"), 0644); err != nil {
t.Fatal(err)
}
run(t, work, "git", "add", ".")
run(t, work, "git", "commit", "-q", "-m", "initial")
run(t, work, "git", "remote", "add", "origin", barePath)
run(t, work, "git", "push", "-q", "origin", "main")
lang, found, err := DominantLanguage(barePath)
if err != nil {
t.Fatalf("DominantLanguage: %v", err)
}
if found {
t.Fatalf("expected no recognized language, got %q", lang)
}
}
func TestDiskUsage(t *testing.T) {
tmp := t.TempDir()
if err := os.WriteFile(filepath.Join(tmp, "a"), make([]byte, 100), 0644); err != nil {
t.Fatal(err)
}
sub := filepath.Join(tmp, "objects", "pack")
if err := os.MkdirAll(sub, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(sub, "b"), make([]byte, 250), 0644); err != nil {
t.Fatal(err)
}
// A subdirectory itself has some size on most filesystems, but
// DiskUsage must count regular files only — otherwise the total would
// vary by filesystem/OS instead of being the exact byte count below.
got, err := DiskUsage(tmp)
if err != nil {
t.Fatalf("DiskUsage: %v", err)
}
if got != 350 {
t.Fatalf("DiskUsage = %d, want 350 (100 + 250 across a nested dir)", got)
}
}
func TestDiskUsageMissingPath(t *testing.T) {
if _, err := DiskUsage(filepath.Join(t.TempDir(), "does-not-exist")); err == nil {
t.Fatal("DiskUsage on a missing path: want an error, got nil")
}
}
func TestListCommitsPage(t *testing.T) {
tmp := t.TempDir()
barePath := filepath.Join(tmp, "repo.git")
if err := InitBareRepo(barePath); err != nil {
t.Fatalf("init bare repo: %v", err)
}
work := filepath.Join(tmp, "work")
if err := os.Mkdir(work, 0755); err != nil {
t.Fatal(err)
}
run(t, work, "git", "init", "-q", "-b", "main")
// 5 commits, subjects "commit 0" (oldest) .. "commit 4" (newest).
for i := 0; i < 5; i++ {
if err := os.WriteFile(filepath.Join(work, "f"), []byte{byte(i)}, 0644); err != nil {
t.Fatal(err)
}
run(t, work, "git", "add", "f")
run(t, work, "git", "commit", "-q", "-m", fmt.Sprintf("commit %d", i))
}
run(t, work, "git", "remote", "add", "origin", barePath)
run(t, work, "git", "push", "-q", "origin", "main")
// Page 1 (limit 2, offset 0): newest two, most-recent-first.
page1, found, err := ListCommitsPage(barePath, 2, 0)
if err != nil || !found {
t.Fatalf("page1: found=%v err=%v", found, err)
}
if len(page1) != 2 || page1[0].Subject != "commit 4" || page1[1].Subject != "commit 3" {
t.Fatalf("page1 = %+v, want [commit 4, commit 3]", subjectsOf(page1))
}
// Page 2 (limit 2, offset 2): next two.
page2, found, err := ListCommitsPage(barePath, 2, 2)
if err != nil || !found {
t.Fatalf("page2: found=%v err=%v", found, err)
}
if len(page2) != 2 || page2[0].Subject != "commit 2" || page2[1].Subject != "commit 1" {
t.Fatalf("page2 = %+v, want [commit 2, commit 1]", subjectsOf(page2))
}
// Page 3 (limit 2, offset 4): only the oldest one left.
page3, found, err := ListCommitsPage(barePath, 2, 4)
if err != nil || !found {
t.Fatalf("page3: found=%v err=%v", found, err)
}
if len(page3) != 1 || page3[0].Subject != "commit 0" {
t.Fatalf("page3 = %+v, want [commit 0]", subjectsOf(page3))
}
// Past the end: found=true (repo has commits), just an empty page.
page4, found, err := ListCommitsPage(barePath, 2, 10)
if err != nil || !found {
t.Fatalf("page4: found=%v err=%v", found, err)
}
if len(page4) != 0 {
t.Fatalf("page4 = %+v, want none", subjectsOf(page4))
}
// ListCommits (no offset) must match ListCommitsPage(..., 0).
plain, _, err := ListCommits(barePath, 2)
if err != nil {
t.Fatalf("ListCommits: %v", err)
}
if len(plain) != 2 || plain[0].Subject != page1[0].Subject || plain[1].Subject != page1[1].Subject {
t.Fatalf("ListCommits = %+v, want same as ListCommitsPage(_, 2, 0) = %+v", subjectsOf(plain), subjectsOf(page1))
}
}
func subjectsOf(commits []Commit) []string {
subjects := make([]string, len(commits))
for i, c := range commits {
subjects[i] = c.Subject
}
return subjects
}
func TestDefaultBranchNameViaHEAD(t *testing.T) {
tmp := t.TempDir()
barePath := filepath.Join(tmp, "repo.git")
if err := InitBareRepo(barePath); err != nil {
t.Fatalf("init bare repo: %v", err)
}
work := filepath.Join(tmp, "work")
if err := os.Mkdir(work, 0755); err != nil {
t.Fatal(err)
}
run(t, work, "git", "init", "-q", "-b", "main")
if err := os.WriteFile(filepath.Join(work, "f"), []byte("x"), 0644); err != nil {
t.Fatal(err)
}
run(t, work, "git", "add", "f")
run(t, work, "git", "commit", "-q", "-m", "initial")
run(t, work, "git", "remote", "add", "origin", barePath)
run(t, work, "git", "push", "-q", "origin", "main")
branch, ok := DefaultBranchName(barePath)
if !ok || branch != "main" {
t.Fatalf("DefaultBranchName: got %q ok=%v, want \"main\"", branch, ok)
}
}
func TestDefaultBranchNameEmptyRepo(t *testing.T) {
tmp := t.TempDir()
barePath := filepath.Join(tmp, "repo.git")
if err := InitBareRepo(barePath); err != nil {
t.Fatalf("init bare repo: %v", err)
}
if _, ok := DefaultBranchName(barePath); ok {
t.Fatal("empty repo should not report a default branch")
}
}
// setupMergeTestRepo builds a bare repo with three branches: main, a
// feature branch that merges into it cleanly, and a feature branch that
// conflicts with a change also made on main after the branch point.
func setupMergeTestRepo(t *testing.T) (barePath string) {
t.Helper()
tmp := t.TempDir()
barePath = filepath.Join(tmp, "repo.git")
if err := InitBareRepo(barePath); err != nil {
t.Fatalf("init bare repo: %v", err)
}
work := filepath.Join(tmp, "work")
if err := os.Mkdir(work, 0755); err != nil {
t.Fatal(err)
}
run(t, work, "git", "init", "-q", "-b", "main")
run(t, work, "git", "remote", "add", "origin", barePath)
writeAndCommit := func(name, content, msg string) {
t.Helper()
if err := os.WriteFile(filepath.Join(work, name), []byte(content), 0644); err != nil {
t.Fatal(err)
}
run(t, work, "git", "add", name)
run(t, work, "git", "commit", "-q", "-m", msg)
}
writeAndCommit("shared.txt", "line one\n", "initial")
run(t, work, "git", "push", "-q", "origin", "main")
run(t, work, "git", "checkout", "-q", "-b", "feature-clean")
writeAndCommit("clean.txt", "new file, no overlap\n", "feature-clean change")
run(t, work, "git", "push", "-q", "origin", "feature-clean")
run(t, work, "git", "checkout", "-q", "main")
run(t, work, "git", "checkout", "-q", "-b", "feature-conflict")
writeAndCommit("shared.txt", "feature-conflict's version\n", "feature-conflict change")
run(t, work, "git", "push", "-q", "origin", "feature-conflict")
run(t, work, "git", "checkout", "-q", "main")
writeAndCommit("shared.txt", "main's own diverging version\n", "main diverges")
run(t, work, "git", "push", "-q", "origin", "main")
return barePath
}
func TestListBranches(t *testing.T) {
bare := setupMergeTestRepo(t)
branches, err := ListBranches(bare)
if err != nil {
t.Fatalf("ListBranches: %v", err)
}
want := []string{"feature-clean", "feature-conflict", "main"}
if len(branches) != len(want) {
t.Fatalf("branches = %v, want %v", branches, want)
}
for i, b := range branches {
if b != want[i] {
t.Fatalf("branches = %v, want %v", branches, want)
}
}
}
func TestBranchDiff(t *testing.T) {
bare := setupMergeTestRepo(t)
files, diff, truncated, found, err := BranchDiff(bare, "main", "feature-clean")
if err != nil {
t.Fatalf("BranchDiff: %v", err)
}
if !found {
t.Fatal("expected found=true for two real branches")
}
if truncated {
t.Fatal("small diff should not be truncated")
}
if len(files) != 1 || files[0].Path != "clean.txt" || files[0].Status != "added" {
t.Fatalf("files = %+v, want one added clean.txt", files)
}
if !strings.Contains(diff, "clean.txt") {
t.Fatalf("diff missing clean.txt: %q", diff)
}
_, _, _, found, err = BranchDiff(bare, "main", "does-not-exist")
if err != nil {
t.Fatalf("BranchDiff nonexistent branch: %v", err)
}
if found {
t.Fatal("nonexistent source branch should report found=false")
}
}
func TestCheckMergeable(t *testing.T) {
bare := setupMergeTestRepo(t)
result, err := CheckMergeable(bare, "main", "feature-clean")
if err != nil {
t.Fatalf("CheckMergeable clean: %v", err)
}
if !result.Clean {
t.Fatalf("feature-clean should merge cleanly, got conflicts: %v", result.ConflictFiles)
}
result, err = CheckMergeable(bare, "main", "feature-conflict")
if err != nil {
t.Fatalf("CheckMergeable conflict: %v", err)
}
if result.Clean {
t.Fatal("feature-conflict should not merge cleanly")
}
if len(result.ConflictFiles) != 1 || result.ConflictFiles[0] != "shared.txt" {
t.Fatalf("ConflictFiles = %v, want [shared.txt]", result.ConflictFiles)
}
// main's own ref must be untouched by a mergeability check.
tip, err := revParse(bare, "refs/heads/main")
if err != nil {
t.Fatal(err)
}
branches, err := ListBranches(bare)
if err != nil || len(branches) != 3 {
t.Fatalf("branches after CheckMergeable = %v, err %v — checking mergeability must not create/delete branches", branches, err)
}
if tip == "" {
t.Fatal("main should still resolve")
}
}
func TestMergeBranchesClean(t *testing.T) {
bare := setupMergeTestRepo(t)
oldTip, err := revParse(bare, "refs/heads/main")
if err != nil {
t.Fatal(err)
}
commit, result, err := MergeBranches(bare, "main", "feature-clean", "Merge feature-clean", "Test User", "test@example.com")
if err != nil {
t.Fatalf("MergeBranches: %v", err)
}
if !result.Clean {
t.Fatalf("expected clean merge, got conflicts: %v", result.ConflictFiles)
}
if commit == "" {
t.Fatal("expected a merge commit hash")
}
newTip, err := revParse(bare, "refs/heads/main")
if err != nil {
t.Fatal(err)
}
if newTip != commit {
t.Fatalf("refs/heads/main = %s, want it to point at the merge commit %s", newTip, commit)
}
if newTip == oldTip {
t.Fatal("main did not move")
}
content, found, err := ReadFileAtHEAD(bare, "clean.txt")
if err != nil || !found {
t.Fatalf("clean.txt should exist on main after merge: found=%v err=%v", found, err)
}
if content != "new file, no overlap\n" {
t.Fatalf("clean.txt content = %q", content)
}
}
func TestMergeBranchesConflict(t *testing.T) {
bare := setupMergeTestRepo(t)
oldTip, err := revParse(bare, "refs/heads/main")
if err != nil {
t.Fatal(err)
}
commit, result, err := MergeBranches(bare, "main", "feature-conflict", "Merge feature-conflict", "Test User", "test@example.com")
if err != nil {
t.Fatalf("MergeBranches: %v", err)
}
if result.Clean {
t.Fatal("expected a conflicted merge")
}
if commit != "" {
t.Fatalf("commit = %q, want empty on conflict", commit)
}
if len(result.ConflictFiles) != 1 || result.ConflictFiles[0] != "shared.txt" {
t.Fatalf("ConflictFiles = %v, want [shared.txt]", result.ConflictFiles)
}
newTip, err := revParse(bare, "refs/heads/main")
if err != nil {
t.Fatal(err)
}
if newTip != oldTip {
t.Fatal("main must not move when the merge conflicts")
}
branches, err := ListBranches(bare)
if err != nil || len(branches) != 3 {
t.Fatalf("branches after a conflicted merge attempt = %v, err %v — no stray branch/worktree should be left behind", branches, err)
}
}
// TestArgumentInjectionIsBlocked is a regression test for a real,
// confirmed vulnerability: git's own porcelain commands (checkout -b,
// branch) refuse to create a ref whose name starts with "-" via
// check-ref-format, but that's only enforced client-side — a raw
// `update-ref` (or an equivalent low-level push) against the bare repo
// happily creates one anyway. Every gitexec function that took a
// hash/branch name as a trailing command-line argument was passing it
// straight to `git`, so a ref literally named e.g. "--output=/some/path"
// got interpreted as git's own -o/--output flag instead of a revision —
// confirmed to make `git diff`/`git show` write attacker-chosen content
// to an attacker-chosen path, reachable by an unauthenticated request to
// the commit-detail page's ?hash= parameter (no push access required at
// all) or by any write collaborator via a merge request's branch fields.
// The fix is `--end-of-options` before every such argument; this test
// proves it holds for every affected entry point.
func TestArgumentInjectionIsBlocked(t *testing.T) {
bare := setupMergeTestRepo(t)
mainTip, err := revParse(bare, "refs/heads/main")
if err != nil {
t.Fatal(err)
}
// A ref name that is also a real git flag which writes to a file —
// bypasses git's client-side ref-name validation the same way a raw
// push directly to the bare repo would.
outFile := filepath.Join(t.TempDir(), "should-not-exist.txt")
evilRef := "--output=" + outFile
run(t, bare, "git", "update-ref", "refs/heads/"+evilRef, mainTip)
assertNoFile := func(step string) {
t.Helper()
if _, err := os.Stat(outFile); err == nil {
t.Fatalf("%s: attacker-controlled file was created at %s — argument injection succeeded", step, outFile)
}
}
// ShowCommit/CommitDiff: the hash comes straight from an HTTP query
// parameter with no validation that it even looks like a commit hash.
// evilRef happens to also be a syntactically valid ref name (we just
// created it), so git resolving it as an ordinary revision is correct,
// safe behavior — the only thing that must never happen is the file
// write.
if _, _, err := CommitDiff(bare, evilRef); err != nil {
t.Logf("CommitDiff(evilRef): %v (acceptable)", err)
}
assertNoFile("CommitDiff")
if _, _, err := ShowCommit(bare, evilRef); err != nil {
t.Logf("ShowCommit(evilRef): %v (acceptable)", err)
}
assertNoFile("ShowCommit")
// BranchDiff/CheckMergeable/MergeBranches: the malicious ref is a real
// branch (per ListBranches), so these take the "found" path rather
// than erroring outright — what matters is still that nothing gets
// written to outFile.
branches, err := ListBranches(bare)
if err != nil {
t.Fatal(err)
}
if !containsBranchName(branches, evilRef) {
t.Fatalf("expected %q to be a listed branch (proves check-ref-format was bypassed), got %v", evilRef, branches)
}
if _, _, _, _, err := BranchDiff(bare, "main", evilRef); err != nil {
// An error here is fine (git may reject the malformed revision
// outright) — a file write is not.
t.Logf("BranchDiff with evilRef as source: %v (acceptable)", err)
}
assertNoFile("BranchDiff (evil source)")
if _, _, _, _, err := BranchDiff(bare, evilRef, "feature-clean"); err != nil {
t.Logf("BranchDiff with evilRef as target: %v (acceptable)", err)
}
assertNoFile("BranchDiff (evil target)")
if _, err := CheckMergeable(bare, "main", evilRef); err != nil {
t.Logf("CheckMergeable with evilRef as source: %v (acceptable)", err)
}
assertNoFile("CheckMergeable (evil source)")
if _, err := CheckMergeable(bare, evilRef, "feature-clean"); err != nil {
t.Logf("CheckMergeable with evilRef as target: %v (acceptable)", err)
}
assertNoFile("CheckMergeable (evil target)")
if _, _, err := MergeBranches(bare, "main", evilRef, "msg", "Test", "test@example.com"); err != nil {
t.Logf("MergeBranches with evilRef as source: %v (acceptable)", err)
}
assertNoFile("MergeBranches (evil source)")
}
func containsBranchName(branches []string, name string) bool {
for _, b := range branches {
if b == name {
return true
}
}
return false
}