package gitcmd import ( "path/filepath" "testing" ) func buildFFRepo(t *testing.T) (repo *Repository, c2 string) { t.Helper() dir := t.TempDir() work := filepath.Join(dir, "work") bare := filepath.Join(dir, "bare.git") gitRun(t, "", nil, "init", "-q", "-b", "main", work) writeFile(t, filepath.Join(work, "a.txt"), "a\n") commitAs(t, work, "A", "a@x", "2024-03-01T00:00:00+00:00", "c1") gitRun(t, work, nil, "branch", "feature") gitRun(t, work, nil, "checkout", "-q", "feature") writeFile(t, filepath.Join(work, "b.txt"), "b\n") commitAs(t, work, "A", "a@x", "2024-03-02T00:00:00+00:00", "c2") c2 = headSHA(t, work) return cloneBareFromWork(t, work, bare), c2 } func buildConflictRepo(t *testing.T) *Repository { t.Helper() dir := t.TempDir() work := filepath.Join(dir, "work") bare := filepath.Join(dir, "bare.git") gitRun(t, "", nil, "init", "-q", "-b", "main", work) writeFile(t, filepath.Join(work, "f.txt"), "base\n") commitAs(t, work, "A", "a@x", "2024-04-01T00:00:00+00:00", "c1") gitRun(t, work, nil, "branch", "feature") writeFile(t, filepath.Join(work, "f.txt"), "main\n") commitAs(t, work, "A", "a@x", "2024-04-02T00:00:00+00:00", "c2main") gitRun(t, work, nil, "checkout", "-q", "feature") writeFile(t, filepath.Join(work, "f.txt"), "feature\n") commitAs(t, work, "A", "a@x", "2024-04-03T00:00:00+00:00", "c3feat") gitRun(t, work, nil, "checkout", "-q", "main") return cloneBareFromWork(t, work, bare) } func TestMerge_Clean(t *testing.T) { f := newMergeFixture(t) res, err := f.repo.Merge("main", "feature", "merge feature", wAuthor, wAuthorEmail, false) if err != nil { t.Fatalf("Merge: %v", err) } if len(res.Conflicts) != 0 { t.Errorf("conflicts = %v, want none", res.Conflicts) } if res.MergeCommit == nil { t.Fatal("nil merge commit") } if res.FastForward { t.Error("want real merge, got FF") } if len(res.MergeCommit.Parents) != 2 { t.Errorf("parents = %v, want 2", res.MergeCommit.Parents) } tree, _ := f.repo.GetTree(res.MergeCommit.ID, "") names := treeNames(tree) for _, want := range []string{"a.txt", "b.txt", "c.txt"} { if !containsStr(names, want) { t.Errorf("merged tree missing %s; got %v", want, names) } } } func TestMerge_FastForward(t *testing.T) { repo, c2 := buildFFRepo(t) res, err := repo.Merge("main", "feature", "ff", wAuthor, wAuthorEmail, false) if err != nil { t.Fatalf("Merge: %v", err) } if !res.FastForward { t.Error("want FF") } if res.MergeCommit == nil || res.MergeCommit.ID != c2 { t.Errorf("merge commit = %v, want %s", res.MergeCommit, c2) } tip, _ := repo.refSHA("refs/heads/main") if tip != c2 { t.Errorf("main tip = %s, want %s", tip, c2) } } func TestMerge_Conflict(t *testing.T) { repo := buildConflictRepo(t) mainBefore, _ := repo.refSHA("refs/heads/main") res, err := repo.Merge("main", "feature", "merge", wAuthor, wAuthorEmail, false) if err != nil { t.Fatalf("Merge: %v", err) } if res.MergeCommit != nil { t.Error("want nil merge commit on conflict") } if len(res.Conflicts) == 0 { t.Error("want conflicts") } else if !containsStr(res.Conflicts, "f.txt") { t.Errorf("conflicts = %v, want f.txt", res.Conflicts) } mainAfter, _ := repo.refSHA("refs/heads/main") if mainAfter != mainBefore { t.Errorf("main moved on conflict: %s -> %s", mainBefore, mainAfter) } } func TestMergeViaTempRepo_Clean(t *testing.T) { f := newMergeFixture(t) res, err := f.repo.MergeViaTempRepo("main", "feature", "merge feature", wAuthor, wAuthorEmail, false) if err != nil { t.Fatalf("MergeViaTempRepo: %v", err) } if len(res.Conflicts) != 0 { t.Errorf("conflicts = %v, want none", res.Conflicts) } if res.MergeCommit == nil { t.Fatal("nil merge commit") } if res.FastForward { t.Error("want real merge, got FF") } if len(res.MergeCommit.Parents) != 2 { t.Errorf("parents = %v, want 2", res.MergeCommit.Parents) } tree, _ := f.repo.GetTree(res.MergeCommit.ID, "") names := treeNames(tree) for _, want := range []string{"a.txt", "b.txt", "c.txt"} { if !containsStr(names, want) { t.Errorf("merged tree missing %s; got %v", want, names) } } } func TestMergeViaTempRepo_FastForward(t *testing.T) { repo, c2 := buildFFRepo(t) res, err := repo.MergeViaTempRepo("main", "feature", "ff", wAuthor, wAuthorEmail, false) if err != nil { t.Fatalf("MergeViaTempRepo: %v", err) } if !res.FastForward { t.Error("want FF") } if res.MergeCommit == nil || res.MergeCommit.ID != c2 { t.Errorf("merge commit = %v, want %s", res.MergeCommit, c2) } tip, _ := repo.refSHA("refs/heads/main") if tip != c2 { t.Errorf("main tip = %s, want %s", tip, c2) } } func TestMergeViaTempRepo_Conflict(t *testing.T) { repo := buildConflictRepo(t) mainBefore, _ := repo.refSHA("refs/heads/main") res, err := repo.MergeViaTempRepo("main", "feature", "merge", wAuthor, wAuthorEmail, false) if err != nil { t.Fatalf("MergeViaTempRepo: %v", err) } if res.MergeCommit != nil { t.Error("want nil merge commit on conflict") } if len(res.Conflicts) == 0 { t.Error("want conflicts") } else if !containsStr(res.Conflicts, "f.txt") { t.Errorf("conflicts = %v, want f.txt", res.Conflicts) } mainAfter, _ := repo.refSHA("refs/heads/main") if mainAfter != mainBefore { t.Errorf("main moved on conflict: %s -> %s", mainBefore, mainAfter) } }