// 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
}