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) } }