--- 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: mediocregophermain
parent
aff3daab19
commit
a580018e1e
@ -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), "#") { |
|
||||||
bodyFiltered.WriteString(line) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return strings.TrimSpace(bodyFiltered.String()), nil |
|
||||||
} |
|
||||||
|
|
||||||
type subCmdCtx struct { |
const ( |
||||||
repo func() *dehub.Repo |
cmdCtxKeyRepo cmdCtxKey = iota |
||||||
flag *flag.FlagSet |
) |
||||||
args []string |
|
||||||
} |
|
||||||
|
|
||||||
func (sctx subCmdCtx) flagParse() { |
func ctxRepo(ctx context.Context) *dehub.Repo { |
||||||
if err := sctx.flag.Parse(sctx.args); err != nil { |
repo, ok := ctx.Value(cmdCtxKeyRepo).(*dehub.Repo) |
||||||
exitErr(err) |
if !ok { |
||||||
|
panic("repo not initialized on the command context") |
||||||
} |
} |
||||||
} |
return repo |
||||||
|
|
||||||
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 |
|
||||||
} |
|
||||||
}, |
|
||||||
}, |
|
||||||
} |
} |
||||||
|
|
||||||
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 { |
cmd.SubCmd("commit", "commits staged changes to the head of the current branch", cmdCommit) |
||||||
fmt.Println("COMMANDS") |
cmd.SubCmd("verify", "verifies one or more commits as having the proper credentials", cmdVerify) |
||||||
for _, subCmd := range subCmds { |
cmd.SubCmd("hook", "use dehub as a git hook", cmdHook) |
||||||
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() |
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…
Reference in new issue