Artifact 5fa15f1a981b0d84bc0ac565c572a7f6595576a7110ee474c05b08ce959f4018:
- File
fossil.go
— part of check-in
[229c02160d]
at
2026-10-25 21:04:33
on branch fix/create-default-login
— fix(create): seed fossil's fallback login when no user is configured
Create with an empty CreateOpts.User read only $USER and then panicked in db.SeedUser on the empty login whenever that variable was unset, as it is in many containers and CI runners. The default login is now chosen as fossil(1)'s db_initial_setup chooses it: the first of USER, LOGNAME and USERNAME that is set, else "root". (user: [email protected], size: 8001) [annotate] [blame] [check-ins using] [more...]
package libfossil import ( "context" "fmt" "math" "os" "path/filepath" "github.com/danmestas/go-libfossil/internal/cone" "github.com/danmestas/go-libfossil/internal/repo" internalsync "github.com/danmestas/go-libfossil/internal/sync" "github.com/danmestas/go-libfossil/simio" ) // CreateOpts configures repository creation. type CreateOpts struct { User string // ProjectCode optionally sets the repo's project-code. Empty // generates a fresh one (current behavior). When non-empty, must // be 40-char lowercase hex (^[0-9a-f]{40}$) — matching the format // of generated project-codes. Invalid values return an error // before any file is written. ProjectCode string // Rand provides random bytes for project-code and server-code generation. // Nil defaults to crypto/rand (production). Set to simio.NewSeededRand // for deterministic simulation testing. Rand simio.Rand } // Create creates a new Fossil repository at the given path. func Create(path string, opts CreateOpts) (*Repo, error) { if opts.ProjectCode != "" && !isProjectCode40HexLower(opts.ProjectCode) { return nil, fmt.Errorf("libfossil: invalid CreateOpts.ProjectCode %q: must be 40-char lowercase hex", opts.ProjectCode) } if _, err := os.Stat(path); err == nil { return nil, fmt.Errorf("libfossil: repository already exists: %s", path) } if dir := filepath.Dir(path); !isDir(dir) { return nil, fmt.Errorf("libfossil: create %s: parent directory does not exist: %s", path, dir) } user := opts.User if user == "" { user = defaultLogin() } rng := opts.Rand if rng == nil { rng = simio.CryptoRand{} } inner, err := repo.Create(path, user, rng, opts.ProjectCode) if err != nil { return nil, fmt.Errorf("libfossil: create: %w", err) } return &Repo{inner: inner, path: path}, nil } // defaultLogin is the admin login a new repository seeds when CreateOpts.User // is empty, chosen as fossil(1)'s db_initial_setup chooses it: the first of // USER, LOGNAME and USERNAME that is set, else "root". It is never empty, so // a process with no user in its environment, as a container or a CI runner // often is, still creates a repository instead of failing on an empty login. func defaultLogin() string { for _, name := range []string{"USER", "LOGNAME", "USERNAME"} { if v := os.Getenv(name); v != "" { return v } } return "root" } func isDir(path string) bool { info, err := os.Stat(path) return err == nil && info.IsDir() } func isProjectCode40HexLower(s string) bool { if len(s) != 40 { return false } for i := 0; i < len(s); i++ { c := s[i] if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') { continue } return false } return true } // Open opens an existing Fossil repository. func Open(path string) (*Repo, error) { inner, err := repo.Open(path) if err != nil { return nil, fmt.Errorf("libfossil: open: %w", err) } return &Repo{inner: inner, path: path}, nil } // CloneResult reports what happened during a clone. type CloneResult struct { Rounds int BlobsRecvd int ArtifactsLinked int ProjectCode string ServerCode string Messages []string // Elided counts the artifacts left absent under the store cone, and // ConeServed the replies whose server acknowledged applying it (pragma // cone-served, draft-fossil-sparse-cone-00 §6.3). Both are zero for a // dense clone. Elided, ConeServed int // HorizonServed counts the replies whose server served the store's // horizon (pragma horizon-served, draft-fossil-horizon-00 §6.2.5); the // other rounds were classified locally. StructuralServed counts those // served at the structural level, and HorizonFallback reports a // structural clone that kept the content horizon instead (§6.3). HorizonServed int StructuralServed int HorizonFallback bool // HorizonHistoryURL is the interface outside the sync protocol at which // the origin says history below a horizon can be queried (pragma // horizon-served URL, draft-fossil-horizon-00 §6.2.5), the last a reply // named; "" when none did. An origin names it with // SettingHorizonHistoryURL. HorizonHistoryURL string // HorizonResolved is the check-in CloneOpts.Horizon named resolved to and // HorizonSteps the steps back it took, fewer than HorizonDepth when // history ran out; HorizonResolvedLocally reports that the origin did // not resolve the name and the clone did, at the content horizon. HorizonResolved string HorizonSteps int HorizonResolvedLocally bool } // Clone performs a full repository clone from a remote Fossil server. // It creates a new repository at the given path, runs the clone protocol // until convergence, and returns the opened Repo handle and a result summary. // On error, the partially-created repo file is removed. Artifacts the clone // rounds leave missing stay phantoms unless opts.PullPhantoms is set. func Clone(ctx context.Context, path string, t Transport, opts CloneOpts) (*Repo, *CloneResult, error) { adapter := &transportAdapter{pub: t} storeCone, named, err := cloneHorizon(opts) if err != nil { return nil, nil, fmt.Errorf("libfossil: clone: %w", err) } iOpts := internalsync.CloneOpts{ User: opts.User, Password: opts.Password, Observer: adaptObserver(opts.Observer), Buggify: opts.Buggify, PullPhantoms: opts.PullPhantoms, StoreCone: storeCone, SeedKey: opts.SeedKey, Named: named, } // A transport that names its peer keys the horizon cursor, as for Sync. if o, ok := t.(interface{ syncOrigin() string }); ok { iOpts.Origin = o.syncOrigin() } inner, iResult, err := internalsync.Clone(ctx, path, adapter, iOpts) if err != nil { return nil, nil, fmt.Errorf("libfossil: clone: %w", err) } result := &CloneResult{ Rounds: iResult.Rounds, BlobsRecvd: iResult.BlobsRecvd, ArtifactsLinked: iResult.ArtifactsLinked, ProjectCode: iResult.ProjectCode, ServerCode: iResult.ServerCode, Messages: iResult.Messages, Elided: iResult.Elided, ConeServed: iResult.ConeServed, HorizonServed: iResult.HorizonServed, StructuralServed: iResult.StructuralServed, HorizonFallback: iResult.HorizonFallback, HorizonHistoryURL: iResult.HorizonHistoryURL, HorizonResolved: iResult.HorizonResolved, HorizonSteps: int(iResult.HorizonSteps), HorizonResolvedLocally: iResult.HorizonResolvedLocally, } return &Repo{inner: inner, path: path}, result, nil } // cloneHorizon splits a clone's horizon into the store cone it starts with // and the horizon the origin is asked to resolve. A Horizon is always sent // by name, a full hash included: the origin tries a full hash as a // check-in and then as a symbolic name (§6.2.7), so a branch spelled as 40 // hex digits is found, and a peer that resolves no name is covered by the // local fallback. Only a horizon given on StoreCone goes out as the hash. func cloneHorizon(opts CloneOpts) (*cone.Cone, *internalsync.NamedHorizon, error) { sc := unwrapCone(opts.StoreCone) if opts.HorizonDepth < 0 || int64(opts.HorizonDepth) > math.MaxUint32 { return nil, nil, fmt.Errorf("horizon depth %d out of range", opts.HorizonDepth) } h := opts.Horizon if h == nil { if opts.HorizonDepth != 0 { return nil, nil, fmt.Errorf("a horizon depth needs a Horizon") } return sc, nil, nil } if _, ok := horizonOf(sc); ok { return nil, nil, fmt.Errorf("a horizon on both StoreCone and Horizon") } if h.Checkin == "" { return nil, nil, fmt.Errorf("a Horizon with no check-in") } level := cone.DepthContent if h.Structural { level = cone.DepthStructural } return sc, &internalsync.NamedHorizon{Name: h.Checkin, Depth: uint32(opts.HorizonDepth), Rule: cone.HorizonRule(h.Rule), Level: level}, nil } // horizonOf is c's horizon, ok false for nil or a cone without one. func horizonOf(c *cone.Cone) (cone.Horizon, bool) { if c == nil { return cone.Horizon{}, false } return c.Horizon() }