{"contents":"package git\n\nimport (\n\t\"fmt\"\n\n\tgogit \"github.com/go-git/go-git/v5\"\n\t\"github.com/go-git/go-git/v5/plumbing\"\n\t\"github.com/go-git/go-git/v5/plumbing/object\"\n\t\"github.com/go-git/go-git/v5/storage\"\n\n\tknotconfig \"tangled.org/http-knot/config\"\n\ts3store \"tangled.org/http-knot/storage/s3\"\n)\n\n// Repo wraps a go-git repository backed by S3 storage.\ntype Repo struct {\n\tr *gogit.Repository\n\ts *s3store.Storage\n\thead *plumbing.Reference\n}\n\n// Open opens an existing repo from S3 at the given ref (branch/tag/sha).\n// If ref is empty, HEAD is used.\nfunc Open(cfg knotconfig.S3, did, name, ref string) (*Repo, error) {\n\ts, err := s3store.NewStorage(cfg, did, name)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open storage: %w\", err)\n\t}\n\n\treturn OpenFromStorer(s, ref)\n}\n\n// OpenFromStorer opens a repo from an existing storage.Storer.\nfunc OpenFromStorer(s storage.Storer, ref string) (*Repo, error) {\n\tst, ok := s.(*s3store.Storage)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"expected *s3store.Storage\")\n\t}\n\n\tr, err := gogit.Open(s, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open repo: %w\", err)\n\t}\n\n\trepo := \u0026Repo{r: r, s: st}\n\n\tif ref != \"\" {\n\t\tresolved, err := repo.resolveRef(ref)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"resolve ref %q: %w\", ref, err)\n\t\t}\n\t\trepo.head = resolved\n\t} else {\n\t\thead, err := r.Head()\n\t\tif err != nil {\n\t\t\t// Empty repo — HEAD points to an unborn branch. That's OK.\n\t\t\trepo.head = nil\n\t\t} else {\n\t\t\trepo.head = head\n\t\t}\n\t}\n\n\treturn repo, nil\n}\n\nfunc (repo *Repo) resolveRef(ref string) (*plumbing.Reference, error) {\n\t// Try as branch\n\tbranchRef, err := repo.r.Reference(plumbing.NewBranchReferenceName(ref), true)\n\tif err == nil {\n\t\treturn branchRef, nil\n\t}\n\n\t// Try as tag\n\ttagRef, err := repo.r.Reference(plumbing.NewTagReferenceName(ref), true)\n\tif err == nil {\n\t\treturn tagRef, nil\n\t}\n\n\t// Try as full ref name\n\tfullRef, err := repo.r.Reference(plumbing.ReferenceName(ref), true)\n\tif err == nil {\n\t\treturn fullRef, nil\n\t}\n\n\t// Try as SHA\n\tif len(ref) == 40 {\n\t\th := plumbing.NewHash(ref)\n\t\treturn plumbing.NewHashReference(\"\", h), nil\n\t}\n\n\treturn nil, fmt.Errorf(\"cannot resolve %q\", ref)\n}\n\n// IsEmpty returns true if the repo has no commits.\nfunc (repo *Repo) IsEmpty() bool {\n\treturn repo.head == nil\n}\n\n// HeadCommit returns the commit at the current head.\nfunc (repo *Repo) HeadCommit() (*object.Commit, error) {\n\tif repo.head == nil {\n\t\treturn nil, fmt.Errorf(\"empty repository\")\n\t}\n\treturn repo.r.CommitObject(repo.head.Hash())\n}\n\n// CommitObject returns a commit by hash.\nfunc (repo *Repo) CommitObject(h plumbing.Hash) (*object.Commit, error) {\n\treturn repo.r.CommitObject(h)\n}\n\n// Repository returns the underlying go-git repository.\nfunc (repo *Repo) Repository() *gogit.Repository {\n\treturn repo.r\n}\n\n// Head returns the resolved head reference.\nfunc (repo *Repo) Head() *plumbing.Reference {\n\treturn repo.head\n}\n","path":"git/repo.go","ref":"main"}