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
This commit is contained in:
parent
aff3daab19
commit
a580018e1e
@ -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.
|
||||
|
85
cmd/dehub/cmd_commit.go
Normal file
85
cmd/dehub/cmd_commit.go
Normal file
@ -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
|
||||
})
|
||||
}
|
103
cmd/dehub/cmd_hook.go
Normal file
103
cmd/dehub/cmd_hook.go
Normal file
@ -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
|
||||
}
|
||||
})
|
||||
}
|
40
cmd/dehub/cmd_verify.go
Normal file
40
cmd/dehub/cmd_verify.go
Normal file
@ -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
|
||||
})
|
||||
}
|
170
cmd/dehub/dcmd/dcmd.go
Normal file
170
cmd/dehub/dcmd/dcmd.go
Normal file
@ -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
|
||||
|
||||
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)
|
||||
}
|
||||
type cmdCtxKey int
|
||||
|
||||
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)
|
||||
const (
|
||||
cmdCtxKeyRepo cmdCtxKey = iota
|
||||
)
|
||||
|
||||
func ctxRepo(ctx context.Context) *dehub.Repo {
|
||||
repo, ok := ctx.Value(cmdCtxKeyRepo).(*dehub.Repo)
|
||||
if !ok {
|
||||
panic("repo not initialized on the command context")
|
||||
}
|
||||
|
||||
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(*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 := "<command>"
|
||||
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("")
|
||||
}
|
||||
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)
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
70
cmd/dehub/tmp_file.go
Normal file
70
cmd/dehub/tmp_file.go
Normal file
@ -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…
Reference in New Issue
Block a user