diff --git a/cmd/dehub/main.go b/cmd/dehub/main.go index 431fc0a..cc56af3 100644 --- a/cmd/dehub/main.go +++ b/cmd/dehub/main.go @@ -1,39 +1,57 @@ package main import ( + "bufio" "dehub" "errors" "flag" "fmt" + "io" "os" "strings" "gopkg.in/src-d/go-git.v4/plumbing" ) +func exitErr(err error) { + fmt.Fprintf(os.Stderr, "exiting: %v\n", err) + os.Stderr.Sync() + os.Stdout.Sync() + os.Exit(1) +} + type subCmdCtx struct { - repo *dehub.Repo + repo func() *dehub.Repo + flag *flag.FlagSet args []string } -var subCmds = []struct { +func (sctx subCmdCtx) flagParse() { + if err := sctx.flag.Parse(sctx.args); err != nil { + exitErr(err) + } +} + +type subCmd struct { name, descr string body func(sctx subCmdCtx) error -}{ +} + +var subCmds = []subCmd{ { name: "commit", descr: "commits staged changes to the head of the current branch", body: func(sctx subCmdCtx) error { - flag := flag.NewFlagSet("commit", flag.ExitOnError) - msg := flag.String("msg", "", "Commit message to use") - accountID := flag.String("account-id", "", "Account to sign commit as") - flag.Parse(sctx.args) + msg := sctx.flag.String("msg", "", "Commit message to use") + accountID := sctx.flag.String("account-id", "", "Account to sign commit as") + sctx.flagParse() if *msg == "" || *accountID == "" { + flag.PrintDefaults() return errors.New("-msg and -account-id are both required") } - cfg, err := sctx.repo.LoadConfig() + cfg, err := sctx.repo().LoadConfig() if err != nil { return err } @@ -58,7 +76,7 @@ var subCmds = []struct { return fmt.Errorf("could not cast %+v to SignifierInterface: %w", sig, err) } - _, hash, err := sctx.repo.CommitMaster(*msg, *accountID, sigInt) + _, hash, err := sctx.repo().CommitMaster(*msg, *accountID, sigInt) if err != nil { return err } @@ -70,16 +88,15 @@ var subCmds = []struct { name: "verify", descr: "verifies one or more commits as having the proper credentials", body: func(sctx subCmdCtx) error { - flag := flag.NewFlagSet("verify", flag.ExitOnError) - rev := flag.String("rev", "HEAD", "Revision of commit to verify") - flag.Parse(sctx.args) + rev := sctx.flag.String("rev", "HEAD", "Revision of commit to verify") + sctx.flagParse() - h, err := sctx.repo.GitRepo.ResolveRevision(plumbing.Revision(*rev)) + h, err := sctx.repo().GitRepo.ResolveRevision(plumbing.Revision(*rev)) if err != nil { return fmt.Errorf("could not resolve revision %q: %w", *rev, err) } - if err := sctx.repo.VerifyMasterCommit(*h); err != nil { + if err := sctx.repo().VerifyMasterCommit(*h); err != nil { return fmt.Errorf("could not verify commit at %q (%s): %w", *rev, *h, err) } @@ -87,50 +104,173 @@ var subCmds = []struct { return nil }, }, -} + { + name: "hook", + descr: "use dehub as a git hook", + body: func(sctx subCmdCtx) error { + preRcv := sctx.flag.Bool("pre-receive", false, "Use dehub as a server-side pre-receive hook") + sctx.flagParse() -func printHelp() { - fmt.Printf("USAGE: %s [-h]\n\n", os.Args[0]) - fmt.Println("COMMANDS") - for _, subCmd := range subCmds { - fmt.Printf("\t%s : %s\n", subCmd.name, subCmd.descr) - } -} + if !*preRcv { + flag.PrintDefaults() + return errors.New("must set the hook type") + } -func exitErr(err error) { - fmt.Fprintf(os.Stderr, "exiting: %v\n", err) - os.Stderr.Sync() - os.Stdout.Sync() - os.Exit(1) + br := bufio.NewReader(os.Stdin) + for { + line, err := br.ReadString('\n') + if errors.Is(err, io.EOF) { + return nil + } else if err != nil { + return fmt.Errorf("error reading next line from stdin: %w", err) + } + + lineParts := strings.Fields(line) + if len(lineParts) < 3 { + return fmt.Errorf("malformed pre-receive hook stdin line %q", line) + } + + if plumbing.ReferenceName(lineParts[2]) != plumbing.Master { + return fmt.Errorf("only commits to the master branch are allowed at the moment (tried to push to %q)", lineParts[2]) + } + + // the zeroRevision gets sent on the very first push + const zeroRevision plumbing.Revision = "0000000000000000000000000000000000000000" + + fromRev := plumbing.Revision(lineParts[0]) + var fromHash *plumbing.Hash + if fromRev != zeroRevision { + fromHash, err = sctx.repo().GitRepo.ResolveRevision(fromRev) + if err != nil { + return fmt.Errorf("unable to resolve revision %q: %w", fromRev, err) + } + } + + toRev := plumbing.Revision(lineParts[1]) + toHash, err := sctx.repo().GitRepo.ResolveRevision(toRev) + if err != nil { + return fmt.Errorf("unable to resolve revision %q: %w", toRev, err) + } + + toCommit, err := sctx.repo().GitRepo.CommitObject(*toHash) + if err != nil { + return fmt.Errorf("unable to find commit %q: %w", *toHash, err) + } + + var hashesToCheck []plumbing.Hash + var found bool + for currCommit := toCommit; ; { + hashesToCheck = append(hashesToCheck, currCommit.Hash) + if currCommit.NumParents() == 0 { + break + } else if currCommit.NumParents() > 1 { + return fmt.Errorf("commit %q has more than one parent: %+v", + currCommit.Hash, currCommit.ParentHashes) + } + + parentCommit, err := currCommit.Parent(0) + if err != nil { + return fmt.Errorf("unable to get parent of commit %q: %w", currCommit.Hash, err) + } else if fromHash != nil && parentCommit.Hash == *fromHash { + found = true + break + } + currCommit = parentCommit + } + if !found && fromHash != nil { + return fmt.Errorf("unable to find commit %q as an ancestor of %q", *fromHash, *toHash) + } + + for i := len(hashesToCheck) - 1; i >= 0; i-- { + hash := hashesToCheck[i] + fmt.Printf("Verifying master commit %q\n", hash) + if err := sctx.repo().VerifyMasterCommit(hash); err != nil { + return fmt.Errorf("could not verify master commit %q", hash) + } + } + fmt.Println("All pushed commits have been verified, well done.") + return nil + } + }, + }, } func main() { - if len(os.Args) < 2 { - printHelp() + var pickedSubCmd bool + var subCmd subCmd + flagSet := flag.NewFlagSet("dehub", flag.ContinueOnError) + flagSet.Usage = func() { + cmd := "" + if pickedSubCmd { + cmd = subCmd.name + } + fmt.Printf("USAGE: %s [global flags] %s [command flags]\n\n", os.Args[0], cmd) + + if !pickedSubCmd { + fmt.Println("COMMANDS") + for _, subCmd := range subCmds { + fmt.Printf("\t%s : %s\n", subCmd.name, subCmd.descr) + } + fmt.Println("") + } + + fmt.Println("GLOBAL FLAGS") + flagSet.PrintDefaults() + fmt.Println("") + } + + bare := flagSet.Bool("bare", false, "If set then dehub will expect to be working with a bare repo") + if err := flagSet.Parse(os.Args[1:]); err != nil { + exitErr(err) return } - subCmdName := strings.ToLower(os.Args[1]) - for _, subCmd := range subCmds { - if subCmd.name != subCmdName { - continue - } + args := flagSet.Args() + if len(args) < 1 { + flagSet.Usage() + exitErr(errors.New("no command selected")) + } - r, err := dehub.OpenRepo(".") - if err != nil { - exitErr(err) + subCmdName, args := strings.ToLower(args[0]), args[1:] + for _, subCmd = range subCmds { + if pickedSubCmd = subCmd.name == subCmdName; pickedSubCmd { + break } + } - err = subCmd.body(subCmdCtx{ - repo: r, - args: os.Args[2:], - }) - if err != nil { + if !pickedSubCmd { + flagSet.Usage() + exitErr(fmt.Errorf("unknown command %q", subCmdName)) + } + + subFlagSet := flag.NewFlagSet(subCmd.name, flag.ExitOnError) + subFlagSet.Usage = func() { + flagSet.Usage() + fmt.Println("COMMAND FLAGS") + subFlagSet.PrintDefaults() + fmt.Println("") + } + + var r *dehub.Repo + repoFn := func() *dehub.Repo { + if r != nil { + return r + } + var err error + if r, err = dehub.OpenRepo(".", dehub.OpenBare(*bare)); err != nil { + wd, _ := os.Getwd() + err = fmt.Errorf("failed to OpenRepo at %q: %w", wd, err) exitErr(err) } - return + return r } - fmt.Printf("unknown command %q\n\n", subCmdName) - printHelp() + err := subCmd.body(subCmdCtx{ + repo: repoFn, + flag: subFlagSet, + args: args, + }) + if err != nil { + exitErr(err) + } } diff --git a/repo.go b/repo.go index 67c6d4d..8fe03b3 100644 --- a/repo.go +++ b/repo.go @@ -26,6 +26,22 @@ var ( ConfigPath = filepath.Join(DehubDir, "config.yml") ) +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 @@ -36,11 +52,16 @@ type Repo struct { // // The given path is expected to have a git repo and .dehub folder already // initialized. -func OpenRepo(path string) (*Repo, error) { +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: true, + DetectDotGit: !opts.bare, } if r.GitRepo, err = git.PlainOpenWithOptions(path, openOpts); err != nil { return nil, fmt.Errorf("could not open git repo: %w", err)