Artifact 41c6ffcbcf8ae526d2ed586cdb114a4cdfce22ec3ad091c086a4f6d0089bc5b7:
- File
internal/verify/rebuild_manifest.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: 6019) [annotate] [blame] [check-ins using]
package verify import ( "fmt" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/blob" "github.com/danmestas/go-libfossil/internal/content" "github.com/danmestas/go-libfossil/internal/deck" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/repo" ) // rebuildManifests walks all non-phantom blobs, parses checkin manifests, // and inserts event/plink/mlink/filename rows. func rebuildManifests(r *repo.Repo, tx *db.Tx, report *Report) error { if r == nil { panic("rebuildManifests: nil *repo.Repo") } if tx == nil { panic("rebuildManifests: nil *db.Tx") } if report == nil { panic("rebuildManifests: nil *Report") } entries, err := collectBlobEntries(tx) if err != nil { return err } for _, e := range entries { data, err := content.Expand(tx, e.rid) if err != nil { report.BlobsSkipped++ continue // not expandable — corrupt, raw data blob, or phantom } d, err := deck.Parse(data) if err != nil { continue // not a manifest — normal for file blobs } if d.Type != deck.Checkin { continue } if err := rebuildCheckin(tx, e.rid, d, report); err != nil { return fmt.Errorf("rebuildManifests rid=%d: %w", e.rid, err) } } return nil } // blobEntry holds a blob's rid and uuid for rebuild iteration. type blobEntry struct { rid libfossil.FslID uuid string } // collectBlobEntries reads all non-phantom blob rid/uuid pairs. func collectBlobEntries(q db.Querier) ([]blobEntry, error) { rows, err := q.Query("SELECT rid, uuid FROM blob WHERE size >= 0") if err != nil { return nil, fmt.Errorf("collectBlobEntries: %w", err) } defer rows.Close() var entries []blobEntry for rows.Next() { var e blobEntry if err := rows.Scan(&e.rid, &e.uuid); err != nil { return nil, fmt.Errorf("collectBlobEntries scan: %w", err) } entries = append(entries, e) } return entries, rows.Err() } // rebuildCheckin inserts event, plink, and mlink rows for one checkin manifest. func rebuildCheckin(tx *db.Tx, rid libfossil.FslID, d *deck.Deck, report *Report) error { if tx == nil { panic("rebuildCheckin: nil *db.Tx") } if d == nil { panic("rebuildCheckin: nil *deck.Deck") } mtime := libfossil.TimeToJulian(d.D) // Insert event row if _, err := tx.Exec( "INSERT OR IGNORE INTO event(type, mtime, objid, user, comment) VALUES('ci', ?, ?, ?, ?)", mtime, rid, d.U, d.C, ); err != nil { return fmt.Errorf("event: %w", err) } // Insert plink rows for parent(s) if err := rebuildPlinks(tx, rid, d, mtime, report); err != nil { return err } // Insert mlink/filename rows for manifest F-cards. // Uses d.F directly (not expanded) — matches Fossil's mlink semantics. if err := rebuildMlinks(tx, rid, d); err != nil { return err } return nil } // rebuildPlinks inserts plink rows for each parent in the manifest. func rebuildPlinks(tx *db.Tx, rid libfossil.FslID, d *deck.Deck, mtime float64, report *Report) error { for i, parentUUID := range d.P { parentRID, ok := blob.Exists(tx, parentUUID) if !ok { report.MissingRefs++ report.addIssue(Issue{ Kind: IssueMissingReference, RID: rid, UUID: parentUUID, Table: "plink", Message: fmt.Sprintf("rid=%d parent %s not found", rid, parentUUID), }) continue } isPrim := 0 if i == 0 { isPrim = 1 } if _, err := tx.Exec( "INSERT OR IGNORE INTO plink(pid, cid, isprim, mtime) VALUES(?, ?, ?, ?)", parentRID, rid, isPrim, mtime, ); err != nil { return fmt.Errorf("plink: %w", err) } } return nil } // rebuildMlinks inserts mlink and filename rows for files that are new or // changed relative to the primary parent. Fossil's rebuild only creates mlink // rows for files that differ from the parent checkin — unchanged files are // skipped. This matches fossil rebuild behavior. func rebuildMlinks(tx *db.Tx, rid libfossil.FslID, d *deck.Deck) error { // Build parent file map (name → uuid) for comparison. parentFiles := buildParentFileMap(tx, d) for _, f := range d.F { if f.UUID == "" { continue // deleted file in delta manifest } // Skip unchanged files — only create mlink for new or modified. if parentUUID, exists := parentFiles[f.Name]; exists && parentUUID == f.UUID { continue } fnid, err := rebuildEnsureFilename(tx, f.Name) if err != nil { return fmt.Errorf("filename %q: %w", f.Name, err) } fileRID, ok := blob.Exists(tx, f.UUID) if !ok { continue // file blob missing — phantom or not yet received } if _, err := tx.Exec( "INSERT OR IGNORE INTO mlink(mid, fid, fnid) VALUES(?, ?, ?)", rid, fileRID, fnid, ); err != nil { return fmt.Errorf("mlink: %w", err) } } return nil } // buildParentFileMap returns a map of filename→UUID from the primary parent's // manifest. Returns an empty map if there is no parent (initial checkin). func buildParentFileMap(tx *db.Tx, d *deck.Deck) map[string]string { if len(d.P) == 0 { return nil // initial checkin — no parent } parentRID, ok := content.AvailableByUUID(tx, d.P[0]) // primary parent if !ok { return nil } parentData, err := content.Expand(tx, parentRID) if err != nil { return nil } parentDeck, err := deck.Parse(parentData) if err != nil { return nil } m := make(map[string]string, len(parentDeck.F)) for _, f := range parentDeck.F { m[f.Name] = f.UUID } return m } // rebuildEnsureFilename ensures a filename row exists and returns its fnid. func rebuildEnsureFilename(tx *db.Tx, name string) (int64, error) { if tx == nil { panic("rebuildEnsureFilename: nil *db.Tx") } if name == "" { panic("rebuildEnsureFilename: empty name") } var fnid int64 err := tx.QueryRow("SELECT fnid FROM filename WHERE name=?", name).Scan(&fnid) if err == nil { return fnid, nil } result, err := tx.Exec("INSERT INTO filename(name) VALUES(?)", name) if err != nil { return 0, fmt.Errorf("insert filename %q: %w", name, err) } return result.LastInsertId() }