josie / simplegit

package git

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

func writeFile(t *testing.T, dir, rel, content string) {
	t.Helper()
	full := filepath.Join(dir, 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)
	}
}

// bareWithMain returns a bare repo and a clone whose main has one commit.
func bareWithMain(t *testing.T) (bare, work string) {
	t.Helper()
	bare = filepath.Join(t.TempDir(), "m.git")
	runGit(t, "", "init", "-q", "--bare", "--initial-branch=main", bare)
	work = filepath.Join(t.TempDir(), "work")
	runGit(t, "", "clone", "-q", bare, work)
	runGit(t, work, "config", "user.email", "t@t")
	runGit(t, work, "config", "user.name", "t")
	writeFile(t, work, "base.txt", "base\n")
	runGit(t, work, "add", ".")
	runGit(t, work, "commit", "-qm", "base")
	runGit(t, work, "push", "-q", "origin", "main")
	return bare, work
}

func TestMergeFastForward(t *testing.T) {
	bare, work := bareWithMain(t)
	runGit(t, work, "checkout", "-qb", "feature")
	writeFile(t, work, "feature.txt", "feature\n")
	runGit(t, work, "add", ".")
	runGit(t, work, "commit", "-qm", "feature work")
	runGit(t, work, "push", "-q", "origin", "feature")
	headSHA, err := ResolveCommit(bare, "refs/heads/feature")
	if err != nil {
		t.Fatalf("ResolveCommit: %v", err)
	}

	sha, ff, err := Merge(bare, "main", "feature", "Merge feature", "tester")
	if err != nil {
		t.Fatalf("Merge: %v", err)
	}
	if !ff {
		t.Error("Merge reported not fast-forward, want fast-forward")
	}
	if sha != headSHA {
		t.Errorf("merge sha = %s, want feature head %s", sha, headSHA)
	}
	mainSHA, _ := ResolveCommit(bare, "refs/heads/main")
	if mainSHA != headSHA {
		t.Errorf("main = %s, want advanced to %s", mainSHA, headSHA)
	}
}

func TestMergeCreatesMergeCommit(t *testing.T) {
	bare, work := bareWithMain(t)
	runGit(t, work, "checkout", "-qb", "feature")
	writeFile(t, work, "feature.txt", "feature\n")
	runGit(t, work, "add", ".")
	runGit(t, work, "commit", "-qm", "feature work")
	runGit(t, work, "push", "-q", "origin", "feature")
	runGit(t, work, "checkout", "-q", "main")
	writeFile(t, work, "main.txt", "main\n")
	runGit(t, work, "add", ".")
	runGit(t, work, "commit", "-qm", "main work")
	runGit(t, work, "push", "-q", "origin", "main")

	sha, ff, err := Merge(bare, "main", "feature", "Merge feature", "tester")
	if err != nil {
		t.Fatalf("Merge: %v", err)
	}
	if ff {
		t.Error("Merge reported fast-forward, want a merge commit")
	}
	commit, err := CommitAt(bare, sha)
	if err != nil {
		t.Fatalf("CommitAt merge: %v", err)
	}
	if len(commit.Parents) != 2 {
		t.Errorf("merge parents = %d, want 2", len(commit.Parents))
	}
	entries, err := LsTree(bare, "refs/heads/main", "")
	if err != nil {
		t.Fatalf("LsTree: %v", err)
	}
	var names []string
	for _, e := range entries {
		names = append(names, filepath.Base(e.Path))
	}
	joined := strings.Join(names, ",")
	if !strings.Contains(joined, "feature.txt") || !strings.Contains(joined, "main.txt") {
		t.Errorf("merged tree = %v, want both feature.txt and main.txt", names)
	}
}

func TestMergeConflictLeavesBase(t *testing.T) {
	bare, work := bareWithMain(t)
	writeFile(t, work, "conflict.txt", "original\n")
	runGit(t, work, "add", ".")
	runGit(t, work, "commit", "-qm", "add conflict file")
	runGit(t, work, "push", "-q", "origin", "main")

	runGit(t, work, "checkout", "-qb", "feature")
	writeFile(t, work, "conflict.txt", "feature side\n")
	runGit(t, work, "add", ".")
	runGit(t, work, "commit", "-qm", "feature edit")
	runGit(t, work, "push", "-q", "origin", "feature")

	runGit(t, work, "checkout", "-q", "main")
	writeFile(t, work, "conflict.txt", "main side\n")
	runGit(t, work, "add", ".")
	runGit(t, work, "commit", "-qm", "main edit")
	runGit(t, work, "push", "-q", "origin", "main")
	before, _ := ResolveCommit(bare, "refs/heads/main")

	if _, _, err := Merge(bare, "main", "feature", "Merge feature", "tester"); !errors.Is(err, ErrMergeConflict) {
		t.Fatalf("Merge err = %v, want ErrMergeConflict", err)
	}
	after, _ := ResolveCommit(bare, "refs/heads/main")
	if after != before {
		t.Errorf("main moved on conflict: %s -> %s", before, after)
	}
}

func TestDiffThreeDot(t *testing.T) {
	bare, work := bareWithMain(t)
	runGit(t, work, "checkout", "-qb", "feature")
	writeFile(t, work, "feature.txt", "feature\n")
	runGit(t, work, "add", ".")
	runGit(t, work, "commit", "-qm", "feature work")
	runGit(t, work, "push", "-q", "origin", "feature")

	patch, err := Diff(bare, "main", "feature", 1<<20)
	if err != nil {
		t.Fatalf("Diff: %v", err)
	}
	if !strings.Contains(string(patch), "feature.txt") {
		t.Errorf("diff missing feature.txt: %s", patch)
	}

	branches, err := Branches(bare)
	if err != nil {
		t.Fatalf("Branches: %v", err)
	}
	if len(branches) != 2 || branches[0] != "feature" || branches[1] != "main" {
		t.Errorf("branches = %v, want [feature main]", branches)
	}
}