josie / simplegit

package git

import (
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
)

func runGit(t *testing.T, dir string, args ...string) {
	t.Helper()
	cmd := exec.Command("git", args...)
	cmd.Dir = dir
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Fatalf("git %v in %s: %v: %s", args, dir, err, out)
	}
}

// seedBare gives a bare repo one commit on main with a few files.
func seedBare(t *testing.T, barePath string) {
	t.Helper()
	work := filepath.Join(t.TempDir(), "work")
	runGit(t, "", "clone", "-q", barePath, work)
	files := map[string]string{
		"README.md":    "# demo\n\n**hi** there\n",
		"LICENSE":      "MIT License\n\nPermission is hereby granted, free of charge...\n",
		"hello.c":      "int main(void) { return 0; }\n",
		"sub/note.txt": "note\n",
	}
	for rel, content := range files {
		full := filepath.Join(work, rel)
		if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
			t.Fatalf("mkdir: %v", err)
		}
		if err := os.WriteFile(full, []byte(content), 0o644); err != nil {
			t.Fatalf("write %s: %v", rel, err)
		}
	}
	runGit(t, work, "add", ".")
	runGit(t, work, "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "seed")
	runGit(t, work, "push", "-q", "origin", "main")
}

func TestRefExists(t *testing.T) {
	path := filepath.Join(t.TempDir(), "demo.git")
	if err := InitBare(path, "main"); err != nil {
		t.Fatalf("InitBare: %v", err)
	}
	exists, err := RefExists(path, "main")
	if err != nil {
		t.Fatalf("RefExists: %v", err)
	}
	if exists {
		t.Error("fresh bare repo resolves main, want empty")
	}
	seedBare(t, path)
	if exists, err := RefExists(path, "main"); err != nil || !exists {
		t.Fatalf("RefExists after seed = (%v, %v), want (true, nil)", exists, err)
	}
	if exists, err := RefExists(path, "nosuchbranch"); err != nil || exists {
		t.Errorf("RefExists nosuchbranch = (%v, %v), want (false, nil)", exists, err)
	}
}

func TestLsTree(t *testing.T) {
	path := filepath.Join(t.TempDir(), "demo.git")
	if err := InitBare(path, "main"); err != nil {
		t.Fatalf("InitBare: %v", err)
	}
	seedBare(t, path)

	entries, err := LsTree(path, "main", "")
	if err != nil {
		t.Fatalf("LsTree root: %v", err)
	}
	types := map[string]string{}
	for _, e := range entries {
		types[e.Path] = e.Type
	}
	if len(entries) != 4 {
		t.Errorf("root entries = %d, want 4: %+v", len(entries), entries)
	}
	if types["README.md"] != "blob" || types["LICENSE"] != "blob" || types["hello.c"] != "blob" {
		t.Errorf("root blobs wrong: %+v", types)
	}
	if types["sub"] != "tree" {
		t.Errorf("sub type = %q, want tree", types["sub"])
	}

	sub, err := LsTree(path, "main", "sub")
	if err != nil {
		t.Fatalf("LsTree sub: %v", err)
	}
	if len(sub) != 1 || sub[0].Path != "sub/note.txt" || sub[0].Type != "blob" {
		t.Errorf("sub entries = %+v, want one blob at sub/note.txt", sub)
	}
}

func TestShowFile(t *testing.T) {
	path := filepath.Join(t.TempDir(), "demo.git")
	if err := InitBare(path, "main"); err != nil {
		t.Fatalf("InitBare: %v", err)
	}
	seedBare(t, path)

	content, err := ShowFile(path, "main", "README.md", 1<<20)
	if err != nil {
		t.Fatalf("ShowFile: %v", err)
	}
	if !strings.Contains(string(content), "**hi** there") {
		t.Errorf("content = %q", content)
	}
	if _, err := ShowFile(path, "main", "nope.txt", 1<<20); err == nil {
		t.Error("ShowFile missing file = nil error, want error")
	} else if !strings.Contains(err.Error(), "not found") && !strings.Contains(err.Error(), "does not exist") {
		t.Logf("missing-file error text: %v", err)
	}
}

// An oversized object must never be buffered in full: ShowFile stops at
// limit+1 bytes and kills the subprocess.
func TestShowFileBounded(t *testing.T) {
	path := filepath.Join(t.TempDir(), "demo.git")
	if err := InitBare(path, "main"); err != nil {
		t.Fatalf("InitBare: %v", err)
	}
	seedBare(t, path)

	content, err := ShowFile(path, "main", "README.md", 4)
	if err != nil {
		t.Fatalf("ShowFile bounded: %v", err)
	}
	if len(content) != 5 {
		t.Errorf("bounded content = %d bytes (%q), want limit+1 = 5", len(content), content)
	}
}

// A hostile parent environment (GIT_DIR et al.) must not redirect browse
// commands to a different repository — gitCommand pins a minimal env.
func TestBrowseIgnoresInheritedGitEnv(t *testing.T) {
	path := filepath.Join(t.TempDir(), "demo.git")
	if err := InitBare(path, "main"); err != nil {
		t.Fatalf("InitBare: %v", err)
	}
	seedBare(t, path)

	decoy := filepath.Join(t.TempDir(), "decoy.git")
	if err := InitBare(decoy, "main"); err != nil {
		t.Fatalf("InitBare decoy: %v", err)
	}
	work := filepath.Join(t.TempDir(), "decoy-work")
	runGit(t, "", "init", "-q", "-b", "main", work)
	if err := os.WriteFile(filepath.Join(work, "README.md"), []byte("DECOY\n"), 0o644); err != nil {
		t.Fatalf("write decoy readme: %v", err)
	}
	runGit(t, work, "add", ".")
	runGit(t, work, "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "decoy")
	runGit(t, work, "push", "-q", decoy, "main")

	t.Setenv("GIT_DIR", decoy)
	t.Setenv("GIT_WORK_TREE", work)

	content, err := ShowFile(path, "main", "README.md", 1<<20)
	if err != nil {
		t.Fatalf("ShowFile with hostile GIT_DIR: %v", err)
	}
	if strings.Contains(string(content), "DECOY") {
		t.Errorf("ShowFile served the decoy repo: %q", content)
	}
	if !strings.Contains(string(content), "**hi** there") {
		t.Errorf("ShowFile content = %q, want the seeded README", content)
	}
	entries, err := LsTree(path, "main", "")
	if err != nil {
		t.Fatalf("LsTree with hostile GIT_DIR: %v", err)
	}
	if len(entries) != 4 {
		t.Errorf("LsTree returned %d entries, want 4 from demo (decoy has 1)", len(entries))
	}
}