josie / simplegit

package main

import (
	"database/sql"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	"git.josie-c.com/josie/simplegit/internal/db"
	"git.josie-c.com/josie/simplegit/internal/git"
)

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: %v: %s", args, err, out)
	}
}

func hashObject(t *testing.T, repoPath, content string) string {
	t.Helper()
	cmd := exec.Command("git", "hash-object", "-w", "--stdin")
	cmd.Dir = repoPath
	cmd.Stdin = strings.NewReader(content)
	out, err := cmd.Output()
	if err != nil {
		t.Fatalf("hash-object: %v", err)
	}
	return strings.TrimSpace(string(out))
}

// commitToBranch writes a one-file commit and points branch at it.
func commitToBranch(t *testing.T, repoPath, branch string) string {
	t.Helper()
	blob := hashObject(t, repoPath, "hello\n")
	treeCmd := exec.Command("git", "mktree")
	treeCmd.Dir = repoPath
	treeCmd.Stdin = strings.NewReader("100644 blob " + blob + "\tfile\n")
	treeOut, err := treeCmd.Output()
	if err != nil {
		t.Fatalf("mktree: %v", err)
	}
	commitCmd := exec.Command("git", "commit-tree", strings.TrimSpace(string(treeOut)), "-m", "seed")
	commitCmd.Dir = repoPath
	commitCmd.Env = append(os.Environ(),
		"GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t",
		"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t")
	commitOut, err := commitCmd.Output()
	if err != nil {
		t.Fatalf("commit-tree: %v", err)
	}
	sha := strings.TrimSpace(string(commitOut))
	runGit(t, repoPath, "update-ref", "refs/heads/"+branch, sha)
	return sha
}

func newHookFixture(t *testing.T) (string, *sql.DB) {
	t.Helper()
	dataDir := t.TempDir()
	repoPath := filepath.Join(dataDir, "repos", "josie", "demo.git")
	if err := os.MkdirAll(filepath.Dir(repoPath), 0o755); err != nil {
		t.Fatalf("mkdir: %v", err)
	}
	if err := git.InitBare(repoPath, "main"); err != nil {
		t.Fatalf("InitBare: %v", err)
	}
	database, err := db.Open(filepath.Join(dataDir, "simplegit.db"))
	if err != nil {
		t.Fatalf("db.Open: %v", err)
	}
	t.Cleanup(func() { database.Close() })

	ownerID, err := db.CreateUser(database, "josie", "hash")
	if err != nil {
		t.Fatalf("CreateUser: %v", err)
	}
	if _, err := db.CreateRepo(database, ownerID, "demo", "", "private"); err != nil {
		t.Fatalf("CreateRepo: %v", err)
	}
	return repoPath, database
}

func runHookWithStdin(t *testing.T, repoPath, stdin string) error {
	t.Helper()
	r, w, err := os.Pipe()
	if err != nil {
		t.Fatalf("pipe: %v", err)
	}
	old := os.Stdin
	os.Stdin = r
	defer func() {
		os.Stdin = old
		r.Close()
	}()
	go func() {
		_, _ = io.WriteString(w, stdin)
		w.Close()
	}()
	return runHook([]string{"--repo", repoPath})
}

func TestRunHookEstablishesDefaultBranch(t *testing.T) {
	repoPath, database := newHookFixture(t)
	sha := commitToBranch(t, repoPath, "master")

	stdin := "0000000000000000000000000000000000000000 " + sha + " refs/heads/master\n"
	if err := runHookWithStdin(t, repoPath, stdin); err != nil {
		t.Fatalf("runHook: %v", err)
	}

	repo, err := db.GetRepoByName(database, "josie", "demo")
	if err != nil {
		t.Fatalf("GetRepoByName: %v", err)
	}
	if repo.DefaultBranch != "master" {
		t.Errorf("DefaultBranch = %q, want master", repo.DefaultBranch)
	}
	if !repo.PushedAt.Valid {
		t.Error("PushedAt is NULL, want a timestamp")
	}
	if branch, err := git.DefaultBranch(repoPath); err != nil || branch != "master" {
		t.Errorf("HEAD branch = %q, %v; want master", branch, err)
	}
}

func TestRunHookKeepsExistingDefaultBranch(t *testing.T) {
	repoPath, database := newHookFixture(t)
	sha := commitToBranch(t, repoPath, "main")

	stdin := "0000000000000000000000000000000000000000 " + sha + " refs/heads/main\n"
	if err := runHookWithStdin(t, repoPath, stdin); err != nil {
		t.Fatalf("runHook: %v", err)
	}
	repo, err := db.GetRepoByName(database, "josie", "demo")
	if err != nil {
		t.Fatalf("GetRepoByName: %v", err)
	}
	if repo.DefaultBranch != "main" {
		t.Errorf("DefaultBranch = %q, want unchanged main", repo.DefaultBranch)
	}
}

func TestSplitRepoPath(t *testing.T) {
	owner, name, dataDir, err := splitRepoPath("/srv/simplegit/data/repos/josie/demo.git")
	if err != nil {
		t.Fatalf("splitRepoPath: %v", err)
	}
	if owner != "josie" || name != "demo" || dataDir != "/srv/simplegit/data" {
		t.Errorf("split = %q/%q in %q", owner, name, dataDir)
	}
	if _, _, _, err := splitRepoPath("/tmp/not-a-repo"); err == nil {
		t.Error("splitRepoPath accepted a non-.git path")
	}
}