Artifact 55901f31edf9c19b01a594b64a608300f3e6f970de7a35f0e414c6584e212501:
- File
internal/shun/shun.go
— part of check-in
[aea957939f]
at
2026-10-12 22:57:27
on branch main
— fix(sync): drop shunned artifacts and never ask for a shunned phantom (#302) (#355)
* fix(sync): drop shunned artifacts and never ask for a shunned phantom (#302)
fossil's xfer_accept_file and xfer_accept_compressed_file drop a received artifact whose hash is on the shun list, and its request_phantoms and content_new leave shunned names out of what a client asks for (sections 3.3 and 7.7). The server, the sync client and clone stored a shunned artifact, and the client asked for shunned phantoms.
storeReceivedFile, which every receive path shares, now drops a shunned artifact; the server checks first so a dropped file is not reported as missing after store. The client's phantom scan and its igot handling skip shunned names, so a delta whose basis is shunned is kept as a delta and the basis is never requested.
* refactor(sync): split the client's reply handling into per-card helpers
processResponse had grown to 232 lines: one switch over every reply card followed by the convergence check. It is now a loop over handleReplyCard, which hands each card to a focused helper, and converged, which keeps the termination rules apart from card handling. Behavior is unchanged. (user: [email protected], size: 2079) [annotate] [blame] [check-ins using]
package shun import ( "database/sql" "errors" "fmt" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/hash" ) // Entry represents a shunned artifact. type Entry struct { UUID string MTime int64 Comment string } // Add marks an artifact as shunned. Idempotent — re-shunning updates // mtime and comment. Returns error if uuid format is invalid. func Add(q db.Querier, uuid, comment string) error { if q == nil { panic("shun.Add: q must not be nil") } if uuid == "" || !hash.IsValidHash(uuid) { return fmt.Errorf("shun.Add: invalid UUID %q", uuid) } _, err := q.Exec( "INSERT OR REPLACE INTO shun(uuid, mtime, scom) VALUES(?, strftime('%s','now'), ?)", uuid, comment, ) return err } // Remove unshuns an artifact. No-op if uuid is not shunned. func Remove(q db.Querier, uuid string) error { if q == nil { panic("shun.Remove: q must not be nil") } _, err := q.Exec("DELETE FROM shun WHERE uuid=?", uuid) return err } // IsShunned returns true if the given uuid is in the shun table. func IsShunned(q db.Querier, uuid string) (bool, error) { if q == nil { panic("shun.IsShunned: q must not be nil") } var n int err := q.QueryRow("SELECT 1 FROM shun WHERE uuid=?", uuid).Scan(&n) if errors.Is(err, sql.ErrNoRows) { return false, nil } if err != nil { // A failed lookup must not let a shunned artifact through. return false, fmt.Errorf("shun.IsShunned: %w", err) } return true, nil } // List returns all shunned artifacts ordered by mtime descending. func List(q db.Querier) ([]Entry, error) { if q == nil { panic("shun.List: q must not be nil") } rows, err := q.Query("SELECT uuid, CAST(mtime AS INTEGER), COALESCE(scom,'') FROM shun ORDER BY mtime DESC") if err != nil { return nil, fmt.Errorf("shun.List: %w", err) } defer rows.Close() var entries []Entry for rows.Next() { var e Entry if err := rows.Scan(&e.UUID, &e.MTime, &e.Comment); err != nil { return nil, fmt.Errorf("shun.List scan: %w", err) } entries = append(entries, e) } return entries, rows.Err() }