A read-only clone of the dehub project, for until dehub.dev can be brought back online.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
dehub/repo.go

273 lines
7.8 KiB

// Package dehub TODO needs package docs
package dehub
import (
"dehub/fs"
"errors"
"fmt"
"path/filepath"
"gopkg.in/src-d/go-billy.v4"
"gopkg.in/src-d/go-billy.v4/memfs"
"gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/object"
"gopkg.in/src-d/go-git.v4/storage"
"gopkg.in/src-d/go-git.v4/storage/memory"
)
const (
// DehubDir defines the name of the directory where all dehub-related files are
// expected to be found.
DehubDir = ".dehub"
)
var (
// ConfigPath defines the expected path to the Repo's configuration file.
ConfigPath = filepath.Join(DehubDir, "config.yml")
// Main defines the name of the main branch.
Main = "main"
// MainRefName defines the reference name of the main branch.
MainRefName = plumbing.NewBranchReferenceName(Main)
)
type repoOpts struct {
bare bool
}
// OpenOption is an option which can be passed to the OpenRepo function to
// affect the Repo's behavior.
type OpenOption func(*repoOpts)
// OpenBare returns an OpenOption which, if true is given, causes the OpenRepo
// function to expect to open a bare repo.
func OpenBare(bare bool) OpenOption {
return func(o *repoOpts) {
o.bare = bare
}
}
// Repo is an object which allows accessing and modifying the dehub repo.
type Repo struct {
GitRepo *git.Repository
Storer storage.Storer
}
// OpenRepo opens the dehub repo in the given directory and returns the object
// for it.
//
// The given path is expected to have a git repo and .dehub folder already
// initialized.
func OpenRepo(path string, options ...OpenOption) (*Repo, error) {
var opts repoOpts
for _, opt := range options {
opt(&opts)
}
r := Repo{}
var err error
openOpts := &git.PlainOpenOptions{
DetectDotGit: !opts.bare,
}
if r.GitRepo, err = git.PlainOpenWithOptions(path, openOpts); err != nil {
return nil, fmt.Errorf("could not open git repo: %w", err)
}
return &r, nil
}
// InitMemRepo initializes an empty repository which only exists in memory.
func InitMemRepo() *Repo {
r, err := git.Init(memory.NewStorage(), memfs.New())
if err != nil {
panic(err)
}
repo := &Repo{GitRepo: r}
if err := repo.init(); err != nil {
panic(err)
}
return repo
}
func (r *Repo) init() error {
h := plumbing.NewSymbolicReference(plumbing.HEAD, MainRefName)
if err := r.GitRepo.Storer.SetReference(h); err != nil {
return fmt.Errorf("setting HEAD reference to %q: %w", MainRefName, err)
}
return nil
}
func (r *Repo) billyFilesystem() (billy.Filesystem, error) {
w, err := r.GitRepo.Worktree()
if err != nil {
return nil, fmt.Errorf("opening git worktree: %w", err)
}
return w.Filesystem, nil
}
// CheckedOutBranch returns the name of the currently checked out branch.
func (r *Repo) CheckedOutBranch() (plumbing.ReferenceName, error) {
// Head() can't be used for this, because it doesn't handle the case of a
// newly initialized repo very well.
ogRef, err := r.GitRepo.Storer.Reference(plumbing.HEAD)
if err != nil {
return "", fmt.Errorf("de-referencing HEAD (is it a bare repo?): %w", err)
}
ref := ogRef
for {
if ref.Type() != plumbing.SymbolicReference {
break
}
target := ref.Target()
if target.IsBranch() {
return target, nil
}
ref, err = r.GitRepo.Storer.Reference(target)
if err != nil {
break
}
}
return "", fmt.Errorf("could not de-reference HEAD to a branch: %w", err)
}
// headFS returns an FS based on the HEAD commit, or if there is no HEAD commit
// (it's an empty repo) an FS based on the raw filesystem.
func (r *Repo) headFS() (fs.FS, error) {
head, err := r.GetGitHead()
if errors.Is(err, ErrNoHead) {
bfs, err := r.billyFilesystem()
if err != nil {
return nil, fmt.Errorf("getting underlying filesystem: %w", err)
}
return fs.FromBillyFilesystem(bfs), nil
} else if err != nil {
return nil, fmt.Errorf("could not get HEAD tree: %w", err)
}
return fs.FromTree(head.GitTree), nil
}
// GitCommit wraps a single git commit object, and also contains various fields
// which are parsed out of it. It is used as a convenience type, in place of
// having to manually retrieve and parse specific information out of commit
// objects.
type GitCommit struct {
GitCommit *object.Commit
// Fields based on that Commit, which can't be directly gleaned from it.
GitTree *object.Tree
Commit Commit
Interface CommitInterface
}
// Root returns true if this commit is the root commit in its branch (i.e. it
// has no parents)
func (gc GitCommit) Root() bool {
return gc.GitCommit.NumParents() == 0
}
// GetGitCommit retrieves the commit at the given hash, and all of its sub-data
// which can be pulled out of it.
func (r *Repo) GetGitCommit(h plumbing.Hash) (gc GitCommit, err error) {
if gc.GitCommit, err = r.GitRepo.CommitObject(h); err != nil {
return gc, fmt.Errorf("getting git commit object: %w", err)
} else if gc.GitTree, err = r.GitRepo.TreeObject(gc.GitCommit.TreeHash); err != nil {
return gc, fmt.Errorf("getting git tree object %q: %w",
gc.GitCommit.TreeHash, err)
} else if gc.Commit.UnmarshalText([]byte(gc.GitCommit.Message)); err != nil {
return gc, fmt.Errorf("decoding commit message: %w", err)
} else if gc.Interface, err = gc.Commit.Interface(); err != nil {
return gc, fmt.Errorf("casting %+v to a CommitInterface: %w", gc.Commit, err)
}
return
}
// GetGitRevision resolves the revision and returns the GitCommit it references.
func (r *Repo) GetGitRevision(rev plumbing.Revision) (GitCommit, error) {
// This returns a pointer for some reason, not sure why.
h, err := r.GitRepo.ResolveRevision(rev)
if err != nil {
return GitCommit{}, fmt.Errorf("resolving revision: %w", err)
}
gc, err := r.GetGitCommit(*h)
if err != nil {
return GitCommit{}, fmt.Errorf("getting commit %q: %w", *h, err)
}
return gc, nil
}
// ErrNoHead is returns from GetGitHead if there is no HEAD reference defined in
// the repo. This can happen if the repo has no commits
var ErrNoHead = errors.New("HEAD reference not found")
// GetGitHead returns the GitCommit which is currently referenced by HEAD.
// This method may return ErrNoHead if the repo has no commits.
func (r *Repo) GetGitHead() (GitCommit, error) {
head, err := r.GitRepo.Head()
if errors.Is(err, plumbing.ErrReferenceNotFound) {
return GitCommit{}, ErrNoHead
} else if err != nil {
return GitCommit{}, fmt.Errorf("resolving HEAD: %w", err)
}
headHash := head.Hash()
gc, err := r.GetGitCommit(headHash)
if err != nil {
return GitCommit{}, fmt.Errorf("getting commit %q: %w", headHash, err)
}
return gc, nil
}
// GetGitCommitRange returns an ancestry of GitCommits, with the first being the
// commit immediately following the given starting hash, and the last being the
// given ending hash.
//
// If start is plumbing.ZeroHash then the root commit will be the starting one.
func (r *Repo) GetGitCommitRange(start, end plumbing.Hash) ([]GitCommit, error) {
curr, err := r.GetGitCommit(end)
if err != nil {
return nil, fmt.Errorf("retrieving commit %q: %w", end, err)
}
var commits []GitCommit
var found bool
for {
commits = append(commits, curr)
numParents := curr.GitCommit.NumParents()
if numParents == 0 {
break
} else if numParents > 1 {
return nil, fmt.Errorf("commit %q has more than one parent: %+v",
curr.GitCommit.Hash, curr.GitCommit.ParentHashes)
}
parentHash := curr.GitCommit.ParentHashes[0]
parent, err := r.GetGitCommit(parentHash)
if err != nil {
return nil, fmt.Errorf("retrieving commit %q: %w", parentHash, err)
} else if start != plumbing.ZeroHash && parentHash == start {
found = true
break
}
curr = parent
}
if !found && start != plumbing.ZeroHash {
return nil, fmt.Errorf("unable to find commit %q as an ancestor of %q",
start, end)
}
// reverse the commits to be in the expected order
for l, r := 0, len(commits)-1; l < r; l, r = l+1, r-1 {
commits[l], commits[r] = commits[r], commits[l]
}
return commits, nil
}