422c444a50
--- type: change description: change pre-receive hook from flag to sub-command fingerprint: AN/k4s8msJaAVVOrm8dQPLpb75lPHy/AFEuyicQfbn1W credentials: - type: pgp_signature pub_key_id: 95C46FA6A41148AC body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl6l8CgACgkQlcRvpqQRSKxA2A//Q8oJFH10/HYp38bG6OUem7nIg7+J0uC6jCpsP3P66jHVQ2+U0tQ1zdrlZLrufVPPTrYuFnY3m2qezl47IOQnMw5qLN0/+8/tv2rWvG+1Ffz/dGhn215r7D59bgihPiyBSEZW6l2uty/LBWHyHHOn4TgGsi49BH+E/CdTS1Wk+6eWcJHBMOjrzdzTq3Im75HU7GRkvVlyuX69dW1tNlItmcEJ0moRCoc5WHUwn64RRjlxQMkMMMNtWL2MdW9/fbVDNQHtS8L91bsUcyp2uAQW0xqVzezen3+LNxMuxpIjD/FRB3NEnkEHspeorIYCTYWxWZaQnSYB26vri/LUSpKDPH6kQ4yBexZDfwU1xI6qJARNPpczUdn0XycddXAHhtlig9Vbx0x8Y7CiuKejhMtkcNK2Zvtt+AYVoF2gCeRNBXbRXRTt3WBNyvju3NeiZD17cIi6XIOIFkpm4hFGrdMEjfeZFsUln3Jt445t1Whg3DIl6rgcS/uVpz01c6OZvH05o0JncuvlMU5akd0i+0j1L7+zH38vj3NC8rLpy4ECnnH72yAnvR2TSB7KVtjVZB3o8VAiQPFAaDn0A8vkrf63/jcyiAFhL/Ee8u38richLfzghjCJJ6JVg61OevlQ6tH+zuzH2+5TleCpjESNZZRu9OclfEFeB/AO5qYY4I38RM4= account: mediocregopher
327 lines
9.8 KiB
Go
327 lines
9.8 KiB
Go
// 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
|
|
}
|