// 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") // Trunk defines the name of the trunk branch. Trunk = "trunk" // TrunkRefName defines the reference name of the trunk branch. TrunkRefName = plumbing.NewBranchReferenceName(Trunk) ) 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, TrunkRefName) if err := r.GitRepo.Storer.SetReference(h); err != nil { return fmt.Errorf("could not set HEAD to %q: %w", TrunkRefName, err) } return nil } func (r *Repo) billyFilesystem() (billy.Filesystem, error) { w, err := r.GitRepo.Worktree() if err != nil { return nil, fmt.Errorf("could not open 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("couldn't de-reference 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) } func (r *Repo) head() (*object.Commit, *object.Tree, error) { head, err := r.GitRepo.Head() if err != nil { return nil, nil, fmt.Errorf("could not get repo HEAD: %w", err) } headHash := head.Hash() headCommit, err := r.GitRepo.CommitObject(headHash) if err != nil { return nil, nil, fmt.Errorf("could not get commit at HEAD (%q): %w", headHash, err) } headTree, err := r.GitRepo.TreeObject(headCommit.TreeHash) if err != nil { return nil, nil, fmt.Errorf("could not get tree object at HEAD (commit:%q tree:%q): %w", headHash, headCommit.TreeHash, err) } return headCommit, headTree, nil } // headOrRawFS 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) headOrRawFS() (fs.FS, error) { _, headTree, err := r.head() if errors.Is(err, plumbing.ErrReferenceNotFound) { bfs, err := r.billyFilesystem() if err != nil { return nil, fmt.Errorf("could not get 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(headTree), nil }