git@cosmocr.at/gitweb.git @ HEAD · summary refs log
HEAD / internal / gitrepo / materialize.go /
internal/gitrepo/materialize.go · 3.4 KiB · raw
view: SourceMarkdown ☑ force

// Package gitrepo bridges upspin-stored bare git repositories to go-git. A bare // repo (HEAD, config, objects/, refs/) is copied out of upspin into a billy // filesystem and opened with go-git — for read-only browsing and clone, and // (via a private copy) for push write-back. package gitrepo

import ( "crypto/sha256" "errors" "fmt" "path" "strings"

"github.com/go-git/go-billy/v5"

"gitweb/internal/store"

)

// FileInfo records a materialized file's size and content hash, used by the // push write-back to detect which files changed. type FileInfo struct { Size int64 Sum [32]byte }

// Manifest maps a repo-relative file path (as stored in the billy filesystem, // using forward slashes and no leading slash) to its FileInfo. type Manifest map[string]FileInfo

// defaultMaxRepoBytes caps the total size materialized per repo. It bounds a // single request's footprint; very large repos are rejected rather than // exhausting the cache filesystem. const defaultMaxRepoBytes = 256 << 20 // 256 MiB

// ErrRepoTooLarge is returned (wrapped) by materialize when a repo exceeds the // materialization size cap. var ErrRepoTooLarge = errors.New("repository too large")

// materialize copies the bare-repo subtree rooted at repoPath out of the store // into fs, with the repo's internal files (HEAD, config, objects/, refs/) at // the filesystem root — the layout go-git's filesystem storage expects. fs // must be empty; total copied bytes are capped at maxBytes. It mirrors the // recursive Glob/recurse/copy walk of upspin's upspin tar command // (cmd/upspin/tar.go doArchive), but goes through the store.Store interface so // it also works against LocalStore in dev. func materialize(st store.Store, repoPath string, fs billy.Filesystem, maxBytes int64) (Manifest, error) { man := Manifest{} var total int64 if err := copyTree(st, strings.TrimRight(repoPath, "/"), "", fs, man, &total, maxBytes); err != nil { return nil, err } // Sanity-check the result really is a bare repo at the root. if _, err := fs.Stat("HEAD"); err != nil { return nil, fmt.Errorf("materialize %s: no HEAD at repo root: %w", repoPath, err) } if fi, err := fs.Stat("objects"); err != nil || !fi.IsDir() { return nil, fmt.Errorf("materialize %s: no objects/ dir at repo root", repoPath) } return man, nil }

// copyTree recursively copies upspin path src into billy path dst, recording // each copied file into man. func copyTree(st store.Store, src, dst string, fs billy.Filesystem, man Manifest, total *int64, maxBytes int64) error { entries, err := st.List(src) if err != nil { return err } for _, e := range entries { srcChild := src + "/" + e.Name dstChild := e.Name if dst != "" { dstChild = path.Join(dst, e.Name) } if e.IsDir { if err := fs.MkdirAll(dstChild, 0o755); err != nil { return err } if err := copyTree(st, srcChild, dstChild, fs, man, total, maxBytes); err != nil { return err } continue } data, err := st.Get(srcChild) if err != nil { return err } *total += int64(len(data)) if *total > maxBytes { return fmt.Errorf("%w (limit %d bytes)", ErrRepoTooLarge, maxBytes) } f, err := fs.Create(dstChild) if err != nil { return err } if _, err := f.Write(data); err != nil { f.Close() return err } if err := f.Close(); err != nil { return err } man[dstChild] = FileInfo{Size: int64(len(data)), Sum: sha256.Sum256(data)} } return nil }