Artifact 61b77385f1cbef9d425a55d29c3197ef495bfa021cb039a7b1f058512273a4fc:
- File
repo_extras.go
— part of check-in
[b5c515e10f]
at
2026-10-12 00:31:53
on branch main
— fix(libfossil): sym- prefix and reserved-prefix refusal in Repo.Tag (#293) (#329)
fossil tag add places a check-in tag in the symbolic namespace with a sym- prefix, so it resolves as a branch/tag name in update and merge, and refuses the reserved wiki-, tkt- and event- prefixes. Repo.Tag stored the name verbatim. It now refuses the reserved prefixes and, when the target is a check-in (fossil's whatis_rid_type==CFTYPE_MANIFEST), prefixes sym-; other artifact types keep the name bare. The raw tag.AddTag primitive is unchanged. Callers pass the bare name; fossil prefixes the raw name, so sym-x becomes sym-sym-x on a check-in. (user: [email protected], size: 5903) [annotate] [blame] [check-ins using]
package libfossil import ( "fmt" "strings" "time" "github.com/danmestas/go-libfossil/internal/content" "github.com/danmestas/go-libfossil/internal/deck" "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/tag" "github.com/danmestas/go-libfossil/internal/uv" ) // reservedTagPrefixes are the tag-name prefixes fossil manages internally and // refuses from `fossil tag add`/`cancel`, to keep a client from tagging an // artifact as, say, a wiki page or ticket (src/tag.c tag_cmd_tagname_check). var reservedTagPrefixes = []string{"wiki-", "tkt-", "event-"} // StashEntry describes a saved stash. type StashEntry struct { ID int64 Comment string Time string } // TagOpts configures a tag operation. type TagOpts struct { Name string TargetID int64 Value string User string Time time.Time } // UVEntry describes an unversioned file. type UVEntry struct { Name string Size int64 Mtime time.Time Hash string } // Tag creates a control artifact that adds a tag to a target artifact. // Returns the UUID of the tag control artifact. // // As `fossil tag add` does, a name with a reserved prefix ("wiki-", "tkt-", // "event-") is refused, and when the target is a check-in the name is placed // in the symbolic namespace with a "sym-" prefix so it resolves as a tag in // commands such as update and merge; other artifact types keep the name bare. // Pass the raw name: fossil prefixes the raw name, so "x" becomes "sym-x" and // "sym-x" becomes "sym-sym-x" on a check-in. func (r *Repo) Tag(opts TagOpts) (int64, error) { for _, prefix := range reservedTagPrefixes { if strings.HasPrefix(opts.Name, prefix) { return 0, fmt.Errorf("libfossil: tag: invalid prefix for tag name: %s", opts.Name) } } name := opts.Name if r.targetIsCheckin(fsltype.FslID(opts.TargetID)) { name = "sym-" + name } rid, err := tag.AddTag(r.inner, tag.TagOpts{ TargetRID: fsltype.FslID(opts.TargetID), TagName: name, TagType: tag.TagSingleton, Value: opts.Value, User: opts.User, Time: opts.Time, }) if err != nil { return 0, fmt.Errorf("libfossil: tag: %w", err) } return int64(rid), nil } // targetIsCheckin reports whether rid is a check-in manifest, fossil's // whatis_rid_type(rid)==CFTYPE_MANIFEST. It reads the artifact's own bytes, so // it holds before the blob is crosslinked, and a blob that is absent, // unreadable, or not an artifact reads as not a check-in. func (r *Repo) targetIsCheckin(rid fsltype.FslID) bool { data, err := content.Expand(r.inner.DB(), rid) if err != nil { return false } d, err := deck.Parse(data) if err != nil { return false } return d.Type == deck.Checkin } // StashSave saves working-tree changes to the stash. // Requires a checkout database; not yet wired (Repo only wraps the repo DB). func (r *Repo) StashSave(dir, comment string) error { return fmt.Errorf("libfossil: stash save: not yet implemented (requires checkout DB)") } // StashPop restores the most recent stash entry and removes it. func (r *Repo) StashPop(dir string) error { return fmt.Errorf("libfossil: stash pop: not yet implemented (requires checkout DB)") } // StashApply restores a stash entry by ID without removing it. func (r *Repo) StashApply(dir string, id int64) error { return fmt.Errorf("libfossil: stash apply: not yet implemented (requires checkout DB)") } // StashList returns all stash entries. func (r *Repo) StashList() ([]StashEntry, error) { return nil, fmt.Errorf("libfossil: stash list: not yet implemented (requires checkout DB)") } // StashDrop removes a stash entry by ID. func (r *Repo) StashDrop(id int64) error { return fmt.Errorf("libfossil: stash drop: not yet implemented (requires checkout DB)") } // StashClear removes all stash entries. func (r *Repo) StashClear() error { return fmt.Errorf("libfossil: stash clear: not yet implemented (requires checkout DB)") } // Undo reverts the last commit or merge. // Requires a checkout database; not yet wired. func (r *Repo) Undo(dir string) error { return fmt.Errorf("libfossil: undo: not yet implemented (requires checkout DB)") } // Redo re-applies the last undone operation. // Requires a checkout database; not yet wired. func (r *Repo) Redo(dir string) error { return fmt.Errorf("libfossil: redo: not yet implemented (requires checkout DB)") } // UVWrite writes an unversioned file to the repository. func (r *Repo) UVWrite(name string, content []byte, mtime time.Time) error { if err := uv.EnsureSchema(r.inner.DB()); err != nil { return fmt.Errorf("libfossil: uv write: %w", err) } mtimeUnix := mtime.Unix() if mtime.IsZero() { mtimeUnix = time.Now().Unix() } return uv.Write(r.inner.DB(), name, content, mtimeUnix) } // UVDelete marks an unversioned file as deleted (tombstone). func (r *Repo) UVDelete(name string, mtime time.Time) error { if err := uv.EnsureSchema(r.inner.DB()); err != nil { return fmt.Errorf("libfossil: uv delete: %w", err) } mtimeUnix := mtime.Unix() if mtime.IsZero() { mtimeUnix = time.Now().Unix() } return uv.Delete(r.inner.DB(), name, mtimeUnix) } // UVRead reads an unversioned file from the repository. // Returns the content, mtime (unix seconds), and content hash. func (r *Repo) UVRead(name string) ([]byte, int64, string, error) { return uv.Read(r.inner.DB(), name) } // UVList returns all unversioned file entries. func (r *Repo) UVList() ([]UVEntry, error) { if err := uv.EnsureSchema(r.inner.DB()); err != nil { return nil, fmt.Errorf("libfossil: uv list: %w", err) } entries, err := uv.List(r.inner.DB()) if err != nil { return nil, fmt.Errorf("libfossil: uv list: %w", err) } result := make([]UVEntry, len(entries)) for i, e := range entries { result[i] = UVEntry{ Name: e.Name, Size: int64(e.Size), Mtime: time.Unix(e.MTime, 0), Hash: e.Hash, } } return result, nil }