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/project.go

327 lines
9.8 KiB

Completely refactor naming of everything, in light of new SPEC --- type: change description: |- Completely refactor naming of everything, in light of new SPEC Writing the SPEC shed some light on just how weakly a lot of concepts, like "commit", had been defined, and prompted the delineation of a lot of things along specific lines (commit vs payload, repo vs project). This commit makes the code reflect the SPEC much better in quite a few ways: * Repo is now Project * Commit is now Payload * GitCommit is now just Commit * Hash is now Fingerprint * A lot of minor fields got renamed * All the XXXInterface types are now just XXX, and their old XXX type is now XXXUnion. More than likely there's still some comments and variable names that have slipped passed, but overall I feel like I got most of the changes. fingerprint: AKkDC5BKhKbfXzZQ/F4KquHeMgVvcNxgLmkZFz/nP/tY credentials: - type: pgp_signature pub_key_id: 95C46FA6A41148AC body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl6l7aYACgkQlcRvpqQRSKxFrA//VQ+f8B6pwGS3ORB4VVBnHvvJTGZvAYTvB0fHuHJx2EreR4FwjhaNakk5ClkwbO7WFMq++2OV4xIkvzwswLdbXZF0IHx3wScQM59v4vIkR4V9Lj5p1aGGhQna52uIKugF2gTqKdU4tqYzmBjDND/c2XDwCN5CwTwwnAHXUSSsHxviiPUYPWV5wzFP7uyRW0ZeK8Isv7QECKRXlsDjcSJa+g+jc091FG/jG9Dkai8fbDbW8YXj7W3ALaXgXWEBJMrgQxZcJJRjgCvLY72FIIrUBquu3FepiyzMtZ0yaIvi4NmGCsYqIv00NcMvMtD7iwhOCZn10Sku4wvaKJ8YBMRduhqC99fnr/ZDW0/HvTNcL7GKx11GjwtmzkJgwsHFPy3zX+kMdF4m3WgtoeI0GwEsBXXZE2C49yAk3Mb/3puegl3a1PPMvOabTzo7Xm6xpWkI6gISChI7My71H3EuKZWhkb+IubPmMvJJXIdVxHnsHPz2dl/BZXLgpfVdEgQa2qWeXtYI4NNm37pLl3gv92V4kka+Kr4gfdoq8mJ7aqvc9was35baJbHg4+fEVJG2Wj+2AQU+ncx3nAFzgYyMxwo9K8VuC4QdfRF4ImyxTnWkuokEn9H6JRrbkBDKIELj6vzdPmsjOUEQ4nsYX66/zSibFD7UvhQmdXFs8Gp8/Qq6g4M= account: mediocregopher
4 years ago
// Package dehub TODO needs package docs
package dehub
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"dehub.dev/src/dehub.git/fs"
"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/cache"
"gopkg.in/src-d/go-git.v4/plumbing/format/config"
"gopkg.in/src-d/go-git.v4/storage"
"gopkg.in/src-d/go-git.v4/storage/filesystem"
)
const (
// DehubDir defines the name of the directory where all dehub-related files
// are expected to be found within the git repo.
DehubDir = ".dehub"
)
var (
// ConfigPath defines the expected path to the Project'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 openOpts struct {
bare bool
}
// OpenOption is an option which can be passed to the OpenProject function to
// affect the Project's behavior.
type OpenOption func(*openOpts)
// OpenBareRepo returns an OpenOption which, if true is given, causes the
// OpenProject function to expect to open a bare git repo.
func OpenBareRepo(bare bool) OpenOption {
return func(o *openOpts) {
o.bare = bare
}
}
// Project implements accessing and modifying a local dehub project, as well as
// extending the functionality of the underlying git repo in ways which are
// specifically useful for dehub projects.
type Project struct {
// GitRepo is the git repository which houses the project.
GitRepo *git.Repository
// GitDirFS corresponds to the .git directory (or the entire repo directory
// if it's a bare repo)
GitDirFS billy.Filesystem
}
func extractGitDirFS(storer storage.Storer) (billy.Filesystem, error) {
dotGitFSer, ok := storer.(interface{ Filesystem() billy.Filesystem })
if !ok {
return nil, fmt.Errorf("git storage object of type %T does not expose its underlying filesystem",
storer)
}
return dotGitFSer.Filesystem(), nil
}
// OpenProject opens the dehub project in the given directory and returns a
// Project instance for it.
//
// The given path is expected to have a git repo already initialized.
func OpenProject(path string, options ...OpenOption) (*Project, error) {
var opts openOpts
for _, opt := range options {
opt(&opts)
}
proj := Project{}
var err error
openOpts := &git.PlainOpenOptions{
DetectDotGit: !opts.bare,
}
if proj.GitRepo, err = git.PlainOpenWithOptions(path, openOpts); err != nil {
return nil, fmt.Errorf("opening git repo: %w", err)
} else if proj.GitDirFS, err = extractGitDirFS(proj.GitRepo.Storer); err != nil {
return nil, err
}
return &proj, nil
}
type initOpts struct {
bare bool
remote bool
}
// InitOption is an option which can be passed into the Init functions to affect
// their behavior.
type InitOption func(*initOpts)
// InitBareRepo returns an InitOption which, if true is given, causes the Init
// function to initialize the project's git repo without a worktree.
func InitBareRepo(bare bool) InitOption {
return func(o *initOpts) {
o.bare = bare
}
}
// InitRemoteRepo returns an InitOption which, if true is given, causes the Init
// function to initialize the project's git repo with certain git configuration
// options set which make the repo able to be used as a remote repo.
func InitRemoteRepo(remote bool) InitOption {
return func(o *initOpts) {
o.remote = remote
}
}
// InitProject will initialize a new project at the given path. If bare is true
// then the project's git repo will not have a worktree.
func InitProject(path string, options ...InitOption) (*Project, error) {
var opts initOpts
for _, opt := range options {
opt(&opts)
}
var proj Project
var err error
if proj.GitRepo, err = git.PlainInit(path, opts.bare); err != nil {
return nil, fmt.Errorf("initializing git repo: %w", err)
} else if proj.GitDirFS, err = extractGitDirFS(proj.GitRepo.Storer); err != nil {
return nil, err
} else if err = proj.init(opts); err != nil {
return nil, fmt.Errorf("initializing repo with dehub defaults: %w", err)
}
return &proj, nil
}
// InitMemProject initializes an empty project which only exists in memory.
func InitMemProject(options ...InitOption) *Project {
var opts initOpts
for _, opt := range options {
opt(&opts)
}
fs := memfs.New()
dotGitFS, err := fs.Chroot(git.GitDirName)
if err != nil {
panic(err)
}
storage := filesystem.NewStorage(dotGitFS, cache.NewObjectLRUDefault())
var worktree billy.Filesystem
if !opts.bare {
worktree = fs
}
r, err := git.Init(storage, worktree)
if err != nil {
panic(err)
}
proj := &Project{GitRepo: r, GitDirFS: dotGitFS}
if err := proj.init(opts); err != nil {
panic(err)
}
return proj
}
func (proj *Project) initRemotePreReceive(bare bool) error {
if err := proj.GitDirFS.MkdirAll("hooks", 0755); err != nil {
return fmt.Errorf("creating hooks directory: %w", err)
}
preRcvFlags := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
preRcv, err := proj.GitDirFS.OpenFile("hooks/pre-receive", preRcvFlags, 0755)
if err != nil {
return fmt.Errorf("opening hooks/pre-receive file: %w", err)
}
defer preRcv.Close()
var preRcvBody string
if bare {
preRcvBody = "#!/bin/sh\nexec dehub hook -bare -pre-receive\n"
} else {
preRcvBody = "#!/bin/sh\nexec dehub hook -pre-receive\n"
}
if _, err := io.Copy(preRcv, bytes.NewBufferString(preRcvBody)); err != nil {
return fmt.Errorf("writing to hooks/pre-receive: %w", err)
}
return nil
}
func (proj *Project) init(opts initOpts) error {
headRef := plumbing.NewSymbolicReference(plumbing.HEAD, MainRefName)
if err := proj.GitRepo.Storer.SetReference(headRef); err != nil {
return fmt.Errorf("setting HEAD reference to %q: %w", MainRefName, err)
}
if opts.remote {
cfg, err := proj.GitRepo.Config()
if err != nil {
return fmt.Errorf("opening git cfg: %w", err)
}
cfg.Raw = cfg.Raw.AddOption("http", config.NoSubsection, "receivepack", "true")
if err := proj.GitRepo.Storer.SetConfig(cfg); err != nil {
return fmt.Errorf("storing modified git config: %w", err)
}
if err := proj.initRemotePreReceive(opts.bare); err != nil {
return fmt.Errorf("initializing pre-receive hook for remote-enabled repo: %w", err)
}
}
return nil
}
func (proj *Project) billyFilesystem() (billy.Filesystem, error) {
w, err := proj.GitRepo.Worktree()
if err != nil {
return nil, fmt.Errorf("opening git worktree: %w", err)
}
return w.Filesystem, nil
}
var errTraverseRefNoMatch = errors.New("failed to find reference matching given predicate")
// TraverseReferenceChain resolves a chain of references, calling the given
// predicate on each one, and returning the first one for which the predicate
// returns true. This method will return an error if it reaches the end of the
// chain and the predicate still has not returned true.
//
// If a reference name is encountered which does not actually exist, then it is
// assumed to be a hash reference to the zero hash.
func (proj *Project) TraverseReferenceChain(refName plumbing.ReferenceName, pred func(*plumbing.Reference) bool) (*plumbing.Reference, error) {
// TODO infinite loop checking
// TODO check that this (and the methods which use it) are actually useful
for {
ref, err := proj.GitRepo.Storer.Reference(refName)
if errors.Is(err, plumbing.ErrReferenceNotFound) {
ref = plumbing.NewHashReference(refName, plumbing.ZeroHash)
} else if err != nil {
return nil, fmt.Errorf("resolving reference %q: %w", refName, err)
}
if pred(ref) {
return ref, nil
} else if ref.Type() != plumbing.SymbolicReference {
return nil, errTraverseRefNoMatch
}
refName = ref.Target()
}
}
// ErrNoBranchReference is returned from ReferenceToBranchName if no reference
// in the reference chain is for a branch.
var ErrNoBranchReference = errors.New("no branch reference found")
// ReferenceToBranchName traverses a chain of references looking for the first
// branch reference, and returns that name, or returns ErrNoBranchReference if
// no branch reference is part of the chain.
func (proj *Project) ReferenceToBranchName(refName plumbing.ReferenceName) (plumbing.ReferenceName, error) {
// first check if the given refName is a branch, if so just return that.
if refName.IsBranch() {
return refName, nil
}
ref, err := proj.TraverseReferenceChain(refName, func(ref *plumbing.Reference) bool {
return ref.Target().IsBranch()
})
if errors.Is(err, errTraverseRefNoMatch) {
return "", ErrNoBranchReference
} else if err != nil {
return "", fmt.Errorf("traversing reference chain: %w", err)
}
return ref.Target(), nil
}
// ReferenceToHash fully resolves a reference to a hash. If a reference cannot
// be resolved then plumbing.ZeroHash is returned.
func (proj *Project) ReferenceToHash(refName plumbing.ReferenceName) (plumbing.Hash, error) {
ref, err := proj.TraverseReferenceChain(refName, func(ref *plumbing.Reference) bool {
return ref.Type() == plumbing.HashReference
})
if errors.Is(err, errTraverseRefNoMatch) {
return plumbing.ZeroHash, errors.New("no hash in reference chain (is this even possible???)")
} else if errors.Is(err, plumbing.ErrReferenceNotFound) {
return plumbing.ZeroHash, nil
} else if err != nil {
return plumbing.ZeroHash, fmt.Errorf("traversing reference chain: %w", err)
}
return ref.Hash(), 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 (proj *Project) headFS() (fs.FS, error) {
head, err := proj.GetHeadCommit()
if errors.Is(err, ErrHeadIsZero) {
bfs, err := proj.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.TreeObject), nil
}