package main import ( "bufio" "bytes" "dehub" "errors" "flag" "fmt" "io" "io/ioutil" "os" "os/exec" "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) } func tmpFileMsg() (string, error) { editor := os.Getenv("EDITOR") if editor == "" { return "", errors.New("EDITOR not set, please set it or use -msg in order to create your commit message") } else if _, err := os.Stat(editor); err != nil { return "", fmt.Errorf("could not stat EDITOR %q: %w", editor, err) } tmpf, err := ioutil.TempFile("", "") if err != nil { return "", fmt.Errorf("could not open temp file: %w", err) } tmpfName := tmpf.Name() defer os.Remove(tmpfName) tmpBody := bytes.NewBufferString(` # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit.`) _, err = io.Copy(tmpf, tmpBody) tmpf.Close() if err != nil { return "", fmt.Errorf("could not write helper message to temp file %q: %w", tmpfName, err) } cmd := exec.Command(editor, tmpfName) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return "", fmt.Errorf("error running '%s %q': %w", editor, tmpfName, err) } body, err := ioutil.ReadFile(tmpfName) if err != nil { return "", fmt.Errorf("error retrieving message body from %q: %w", tmpfName, err) } bodyFiltered := new(bytes.Buffer) bodyBR := bufio.NewReader(bytes.NewBuffer(body)) for { line, err := bodyBR.ReadString('\n') if errors.Is(err, io.EOF) { break } else if err != nil { return "", fmt.Errorf("error reading from buffered body: %w", err) } if !strings.HasPrefix(strings.TrimSpace(line), "#") { bodyFiltered.WriteString(line) } } return strings.TrimSpace(bodyFiltered.String()), nil } type subCmdCtx struct { repo func() *dehub.Repo flag *flag.FlagSet args []string } 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 { msg := sctx.flag.String("msg", "", "Commit message") accountID := sctx.flag.String("account-id", "", "Account to sign commit as") sctx.flagParse() // Don't bother checking any of the parameters, especially commit // message, if there's no staged changes, hasStaged, err := sctx.repo().HasStagedChanges() if err != nil { return fmt.Errorf("error determining if any changes have been staged: %w", err) } else if !hasStaged { return errors.New("no changes have been staged for commit") } if *accountID == "" { flag.PrintDefaults() return errors.New("-account-id is required") } if *msg == "" { var err error if *msg, err = tmpFileMsg(); err != nil { return fmt.Errorf("error collecting commit message from user: %w", err) } else if *msg == "" { return errors.New("empty commit message, not doing anything") } } cfg, err := sctx.repo().LoadConfig() if err != nil { return err } var account dehub.Account var ok bool for _, account = range cfg.Accounts { if account.ID == *accountID { ok = true break } } if !ok { return fmt.Errorf("account ID %q not found in config", *accountID) } else if l := len(account.Signifiers); l == 0 || l > 1 { return fmt.Errorf("account %q has %d signifiers, only one is supported right now", *accountID, l) } sig := account.Signifiers[0] sigInt, err := sig.Interface() if err != nil { return fmt.Errorf("could not cast %+v to SignifierInterface: %w", sig, err) } tc, err := sctx.repo().NewChangeCommit(*msg, *accountID, sigInt) if err != nil { return fmt.Errorf("could not construct change commit: %w", err) } hash, err := sctx.repo().Commit(tc, *accountID) if err != nil { return fmt.Errorf("could not commit change commit: %w", err) } fmt.Printf("changes committed to HEAD as %s\n", hash) return nil }, }, { name: "verify", descr: "verifies one or more commits as having the proper credentials", body: func(sctx subCmdCtx) error { rev := sctx.flag.String("rev", "HEAD", "Revision of commit to verify") branch := sctx.flag.String("branch", "", "Branch that the revision is on. If not given then the currently checked out branch is assumed") sctx.flagParse() h, err := sctx.repo().GitRepo.ResolveRevision(plumbing.Revision(*rev)) if err != nil { return fmt.Errorf("could not resolve revision %q: %w", *rev, err) } var branchName plumbing.ReferenceName if *branch == "" { if branchName, err = sctx.repo().CheckedOutBranch(); err != nil { return fmt.Errorf("could not determined currently checked out branch: %w", err) } } else { branchName = plumbing.NewBranchReferenceName(*branch) } if err := sctx.repo().VerifyChangeCommit(branchName, *h); err != nil { return fmt.Errorf("could not verify commit at %q (%s): %w", *rev, *h, err) } fmt.Printf("commit at %q (%s) is good to go!\n", *rev, *h) 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() if !*preRcv { flag.PrintDefaults() return errors.New("must set the hook type") } 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) } fmt.Printf("Processing line %q\n", strings.TrimSpace(line)) lineParts := strings.Fields(line) if len(lineParts) < 3 { return fmt.Errorf("malformed pre-receive hook stdin line %q", line) } branchName := plumbing.ReferenceName(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 change commit %q\n", hash) if err := sctx.repo().VerifyChangeCommit(branchName, hash); err != nil { return fmt.Errorf("could not verify change commit %q: %w", hash, err) } } fmt.Println("All pushed commits have been verified, well done.") return nil } }, }, } func main() { 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 } args := flagSet.Args() if len(args) < 1 { flagSet.Usage() exitErr(errors.New("no command selected")) } subCmdName, args := strings.ToLower(args[0]), args[1:] for _, subCmd = range subCmds { if pickedSubCmd = subCmd.name == subCmdName; pickedSubCmd { break } } 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 r } err := subCmd.body(subCmdCtx{ repo: repoFn, flag: subFlagSet, args: args, }) if err != nil { exitErr(err) } }