From a580018e1e0345d73b42be89c73a102fc3d7ae0f Mon Sep 17 00:00:00 2001 From: mediocregopher <> Date: Fri, 13 Mar 2020 18:06:35 -0600 Subject: [PATCH] Implement dcmd package and use it to simplify cmd/dehub's implementation significantly --- type: change message: Implement dcmd package and use it to simplify cmd/dehub's implementation significantly change_hash: AFNRl+3KopuAHWBIbfr9NzWuNMH+CV5kEMZt+1zEji3q credentials: - type: pgp_signature pub_key_id: 95C46FA6A41148AC body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl5sIAUACgkQlcRvpqQRSKxuIRAAnYRA+YmItXFmDKGaja66LxN5lkfUhkPKCE8pJirgI0hGGfJKa72NC6TkqgN4gSILYDG8jGuoa+lJKHJ1+lvfJuel3V84gjPoTeUSZMjGl+YHeNfR89+IkZqUH2vRxag9BgzPwyEN12e7Koc47RKblCuFhqrEWGBXCL4uqbGE2BDsb+v0lO6oL6xtInvzixw4vxdixhkylrWldj1bSxeKW3RnA+BG6kRGWQ0qMsI/x3055eixKIvRXoS/Jpg0KOBlMOcX+KLh0/yJ2OeeqWAW0h5c1elycYFcZ3HblHBbaogYcY5GZxeX0c92sWQR3AlZv3xuJt2QBMAmBva64S6oWIIoh1qK7b1zYK2IyeTG1h10sUaOxBinN2TbAfeuGqbsYwdem2z7tpIiw7c3CAjFSF9zNzXM6rLIgcF8msmPs2ZxwoYZO/PPFaI/8T5SrW7q7MT4ViJm+Yw0DPUWhmXDO0okbz1a7ugqCVUpxsHtQA6LHAaantlTT1bJkFTcbH1r7OkvlvHemiYmdQffG0LROFAQOilfbiscSCKMBuLwlnd9k65Qupiw0QxFQHgG2eorK6IgVFsxe92rsAshvJo9uaWnaUFq62Y+akGkml11rMo1S+jeHxKfpSWdv48qe91szvIq4fDb1kFaRPntQeg/7wrNjQUKiTwz3kwELv13RJo= account: mediocregopher --- ROADMAP.md | 3 +- cmd/dehub/cmd_commit.go | 85 +++++++++ cmd/dehub/cmd_hook.go | 103 +++++++++++ cmd/dehub/cmd_verify.go | 40 +++++ cmd/dehub/dcmd/dcmd.go | 170 ++++++++++++++++++ cmd/dehub/main.go | 376 +++------------------------------------- cmd/dehub/tmp_file.go | 70 ++++++++ 7 files changed, 492 insertions(+), 355 deletions(-) create mode 100644 cmd/dehub/cmd_commit.go create mode 100644 cmd/dehub/cmd_hook.go create mode 100644 cmd/dehub/cmd_verify.go create mode 100644 cmd/dehub/dcmd/dcmd.go create mode 100644 cmd/dehub/tmp_file.go diff --git a/ROADMAP.md b/ROADMAP.md index f82a797..586b960 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -26,7 +26,8 @@ set, only a sequence of milestones and the requirements to hit them. * Polish commands - New flag system, some kind of interactivity support (e.g. user doesn't specify required argument, give them a prompt on the CLI to input it - rather than an error. This can be built into the flag system. + rather than an error). This is partially done, in that a new flag system + has been started. Needs further work. - Review flags, probably make some of them into positional arguments, document everything better. diff --git a/cmd/dehub/cmd_commit.go b/cmd/dehub/cmd_commit.go new file mode 100644 index 0000000..d010bca --- /dev/null +++ b/cmd/dehub/cmd_commit.go @@ -0,0 +1,85 @@ +package main + +import ( + "context" + "dehub" + "errors" + "fmt" + + "dehub/cmd/dehub/dcmd" +) + +func cmdCommit(ctx context.Context, cmd *dcmd.Cmd) { + flag := cmd.FlagSet() + msg := flag.String("msg", "", "Commit message") + accountID := flag.String("account-id", "", "Account to sign commit as") + cmd.Run(func() (context.Context, error) { + repo := ctxRepo(ctx) + + // Don't bother checking any of the parameters, especially commit + // message, if there's no staged changes, + hasStaged, err := repo.HasStagedChanges() + if err != nil { + return nil, fmt.Errorf("error determining if any changes have been staged: %w", err) + } else if !hasStaged { + return nil, errors.New("no changes have been staged for commit") + } + + if *accountID == "" { + return nil, errors.New("-account-id is required") + } + + if *msg == "" { + var err error + if *msg, err = tmpFileMsg(); err != nil { + return nil, fmt.Errorf("error collecting commit message from user: %w", err) + + } else if *msg == "" { + return nil, errors.New("empty commit message, not doing anything") + } + } + + cfg, err := repo.LoadConfig() + if err != nil { + return nil, err + } + + var account dehub.Account + var ok bool + for _, account = range cfg.Accounts { + if account.ID == *accountID { + ok = true + break + } + } + if !ok { + return nil, fmt.Errorf("account ID %q not found in config", *accountID) + } else if l := len(account.Signifiers); l == 0 || l > 1 { + return nil, fmt.Errorf("account %q has %d signifiers, only one is supported right now", *accountID, l) + } + + sig := account.Signifiers[0] + sigInt, err := sig.Interface(*accountID) + if err != nil { + return nil, fmt.Errorf("could not cast %+v to SignifierInterface: %w", sig, err) + } + + commit, err := repo.NewCommitChange(*msg) + if err != nil { + return nil, fmt.Errorf("could not construct change commit: %w", err) + } + + commit, err = repo.AccreditCommit(commit, sigInt) + if err != nil { + return nil, fmt.Errorf("could not accredit commit: %w", err) + } + + hash, err := repo.Commit(commit, *accountID) + if err != nil { + return nil, fmt.Errorf("could not commit change commit: %w", err) + } + + fmt.Printf("changes committed to HEAD as %s\n", hash) + return nil, nil + }) +} diff --git a/cmd/dehub/cmd_hook.go b/cmd/dehub/cmd_hook.go new file mode 100644 index 0000000..121867d --- /dev/null +++ b/cmd/dehub/cmd_hook.go @@ -0,0 +1,103 @@ +package main + +import ( + "bufio" + "context" + "dehub/cmd/dehub/dcmd" + "errors" + "fmt" + "io" + "os" + "strings" + + "gopkg.in/src-d/go-git.v4/plumbing" +) + +func cmdHook(ctx context.Context, cmd *dcmd.Cmd) { + flag := cmd.FlagSet() + preRcv := flag.Bool("pre-receive", false, "Use dehub as a server-side pre-receive hook") + + cmd.Run(func() (context.Context, error) { + + if !*preRcv { + return nil, errors.New("must set the hook type") + } + + repo := ctxRepo(ctx) + + br := bufio.NewReader(os.Stdin) + for { + line, err := br.ReadString('\n') + if errors.Is(err, io.EOF) { + return nil, nil + } else if err != nil { + return nil, 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 nil, 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 = repo.GitRepo.ResolveRevision(fromRev) + if err != nil { + return nil, fmt.Errorf("unable to resolve revision %q: %w", fromRev, err) + } + } + + toRev := plumbing.Revision(lineParts[1]) + toHash, err := repo.GitRepo.ResolveRevision(toRev) + if err != nil { + return nil, fmt.Errorf("unable to resolve revision %q: %w", toRev, err) + } + + toCommit, err := repo.GitRepo.CommitObject(*toHash) + if err != nil { + return nil, 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 nil, fmt.Errorf("commit %q has more than one parent: %+v", + currCommit.Hash, currCommit.ParentHashes) + } + + parentCommit, err := currCommit.Parent(0) + if err != nil { + return nil, 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 nil, 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 := repo.VerifyCommit(branchName, hash); err != nil { + return nil, fmt.Errorf("could not verify change commit %q: %w", hash, err) + } + } + fmt.Println("All pushed commits have been verified, well done.") + return nil, nil + } + }) +} diff --git a/cmd/dehub/cmd_verify.go b/cmd/dehub/cmd_verify.go new file mode 100644 index 0000000..1ce62d7 --- /dev/null +++ b/cmd/dehub/cmd_verify.go @@ -0,0 +1,40 @@ +package main + +import ( + "context" + "dehub/cmd/dehub/dcmd" + "fmt" + + "gopkg.in/src-d/go-git.v4/plumbing" +) + +func cmdVerify(ctx context.Context, cmd *dcmd.Cmd) { + flag := cmd.FlagSet() + rev := flag.String("rev", "HEAD", "Revision of commit to verify") + branch := flag.String("branch", "", "Branch that the revision is on. If not given then the currently checked out branch is assumed") + + cmd.Run(func() (context.Context, error) { + repo := ctxRepo(ctx) + + h, err := repo.GitRepo.ResolveRevision(plumbing.Revision(*rev)) + if err != nil { + return nil, fmt.Errorf("could not resolve revision %q: %w", *rev, err) + } + + var branchName plumbing.ReferenceName + if *branch == "" { + if branchName, err = repo.CheckedOutBranch(); err != nil { + return nil, fmt.Errorf("could not determined currently checked out branch: %w", err) + } + } else { + branchName = plumbing.NewBranchReferenceName(*branch) + } + + if err := repo.VerifyCommit(branchName, *h); err != nil { + return nil, 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, nil + }) +} diff --git a/cmd/dehub/dcmd/dcmd.go b/cmd/dehub/dcmd/dcmd.go new file mode 100644 index 0000000..24bcf33 --- /dev/null +++ b/cmd/dehub/dcmd/dcmd.go @@ -0,0 +1,170 @@ +// Package dcmd implements command and sub-command parsing and runtime +// management. It wraps the stdlib flag package as well, to incorporate +// configuration into the mix. +package dcmd + +import ( + "context" + "errors" + "flag" + "fmt" + "os" + "sort" + "strings" +) + +func exitErr(err error) { + fmt.Fprintf(os.Stderr, "exiting: %v\n", err) + os.Stderr.Sync() + os.Stdout.Sync() + os.Exit(1) +} + +type subCmd struct { + name, descr string + run func(context.Context, *Cmd) +} + +// Cmd wraps a flag.FlagSet instance to provide extra functionality that dehub +// wants, specifically around sub-command support. +type Cmd struct { + flagSet *flag.FlagSet + binary string // only gets set on root Cmd, during Run + subCmds []subCmd + + // these fields get set by the parent Cmd, if this is a sub-command. + name string + args []string + parent *Cmd +} + +// New initializes and returns an empty Cmd instance. +func New() *Cmd { + return &Cmd{} +} + +func (cmd *Cmd) getFlagSet() *flag.FlagSet { + if cmd.flagSet == nil { + cmd.flagSet = flag.NewFlagSet(cmd.name, flag.ContinueOnError) + } + return cmd.flagSet +} + +// FlagSet returns a flag.Cmd instance on which parameter creation methods can +// be called, e.g. String(...) or Int(...). +func (cmd *Cmd) FlagSet() *flag.FlagSet { + return cmd.getFlagSet() +} + +// SubCmd registers a sub-command of this Cmd. +// +// A new Cmd will be instantiated when this sub-command is picked on the +// command-line during this Cmd's Run method. The Context returned from that Run +// and the new Cmd will be passed into the callback given here. The sub-command +// should then be performed in the same manner as this Cmd is performed +// (including setting flags, adding sub-sub-commands, etc...) +func (cmd *Cmd) SubCmd(name, descr string, run func(context.Context, *Cmd)) { + cmd.subCmds = append(cmd.subCmds, subCmd{ + name: name, + descr: descr, + run: run, + }) + + // it's not the most efficient to do this here, but it is the easiest + sort.Slice(cmd.subCmds, func(i, j int) bool { + return cmd.subCmds[i].name < cmd.subCmds[j].name + }) +} + +func (cmd *Cmd) printUsageHead(subCmdTitle string) { + var title string + if cmd.parent == nil { + title = fmt.Sprintf("USAGE: %s [flags]", cmd.binary) + } else { + title = fmt.Sprintf("%s [%s flags]", cmd.name, cmd.name) + } + + if subCmdTitle != "" { + title += " " + subCmdTitle + } else if len(cmd.subCmds) > 0 { + title += fmt.Sprint(" [sub-command flags]") + } + + if cmd.parent == nil { + fmt.Printf("\n%s\n\n", title) + fmt.Print("### FLAGS ###\n\n") + } else { + cmd.parent.printUsageHead(title) + fmt.Printf("### %s FLAGS ###\n\n", strings.ToUpper(cmd.name)) + } + + cmd.getFlagSet().PrintDefaults() + fmt.Print("\n") +} + +// Run performs the comand. It starts by parsing all flags in the Cmd's FlagSet, +// and possibly exiting with a usage message if appropriate. It will then +// perform the given body callback, and then perform any sub-commands (if +// selected). +// +// The context returned from the callback will be passed into the callback +// (given to SubCmd) of any sub-commands which are run, and so on. +func (cmd *Cmd) Run(body func() (context.Context, error)) { + args := cmd.args + if cmd.parent == nil { + cmd.binary, args = os.Args[0], os.Args[1:] + } + + fs := cmd.getFlagSet() + fs.Usage = func() { + cmd.printUsageHead("") + if len(cmd.subCmds) == 0 { + return + } + + fmt.Printf("### SUB-COMMANDS ###\n\n") + for _, subCmd := range cmd.subCmds { + fmt.Printf("\t%s : %s\n", subCmd.name, subCmd.descr) + } + fmt.Println("") + } + + if err := fs.Parse(args); err != nil { + exitErr(err) + return + } + + ctx, err := body() + if err != nil { + exitErr(err) + } + + // body has run, now do sub-command (if there is one) + subArgs := fs.Args() + if len(cmd.subCmds) == 0 { + return + } else if len(subArgs) == 0 && len(cmd.subCmds) > 0 { + fs.Usage() + exitErr(errors.New("no sub-command selected")) + } + + // now find that sub-command + subCmdName, args := strings.ToLower(args[0]), args[1:] + var subCmd subCmd + var subCmdOk bool + for _, subCmd = range cmd.subCmds { + if subCmdOk = subCmd.name == subCmdName; subCmdOk { + break + } + } + if !subCmdOk { + fs.Usage() + exitErr(fmt.Errorf("unknown command %q", subCmdName)) + } + + subCmdCmd := New() + subCmdCmd.name = subCmd.name + subCmdCmd.args = subArgs[1:] + subCmdCmd.parent = cmd + subCmd.run(ctx, subCmdCmd) +} diff --git a/cmd/dehub/main.go b/cmd/dehub/main.go index 08cf4e0..5f478d7 100644 --- a/cmd/dehub/main.go +++ b/cmd/dehub/main.go @@ -1,375 +1,43 @@ package main import ( - "bufio" - "bytes" + "context" "dehub" - "errors" - "flag" + "dehub/cmd/dehub/dcmd" "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 cmdCtxKey int -type subCmdCtx struct { - repo func() *dehub.Repo - flag *flag.FlagSet - args []string -} +const ( + cmdCtxKeyRepo cmdCtxKey = iota +) -func (sctx subCmdCtx) flagParse() { - if err := sctx.flag.Parse(sctx.args); err != nil { - exitErr(err) +func ctxRepo(ctx context.Context) *dehub.Repo { + repo, ok := ctx.Value(cmdCtxKeyRepo).(*dehub.Repo) + if !ok { + panic("repo not initialized on the command context") } -} - -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(*accountID) - if err != nil { - return fmt.Errorf("could not cast %+v to SignifierInterface: %w", sig, err) - } - - commit, err := sctx.repo().NewCommitChange(*msg) - if err != nil { - return fmt.Errorf("could not construct change commit: %w", err) - } - - commit, err = sctx.repo().AccreditCommit(commit, sigInt) - if err != nil { - return fmt.Errorf("could not accredit commit: %w", err) - } - - hash, err := sctx.repo().Commit(commit, *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().VerifyCommit(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().VerifyCommit(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 - } - }, - }, + return repo } 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) + cmd := dcmd.New() + flag := cmd.FlagSet() + bare := flag.Bool("bare", false, "If set then dehub will expect to be working with a bare repo") - 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 - } + cmd.SubCmd("commit", "commits staged changes to the head of the current branch", cmdCommit) + cmd.SubCmd("verify", "verifies one or more commits as having the proper credentials", cmdVerify) + cmd.SubCmd("hook", "use dehub as a git hook", cmdHook) - 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 { + cmd.Run(func() (context.Context, error) { + repo, err := dehub.OpenRepo(".", dehub.OpenBare(*bare)) + if err != nil { wd, _ := os.Getwd() - err = fmt.Errorf("failed to OpenRepo at %q: %w", wd, err) - exitErr(err) + return nil, fmt.Errorf("failed to OpenRepo at %q: %w", wd, err) } - return r - } - err := subCmd.body(subCmdCtx{ - repo: repoFn, - flag: subFlagSet, - args: args, + return context.WithValue(context.Background(), cmdCtxKeyRepo, repo), nil }) - if err != nil { - exitErr(err) - } } diff --git a/cmd/dehub/tmp_file.go b/cmd/dehub/tmp_file.go new file mode 100644 index 0000000..1fadb7f --- /dev/null +++ b/cmd/dehub/tmp_file.go @@ -0,0 +1,70 @@ +package main + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "strings" +) + +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 +}