Artifact 4d978c36c5c2d577ecb142b61e3650a775cfddbbb4055d356ab777e137f843cd:
- File
repo_checkout_test.go
— part of check-in
[862dcb20c3]
at
2026-05-12 23:48:08
on branch trunk
— fix: Repo.Commit preserves parent's tracked files (#30) (#32)
Repo.Commit was a thin pass-through to manifest.Checkin: callers who supplied a subset of the parent's tracked files got a partial manifest, so files tracked at the parent silently disappeared from the new commit. A checkout at the child's rev saw only the supplied subset — bones, EdgeSync, and sesh all hit this as silent data loss across slot commits.
Default behavior now matches `fossil ci`: when ParentID > 0 the parent's F-cards are merged into the commit's file set (caller wins on name collision; untouched parent files are loaded by UUID and carried forward).
For the rare case where a caller intentionally wants omission to mean deletion, CommitOpts.PartialManifest opts out of the merge. The internal diff/deletion tests that previously relied on the partial behavior now set this flag explicitly. A first-class deletion API may replace the escape hatch later.
Adds TestCommitPreservesParentFiles as the regression: parent [a,b,c], child supplies only [b'], child's ListFiles must return [a,b',c] with a and c keeping the parent's blob UUIDs. (user: [email protected], size: 4711) [annotate] [blame] [check-ins using]
package libfossil import ( "path/filepath" "testing" _ "github.com/danmestas/libfossil/internal/testdriver" ) func TestCommitAndTimeline(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.fossil") r, err := Create(path, CreateOpts{User: "test"}) if err != nil { t.Fatal(err) } defer r.Close() rid1, uuid, err := r.Commit(CommitOpts{ Files: []FileToCommit{ {Name: "hello.txt", Content: []byte("hello world\n")}, }, Comment: "initial commit", User: "test", }) if err != nil { t.Fatal(err) } if uuid == "" { t.Fatal("Commit returned empty UUID") } if len(uuid) != 40 && len(uuid) != 64 { t.Errorf("UUID length = %d, want 40 or 64", len(uuid)) } if rid1 <= 0 { t.Fatalf("Commit returned rid=%d, want > 0", rid1) } // Second commit to test timeline walking. rid2, uuid2, err := r.Commit(CommitOpts{ ParentID: rid1, Files: []FileToCommit{ {Name: "hello.txt", Content: []byte("hello world v2\n")}, }, Comment: "second commit", User: "test", }) if err != nil { t.Fatal(err) } entries, err := r.Timeline(LogOpts{Start: rid2, Limit: 10}) if err != nil { t.Fatal(err) } if len(entries) < 2 { t.Fatalf("Timeline returned %d entries, want >= 2", len(entries)) } if entries[0].UUID != uuid2 { t.Errorf("first entry UUID = %q, want %q", entries[0].UUID, uuid2) } if entries[0].Comment != "second commit" { t.Errorf("first entry Comment = %q, want %q", entries[0].Comment, "second commit") } if entries[1].UUID != uuid { t.Errorf("second entry UUID = %q, want %q", entries[1].UUID, uuid) } } // TestCommitPreservesParentFiles is the regression for #30: a child // commit that supplies only a subset of the parent's tracked files must // still produce a full-tree manifest. Before the fix, the child's // manifest contained only the supplied subset and a checkout at the // child's rev silently lost every file the caller did not re-supply. func TestCommitPreservesParentFiles(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.fossil") r, err := Create(path, CreateOpts{User: "test"}) if err != nil { t.Fatal(err) } defer r.Close() parentRID, _, err := r.Commit(CommitOpts{ Files: []FileToCommit{ {Name: "a.txt", Content: []byte("alpha\n")}, {Name: "b.txt", Content: []byte("bravo v1\n")}, {Name: "c.txt", Content: []byte("charlie\n")}, }, Comment: "parent: a, b, c", User: "test", }) if err != nil { t.Fatalf("parent commit: %v", err) } childRID, _, err := r.Commit(CommitOpts{ ParentID: parentRID, Files: []FileToCommit{ {Name: "b.txt", Content: []byte("bravo v2\n")}, }, Comment: "child: only b modified", User: "test", }) if err != nil { t.Fatalf("child commit: %v", err) } files, err := r.ListFiles(childRID) if err != nil { t.Fatalf("ListFiles(child): %v", err) } got := map[string]string{} for _, f := range files { got[f.Name] = f.UUID } if len(got) != 3 { t.Fatalf("child manifest has %d files, want 3 (a.txt, b.txt, c.txt); got %v", len(got), got) } for _, name := range []string{"a.txt", "b.txt", "c.txt"} { if _, ok := got[name]; !ok { t.Errorf("child manifest missing %q (file tracked at parent was dropped)", name) } } // a.txt and c.txt should keep the parent's blob UUIDs (untouched). parentFiles, err := r.ListFiles(parentRID) if err != nil { t.Fatalf("ListFiles(parent): %v", err) } parentUUIDs := map[string]string{} for _, f := range parentFiles { parentUUIDs[f.Name] = f.UUID } if got["a.txt"] != parentUUIDs["a.txt"] { t.Errorf("a.txt UUID changed across commits: parent=%s child=%s", parentUUIDs["a.txt"], got["a.txt"]) } if got["c.txt"] != parentUUIDs["c.txt"] { t.Errorf("c.txt UUID changed across commits: parent=%s child=%s", parentUUIDs["c.txt"], got["c.txt"]) } // b.txt should have a NEW UUID — caller supplied new content. if got["b.txt"] == parentUUIDs["b.txt"] { t.Errorf("b.txt UUID unchanged despite new content: %s", got["b.txt"]) } } func TestListFiles(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.fossil") r, err := Create(path, CreateOpts{User: "test"}) if err != nil { t.Fatal(err) } defer r.Close() rid, _, err := r.Commit(CommitOpts{ Files: []FileToCommit{ {Name: "a.txt", Content: []byte("aaa\n")}, {Name: "b.txt", Content: []byte("bbb\n")}, }, Comment: "two files", User: "test", }) if err != nil { t.Fatal(err) } files, err := r.ListFiles(rid) if err != nil { t.Fatal(err) } if len(files) != 2 { t.Fatalf("ListFiles returned %d files, want 2", len(files)) } if files[0].Name != "a.txt" || files[1].Name != "b.txt" { t.Errorf("files = %v, want a.txt and b.txt", files) } }