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
main
mediocregopher 4 years ago
parent aff3daab19
commit a580018e1e
  1. 3
      ROADMAP.md
  2. 85
      cmd/dehub/cmd_commit.go
  3. 103
      cmd/dehub/cmd_hook.go
  4. 40
      cmd/dehub/cmd_verify.go
  5. 170
      cmd/dehub/dcmd/dcmd.go
  6. 374
      cmd/dehub/main.go
  7. 70
      cmd/dehub/tmp_file.go

@ -26,7 +26,8 @@ set, only a sequence of milestones and the requirements to hit them.
* Polish commands * Polish commands
- New flag system, some kind of interactivity support (e.g. user doesn't - 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 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, - Review flags, probably make some of them into positional arguments,
document everything better. document everything better.

@ -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
})
}

@ -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
}
})
}

@ -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
})
}

@ -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> [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)
}

@ -1,375 +1,43 @@
package main package main
import ( import (
"bufio" "context"
"bytes"
"dehub" "dehub"
"errors" "dehub/cmd/dehub/dcmd"
"flag"
"fmt" "fmt"
"io"
"io/ioutil"
"os" "os"
"os/exec"
"strings"
"gopkg.in/src-d/go-git.v4/plumbing"
) )
func exitErr(err error) { type cmdCtxKey int
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), "#") { const (
bodyFiltered.WriteString(line) cmdCtxKeyRepo cmdCtxKey = iota
} )
}
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 func ctxRepo(ctx context.Context) *dehub.Repo {
var ok bool repo, ok := ctx.Value(cmdCtxKeyRepo).(*dehub.Repo)
for _, account = range cfg.Accounts {
if account.ID == *accountID {
ok = true
break
}
}
if !ok { if !ok {
return fmt.Errorf("account ID %q not found in config", *accountID) panic("repo not initialized on the command context")
} 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)
} }
return repo
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
}
},
},
} }
func main() { func main() {
var pickedSubCmd bool cmd := dcmd.New()
var subCmd subCmd flag := cmd.FlagSet()
flagSet := flag.NewFlagSet("dehub", flag.ContinueOnError) bare := flag.Bool("bare", false, "If set then dehub will expect to be working with a bare repo")
flagSet.Usage = func() {
cmd := "<command>"
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") cmd.SubCmd("commit", "commits staged changes to the head of the current branch", cmdCommit)
flagSet.PrintDefaults() cmd.SubCmd("verify", "verifies one or more commits as having the proper credentials", cmdVerify)
fmt.Println("") cmd.SubCmd("hook", "use dehub as a git hook", cmdHook)
}
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() cmd.Run(func() (context.Context, error) {
if len(args) < 1 { repo, err := dehub.OpenRepo(".", dehub.OpenBare(*bare))
flagSet.Usage() if err != nil {
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() wd, _ := os.Getwd()
err = fmt.Errorf("failed to OpenRepo at %q: %w", wd, err) return nil, fmt.Errorf("failed to OpenRepo at %q: %w", wd, err)
exitErr(err)
}
return r
} }
err := subCmd.body(subCmdCtx{ return context.WithValue(context.Background(), cmdCtxKeyRepo, repo), nil
repo: repoFn,
flag: subFlagSet,
args: args,
}) })
if err != nil {
exitErr(err)
}
} }

@ -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
}
Loading…
Cancel
Save