Artifact 9cb2a1630586f9259405afdfcdfdded472400b76e27860304cba104bd84dadd9:
- File
cli/repo_merge_test.go
— part of check-in
[7ea6fba63f]
at
2026-10-06 15:57:25
on branch trunk
— fix(checkout): merge the way fossil does, and commit merges with their parent (#244) (#252)
The CLI's merge marked a conflict with vfile.chnged=5, which fossil reads as "added by integrate", and recorded no merge-in version or vmerge row, so a merge went unseen and the next commit dropped the merge parent.
Merging now lives in the library as Checkout.Merge and records what fossil records: chnged=2 and mrid for a file taken from the merged-in version, chnged=3 for an added file, a per-file vmerge row for a merged file, and vmerge id 0 for the merge. Conflicts use fossil's markers and are found by looking for them (Checkout.Conflicts), so fossil and go-libfossil read each other's merges. Where fossil relies on its undo copy, Merge keeps the file or refuses instead.
Commit reads vmerge for merge parents, and, as fossil's commit does, refuses unresolved conflict markers, empty commits, unknown names and partial commits of a merge. The CLI's merge, conflicts, mark-resolved and ci commands call the library; ci now commits the checkout. (user: [email protected], size: 5973) [annotate] [blame] [check-ins using]
package cli_test // Tests for the CLI's merge and mark-resolved commands (#244), checked // against what the fossil binary reads from the checkout afterwards. import ( "os" "path/filepath" "strings" "testing" "github.com/danmestas/go-libfossil/cli" ) // branchedCheckout leaves a fossil checkout on trunk with a feat branch // that changed a.txt alone and b.txt in conflict with trunk. feat is // committed last, so it is the repository's newest checkin. func branchedCheckout(t *testing.T) (bin, repoPath, ckDir string) { t.Helper() bin, repoPath, ckDir = fossilCheckout(t) seed := checkoutHash(t, bin, ckDir) writeCkFile(t, ckDir, "b.txt", "two trunk\n") runFossil(t, bin, ckDir, "commit", "-m", "trunk") runFossil(t, bin, ckDir, "update", seed) writeCkFile(t, ckDir, "a.txt", "one feat\n") writeCkFile(t, ckDir, "b.txt", "two feat\n") runFossil(t, bin, ckDir, "commit", "-m", "feat", "--branch", "feat") runFossil(t, bin, ckDir, "update", "trunk") return bin, repoPath, ckDir } // Merge merges into the checked-out version, not the repository's newest // checkin, and records the merge the way fossil reads it. func TestRepoMergeRecordsMergeForFossil(t *testing.T) { bin, repoPath, ckDir := branchedCheckout(t) cmd := &cli.RepoMergeCmd{Version: "feat", Dir: ckDir} if err := cmd.Run(&cli.Globals{Repo: repoPath}); err != nil { t.Fatalf("merge: %v", err) } changes := fossilChanges(t, bin, ckDir) for _, want := range []string{"UPDATED_BY_MERGE a.txt", "CONFLICT", "MERGED_WITH"} { if !strings.Contains(changes, want) { t.Fatalf("fossil changes lacks %q:\n%s", want, changes) } } if got := readCkFile(t, ckDir, "a.txt"); got != "one feat\n" { t.Fatalf("a.txt = %q, want feat's content", got) } } // mark-resolved refuses a file that still holds conflict markers: the // conflict ends when the markers are edited out, and fossil agrees. ci then // commits the checkout with the merge parent. func TestRepoMergeResolveAndCommit(t *testing.T) { bin, repoPath, ckDir := branchedCheckout(t) g := &cli.Globals{Repo: repoPath} if err := (&cli.RepoMergeCmd{Version: "feat", Dir: ckDir}).Run(g); err != nil { t.Fatalf("merge: %v", err) } err := (&cli.RepoMergeResolveCmd{File: "b.txt", Dir: ckDir}).Run(g) if err == nil || !strings.Contains(err.Error(), "still has conflict markers") { t.Fatalf("mark-resolved err = %v, want a refusal", err) } writeCkFile(t, ckDir, "b.txt", "two both\n") if changes := fossilChanges(t, bin, ckDir); strings.Contains(changes, "CONFLICT") { t.Fatalf("fossil still sees a conflict after the edit:\n%s", changes) } ci := &cli.RepoCiCmd{Message: "merge feat", User: "test", Dir: ckDir} if err := ci.Run(g); err != nil { t.Fatalf("ci: %v", err) } if info := runFossil(t, bin, ckDir, "info"); !strings.Contains(info, "merged-from:") { t.Fatalf("commit has no merge parent:\n%s", info) } if changes := fossilChanges(t, bin, ckDir); changes != "" { t.Fatalf("fossil changes after ci:\n%s", changes) } } // checkoutHash returns the hash of the checkin the checkout is on. func checkoutHash(t *testing.T, bin, ckDir string) string { t.Helper() for _, line := range strings.Split(runFossil(t, bin, ckDir, "info"), "\n") { if fields := strings.Fields(line); len(fields) > 1 && fields[0] == "checkout:" { return fields[1] } } t.Fatal("fossil info has no checkout line") return "" } // Merge run from inside the checkout, as the CLI defaults to (-d .), with a // merged-in file under a directory: the checkout path is relative. func TestRepoMergeFromInsideCheckout(t *testing.T) { bin, _, ckDir := fossilCheckout(t) // the repository is found from the checkout seed := checkoutHash(t, bin, ckDir) writeCkFile(t, ckDir, "b.txt", "two trunk\n") runFossil(t, bin, ckDir, "commit", "-m", "trunk") runFossil(t, bin, ckDir, "update", seed) if err := os.MkdirAll(filepath.Join(ckDir, "d"), 0o755); err != nil { t.Fatal(err) } writeCkFile(t, ckDir, "d/x.txt", "x\n") runFossil(t, bin, ckDir, "add", "d/x.txt") runFossil(t, bin, ckDir, "commit", "-m", "feat", "--branch", "feat") runFossil(t, bin, ckDir, "update", "trunk") t.Chdir(ckDir) if err := (&cli.RepoMergeCmd{Version: "feat", Dir: "."}).Run(&cli.Globals{}); err != nil { t.Fatalf("merge: %v", err) } changes := fossilChanges(t, bin, ckDir) if !strings.Contains(changes, "ADDED_BY_MERGE d/x.txt") { t.Fatalf("fossil changes:\n%s", changes) } } // ci follows fossil's commit on what it will commit: a name it does not // track is refused, as is a commit with nothing changed (unless // --allow-empty), and a directory names the files under it. func TestRepoCiSelectsLikeFossil(t *testing.T) { bin, repoPath, ckDir := fossilCheckout(t) g := &cli.Globals{Repo: repoPath} ci := func(files []string, allowEmpty bool) error { return (&cli.RepoCiCmd{ Message: "m", User: "test", Dir: ckDir, Files: files, AllowEmpty: allowEmpty, }).Run(g) } if err := ci(nil, false); err == nil || !strings.Contains(err.Error(), "nothing") { t.Fatalf("ci with no changes: err = %v, want a refusal", err) } if err := ci([]string{filepath.Join(ckDir, "nope.txt")}, false); err == nil || !strings.Contains(err.Error(), "not tracked") { t.Fatalf("ci of an unknown name: err = %v, want a refusal", err) } if err := ci([]string{filepath.Join(ckDir, "a.txt")}, false); err == nil { t.Fatal("ci of an unchanged file succeeded") } if err := ci(nil, true); err != nil { t.Fatalf("ci --allow-empty: %v", err) } if err := os.MkdirAll(filepath.Join(ckDir, "d"), 0o755); err != nil { t.Fatal(err) } writeCkFile(t, ckDir, "d/x.txt", "x\n") runFossil(t, bin, ckDir, "add", "d/x.txt") writeCkFile(t, ckDir, "a.txt", "one edited\n") if err := ci([]string{filepath.Join(ckDir, "d")}, false); err != nil { t.Fatalf("ci of a directory: %v", err) } if got := fossilChanges(t, bin, ckDir); got != "EDITED a.txt" { t.Fatalf("fossil changes after ci of d = %q, want only a.txt edited", got) } }