dehub/repo.go
mediocregopher cf05b3a072 Refactor commit type and logic to account for future commit types
---
type: change
message: |-
  Refactor commit type and logic to account for future commit types

  This commit introduces a CommitInterface which CommitChange (previously
  ChangeCommit) now implements. Additionally, now all commit messages will include
  a type field and a "---" separator. The code is written to still accept all the
  old commit messages used in this repo. Other than those changes, most of this is
  just rearranging existing code.
change_hash: AHjWAUxCjXgOL0Sb+oQZc6TmuVgVHJn08zpGreYyChwx
credentials:
- type: pgp_signature
  pub_key_id: 95C46FA6A41148AC
  body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl5gOuoACgkQlcRvpqQRSKxapA//bODd2IwX2D7nFBkEEd00ol1l4vaw7pgwCjqyQtyskeCZ5IH6H6PkYOSmU9DIBde+cGo35Oi5ynChmfnSatvUZ1dLRJqm8FfOGDw/IsccyYDd1iptj16Ckr6Bsht1XgFJNN10hufuAg77fRIwbGi003WCQdnrJZ1Xbtgex6a4rUqVFW+sjXAB1msmo5B5nabfm/ta0epptxhlINIY2qP5Vb+ftdb2lRUNQwkIEr5GErfAgN3sxYEfqQvLzr/007tEeVznyhdgww47awWsBaTEd6njycbWpphnNA8PbcgUGKDYsj3qLz/bCR1VQOdWgod8PLEru2O+uC2+72ssfT9NDn8HB/WrBKG4W4oykJIhXJ5um18/B9ZSxPMlTQv2Yr4nF7w+Sx3UynEdUTducbIOUq2K+JnI+Ln3gHe0yRw5UEfrzymR4f2Nfe1I13rJk2W7SVRDmOYidr0MwBlLs8tmnJFMWmWZtd1hpPOpyOyUF1jKgvMuR9ferb5niuc3Lk4trqDiaF+tjy/NmAv/7c+qiVmKKpVJVVqvT60TqBR9DTHjRGktcPFD50sc811Th+Xd9RdhzpIYM+0DT790FTf8E0hY6wm/NKTGplfqwBSNZk87SeIiFTu7sZWVpAaPz1vTmVGduC1oj3/Zlv6TzNrUAp3VwBepROBhZlHCHUr9tKg=
  account: mediocregopher
2020-03-04 16:34:02 -07:00

176 lines
4.5 KiB
Go

// 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("could not set HEAD 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("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
}
// 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) {
_, 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
}