Artifact 5f5c181508de69766c37c137be0084daf792072ef4a9a1425db7d7b2e5a0dca4:
- File
repo_history_test.go
— part of check-in
[6273a0750d]
at
2026-07-22 23:03:24
on branch trunk
— fix: correct go.mod module path to go-libfossil (#76) (#117)
* fix: correct go.mod module path to go-libfossil (#76)
The module directive still declared github.com/danmestas/libfossil after the repository was renamed to go-libfossil. pkg.go.dev/godoc scraping matches the declared module path against its hosting path, so the stale path broke SDK doc generation.
Rename the module path across all four in-repo modules (root, the two db/driver submodules, observer/otel, and docs/site) and every internal import, plus the release workflow, the doc-generation script, and the docs.
No tag has been published under the go-libfossil path yet, so the cross-module requires cannot resolve from the proxy. Add replace directives pointing the in-repo requires at their local directories so GOWORK=off builds and the CI matrix resolve without a published version. These replaces are ignored by downstream consumers and can be dropped once new-path tags are cut.
* Guard releases against replace directives; regenerate SDK docs
The go-libfossil rename bridges the in-repo modules with `replace` directives. Go refuses a main module that carries them, so a tag cut while they are present breaks `go install .../cmd/libfossil@<tag>` and every `@version` fetch of the driver submodules. Add a Release workflow step that fails the job before anything is published if any tracked go.mod still contains a replace directive, so the bridge is a CI-enforced TODO rather than a silent landmine. The step comment records what unblocks a release: re-tag the submodules with the corrected module paths, bump the root requires, then drop the replace blocks.
Regenerate the SDK reference docs. The previous pass only substituted URLs over the pre-existing generated files, leaving stale signatures (Repo.Timeline was still documented as taking LogOpts rather than TimelineOpts). Note that gomarkdoc must be run outside a git worktree — inside one it silently strips every source link.
Also reword the README rename note, which read as though the import path had not changed, and re-sort the ncruces require block. (user: [email protected], size: 10866) [annotate] [blame] [check-ins using]
package libfossil import ( "errors" "path/filepath" "strings" "testing" _ "github.com/danmestas/go-libfossil/internal/testdriver" ) func newTestRepo(t *testing.T) *Repo { t.Helper() dir := t.TempDir() r, err := Create(filepath.Join(dir, "test.fossil"), CreateOpts{User: "test"}) if err != nil { t.Fatalf("Create: %v", err) } t.Cleanup(func() { _ = r.Close() }) return r } func commit(t *testing.T, r *Repo, parent int64, comment string, files []FileToCommit) int64 { t.Helper() rid, _, err := r.Commit(CommitOpts{ ParentID: parent, Files: files, Comment: comment, User: "test", }) if err != nil { t.Fatalf("Commit %q: %v", comment, err) } return rid } func TestDiff_Modification(t *testing.T) { r := newTestRepo(t) a := commit(t, r, 0, "v1", []FileToCommit{ {Name: "hello.txt", Content: []byte("hello\nworld\n")}, }) b := commit(t, r, a, "v2", []FileToCommit{ {Name: "hello.txt", Content: []byte("hello\nbrave new world\n")}, }) entries, err := r.Diff(a, b, "hello.txt") if err != nil { t.Fatalf("Diff: %v", err) } if len(entries) != 1 { t.Fatalf("got %d entries, want 1", len(entries)) } e := entries[0] if e.Name != "hello.txt" { t.Errorf("Name = %q, want %q", e.Name, "hello.txt") } if !strings.Contains(e.Unified, "a/hello.txt") || !strings.Contains(e.Unified, "b/hello.txt") { t.Errorf("missing headers in unified output:\n%s", e.Unified) } if !strings.Contains(e.Unified, "-world") || !strings.Contains(e.Unified, "+brave new world") { t.Errorf("missing hunk lines in unified output:\n%s", e.Unified) } } func TestDiff_Identical(t *testing.T) { r := newTestRepo(t) a := commit(t, r, 0, "v1", []FileToCommit{ {Name: "hello.txt", Content: []byte("hello\n")}, {Name: "other.txt", Content: []byte("first\n")}, }) b := commit(t, r, a, "touch other", []FileToCommit{ {Name: "hello.txt", Content: []byte("hello\n")}, {Name: "other.txt", Content: []byte("second\n")}, }) entries, err := r.Diff(a, b, "hello.txt") if err != nil { t.Fatalf("Diff: %v", err) } if len(entries) != 0 { t.Errorf("want empty slice for identical content, got %d entries:\n%+v", len(entries), entries) } } func TestDiff_Addition(t *testing.T) { r := newTestRepo(t) a := commit(t, r, 0, "v1", []FileToCommit{ {Name: "keep.txt", Content: []byte("anchor\n")}, }) b := commit(t, r, a, "add new", []FileToCommit{ {Name: "new.txt", Content: []byte("line one\nline two\n")}, }) entries, err := r.Diff(a, b, "new.txt") if err != nil { t.Fatalf("Diff: %v", err) } if len(entries) != 1 { t.Fatalf("got %d entries, want 1", len(entries)) } out := entries[0].Unified if !strings.Contains(out, "+line one") || !strings.Contains(out, "+line two") { t.Errorf("expected insertion lines, got:\n%s", out) } if strings.Contains(out, "-line one") { t.Errorf("unexpected deletion marker for pure addition:\n%s", out) } } func TestDiff_Deletion(t *testing.T) { r := newTestRepo(t) a := commit(t, r, 0, "v1", []FileToCommit{ {Name: "doomed.txt", Content: []byte("goodbye\ncruel world\n")}, {Name: "keep.txt", Content: []byte("anchor\n")}, }) // PartialManifest: true models deletion by omission — only callers // intentionally erasing tracked files should opt in. b, _, err := r.Commit(CommitOpts{ ParentID: a, Files: []FileToCommit{{Name: "keep.txt", Content: []byte("anchor\n")}}, Comment: "drop doomed", User: "test", PartialManifest: true, }) if err != nil { t.Fatalf("Commit: %v", err) } entries, err := r.Diff(a, b, "doomed.txt") if err != nil { t.Fatalf("Diff: %v", err) } if len(entries) != 1 { t.Fatalf("got %d entries, want 1", len(entries)) } out := entries[0].Unified if !strings.Contains(out, "-goodbye") || !strings.Contains(out, "-cruel world") { t.Errorf("expected deletion lines, got:\n%s", out) } if strings.Contains(out, "+goodbye") { t.Errorf("unexpected insertion marker for pure deletion:\n%s", out) } } func TestDiff_AbsentBothSides(t *testing.T) { r := newTestRepo(t) a := commit(t, r, 0, "v1", []FileToCommit{ {Name: "only.txt", Content: []byte("x\n")}, }) b := commit(t, r, a, "v2", []FileToCommit{ {Name: "only.txt", Content: []byte("y\n")}, }) entries, err := r.Diff(a, b, "never-existed.txt") if err != nil { t.Fatalf("Diff: %v", err) } if len(entries) != 0 { t.Errorf("want empty slice for file absent in both checkins, got %d entries:\n%+v", len(entries), entries) } } func TestReadFile_Present(t *testing.T) { r := newTestRepo(t) rid := commit(t, r, 0, "v1", []FileToCommit{ {Name: "hello.txt", Content: []byte("hello\nworld\n")}, {Name: "other.txt", Content: []byte("other\n")}, }) data, err := r.ReadFile(rid, "hello.txt") if err != nil { t.Fatalf("ReadFile: %v", err) } if string(data) != "hello\nworld\n" { t.Errorf("content = %q, want %q", data, "hello\nworld\n") } } func TestReadFile_AcrossRevs(t *testing.T) { r := newTestRepo(t) a := commit(t, r, 0, "v1", []FileToCommit{ {Name: "f.txt", Content: []byte("first\n")}, }) b := commit(t, r, a, "v2", []FileToCommit{ {Name: "f.txt", Content: []byte("second\n")}, }) got, err := r.ReadFile(a, "f.txt") if err != nil { t.Fatalf("ReadFile(a): %v", err) } if string(got) != "first\n" { t.Errorf("rev a = %q, want %q", got, "first\n") } got, err = r.ReadFile(b, "f.txt") if err != nil { t.Fatalf("ReadFile(b): %v", err) } if string(got) != "second\n" { t.Errorf("rev b = %q, want %q", got, "second\n") } } func TestReadFile_NotInCheckin(t *testing.T) { r := newTestRepo(t) rid := commit(t, r, 0, "v1", []FileToCommit{ {Name: "only.txt", Content: []byte("x\n")}, }) _, err := r.ReadFile(rid, "missing.txt") if err == nil { t.Fatal("expected error for missing file, got nil") } if !errors.Is(err, ErrFileNotFound) { t.Errorf("err = %v, want errors.Is ErrFileNotFound", err) } } func TestReadFile_EmptyFilePath(t *testing.T) { r := newTestRepo(t) rid := commit(t, r, 0, "v1", []FileToCommit{ {Name: "x.txt", Content: []byte("x\n")}, }) _, err := r.ReadFile(rid, "") if err == nil { t.Fatal("expected error for empty filePath, got nil") } if !strings.Contains(err.Error(), "filePath is required") { t.Errorf("err = %q, want message mentioning filePath", err.Error()) } } func TestDiff_WholeCheckin_Identical(t *testing.T) { r := newTestRepo(t) a := commit(t, r, 0, "v1", []FileToCommit{ {Name: "hello.txt", Content: []byte("hello\n")}, {Name: "other.txt", Content: []byte("other\n")}, }) entries, err := r.Diff(a, a, "") if err != nil { t.Fatalf("Diff: %v", err) } if entries == nil { t.Fatal("entries = nil, want empty non-nil slice for identical checkins") } if len(entries) != 0 { t.Errorf("got %d entries, want 0:\n%+v", len(entries), entries) } } func TestDiff_WholeCheckin_SingleFileChange(t *testing.T) { r := newTestRepo(t) a := commit(t, r, 0, "v1", []FileToCommit{ {Name: "hello.txt", Content: []byte("hello\nworld\n")}, {Name: "stable.txt", Content: []byte("unchanged\n")}, }) b := commit(t, r, a, "v2", []FileToCommit{ {Name: "hello.txt", Content: []byte("hello\nbrave new world\n")}, {Name: "stable.txt", Content: []byte("unchanged\n")}, }) entries, err := r.Diff(a, b, "") if err != nil { t.Fatalf("Diff: %v", err) } if len(entries) != 1 { t.Fatalf("got %d entries, want 1:\n%+v", len(entries), entries) } if entries[0].Name != "hello.txt" { t.Errorf("Name = %q, want %q", entries[0].Name, "hello.txt") } if !strings.Contains(entries[0].Unified, "+brave new world") { t.Errorf("missing expected hunk in unified diff:\n%s", entries[0].Unified) } } func TestDiff_WholeCheckin_MultiFileChange(t *testing.T) { r := newTestRepo(t) a := commit(t, r, 0, "v1", []FileToCommit{ {Name: "a.txt", Content: []byte("a-old\n")}, {Name: "b.txt", Content: []byte("b-old\n")}, {Name: "c.txt", Content: []byte("c-stable\n")}, }) b := commit(t, r, a, "v2", []FileToCommit{ {Name: "a.txt", Content: []byte("a-new\n")}, {Name: "b.txt", Content: []byte("b-new\n")}, {Name: "c.txt", Content: []byte("c-stable\n")}, }) entries, err := r.Diff(a, b, "") if err != nil { t.Fatalf("Diff: %v", err) } if len(entries) != 2 { t.Fatalf("got %d entries, want 2:\n%+v", len(entries), entries) } // Deterministic order: sorted by Name. if entries[0].Name != "a.txt" || entries[1].Name != "b.txt" { t.Errorf("entry order = [%q, %q], want [a.txt, b.txt]", entries[0].Name, entries[1].Name) } if !strings.Contains(entries[0].Unified, "-a-old") || !strings.Contains(entries[0].Unified, "+a-new") { t.Errorf("a.txt hunk missing expected lines:\n%s", entries[0].Unified) } if !strings.Contains(entries[1].Unified, "-b-old") || !strings.Contains(entries[1].Unified, "+b-new") { t.Errorf("b.txt hunk missing expected lines:\n%s", entries[1].Unified) } } func TestDiff_WholeCheckin_AdditionOnly(t *testing.T) { r := newTestRepo(t) a := commit(t, r, 0, "v1", []FileToCommit{ {Name: "anchor.txt", Content: []byte("anchor\n")}, }) b := commit(t, r, a, "add new", []FileToCommit{ {Name: "anchor.txt", Content: []byte("anchor\n")}, {Name: "new.txt", Content: []byte("brand new\n")}, }) entries, err := r.Diff(a, b, "") if err != nil { t.Fatalf("Diff: %v", err) } if len(entries) != 1 { t.Fatalf("got %d entries, want 1:\n%+v", len(entries), entries) } if entries[0].Name != "new.txt" { t.Errorf("Name = %q, want %q", entries[0].Name, "new.txt") } if !strings.Contains(entries[0].Unified, "+brand new") { t.Errorf("expected addition hunk, got:\n%s", entries[0].Unified) } if strings.Contains(entries[0].Unified, "-brand new") { t.Errorf("unexpected deletion marker for pure addition:\n%s", entries[0].Unified) } } func TestDiff_WholeCheckin_DeletionOnly(t *testing.T) { r := newTestRepo(t) a := commit(t, r, 0, "v1", []FileToCommit{ {Name: "anchor.txt", Content: []byte("anchor\n")}, {Name: "doomed.txt", Content: []byte("goodbye\n")}, }) // PartialManifest: true models deletion by omission. b, _, err := r.Commit(CommitOpts{ ParentID: a, Files: []FileToCommit{{Name: "anchor.txt", Content: []byte("anchor\n")}}, Comment: "drop doomed", User: "test", PartialManifest: true, }) if err != nil { t.Fatalf("Commit: %v", err) } entries, err := r.Diff(a, b, "") if err != nil { t.Fatalf("Diff: %v", err) } if len(entries) != 1 { t.Fatalf("got %d entries, want 1:\n%+v", len(entries), entries) } if entries[0].Name != "doomed.txt" { t.Errorf("Name = %q, want %q", entries[0].Name, "doomed.txt") } if !strings.Contains(entries[0].Unified, "-goodbye") { t.Errorf("expected deletion hunk, got:\n%s", entries[0].Unified) } if strings.Contains(entries[0].Unified, "+goodbye") { t.Errorf("unexpected insertion marker for pure deletion:\n%s", entries[0].Unified) } }