You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
375 lines
9.8 KiB
375 lines
9.8 KiB
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"dehub"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"gopkg.in/src-d/go-git.v4/plumbing"
|
|
)
|
|
|
|
func exitErr(err error) {
|
|
fmt.Fprintf(os.Stderr, "exiting: %v\n", err)
|
|
os.Stderr.Sync()
|
|
os.Stdout.Sync()
|
|
os.Exit(1)
|
|
}
|
|
|
|
func tmpFileMsg() (string, error) {
|
|
editor := os.Getenv("EDITOR")
|
|
if editor == "" {
|
|
return "", errors.New("EDITOR not set, please set it or use -msg in order to create your commit message")
|
|
} else if _, err := os.Stat(editor); err != nil {
|
|
return "", fmt.Errorf("could not stat EDITOR %q: %w", editor, err)
|
|
}
|
|
|
|
tmpf, err := ioutil.TempFile("", "")
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not open temp file: %w", err)
|
|
}
|
|
tmpfName := tmpf.Name()
|
|
defer os.Remove(tmpfName)
|
|
|
|
tmpBody := bytes.NewBufferString(`
|
|
|
|
# Please enter the commit message for your changes. Lines starting
|
|
# with '#' will be ignored, and an empty message aborts the commit.`)
|
|
|
|
_, err = io.Copy(tmpf, tmpBody)
|
|
tmpf.Close()
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not write helper message to temp file %q: %w", tmpfName, err)
|
|
}
|
|
|
|
cmd := exec.Command(editor, tmpfName)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
return "", fmt.Errorf("error running '%s %q': %w", editor, tmpfName, err)
|
|
}
|
|
|
|
body, err := ioutil.ReadFile(tmpfName)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error retrieving message body from %q: %w", tmpfName, err)
|
|
}
|
|
|
|
bodyFiltered := new(bytes.Buffer)
|
|
bodyBR := bufio.NewReader(bytes.NewBuffer(body))
|
|
for {
|
|
line, err := bodyBR.ReadString('\n')
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
} else if err != nil {
|
|
return "", fmt.Errorf("error reading from buffered body: %w", err)
|
|
}
|
|
|
|
if !strings.HasPrefix(strings.TrimSpace(line), "#") {
|
|
bodyFiltered.WriteString(line)
|
|
}
|
|
}
|
|
|
|
return strings.TrimSpace(bodyFiltered.String()), nil
|
|
}
|
|
|
|
type subCmdCtx struct {
|
|
repo func() *dehub.Repo
|
|
flag *flag.FlagSet
|
|
args []string
|
|
}
|
|
|
|
func (sctx subCmdCtx) flagParse() {
|
|
if err := sctx.flag.Parse(sctx.args); err != nil {
|
|
exitErr(err)
|
|
}
|
|
}
|
|
|
|
type subCmd struct {
|
|
name, descr string
|
|
body func(sctx subCmdCtx) error
|
|
}
|
|
|
|
var subCmds = []subCmd{
|
|
{
|
|
name: "commit",
|
|
descr: "commits staged changes to the head of the current branch",
|
|
body: func(sctx subCmdCtx) error {
|
|
msg := sctx.flag.String("msg", "", "Commit message")
|
|
accountID := sctx.flag.String("account-id", "", "Account to sign commit as")
|
|
sctx.flagParse()
|
|
|
|
// Don't bother checking any of the parameters, especially commit
|
|
// message, if there's no staged changes,
|
|
hasStaged, err := sctx.repo().HasStagedChanges()
|
|
if err != nil {
|
|
return fmt.Errorf("error determining if any changes have been staged: %w", err)
|
|
} else if !hasStaged {
|
|
return errors.New("no changes have been staged for commit")
|
|
}
|
|
|
|
if *accountID == "" {
|
|
flag.PrintDefaults()
|
|
return errors.New("-account-id is required")
|
|
}
|
|
|
|
if *msg == "" {
|
|
var err error
|
|
if *msg, err = tmpFileMsg(); err != nil {
|
|
return fmt.Errorf("error collecting commit message from user: %w", err)
|
|
|
|
} else if *msg == "" {
|
|
return errors.New("empty commit message, not doing anything")
|
|
}
|
|
}
|
|
|
|
cfg, err := sctx.repo().LoadConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var account dehub.Account
|
|
var ok bool
|
|
for _, account = range cfg.Accounts {
|
|
if account.ID == *accountID {
|
|
ok = true
|
|
break
|
|
}
|
|
}
|
|
if !ok {
|
|
return fmt.Errorf("account ID %q not found in config", *accountID)
|
|
} else if l := len(account.Signifiers); l == 0 || l > 1 {
|
|
return fmt.Errorf("account %q has %d signifiers, only one is supported right now", *accountID, l)
|
|
}
|
|
|
|
sig := account.Signifiers[0]
|
|
sigInt, err := sig.Interface()
|
|
if err != nil {
|
|
return fmt.Errorf("could not cast %+v to SignifierInterface: %w", sig, err)
|
|
}
|
|
|
|
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, *accountID, 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() {
|
|
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)
|
|
|
|
if !pickedSubCmd {
|
|
fmt.Println("COMMANDS")
|
|
for _, subCmd := range subCmds {
|
|
fmt.Printf("\t%s : %s\n", subCmd.name, subCmd.descr)
|
|
}
|
|
fmt.Println("")
|
|
}
|
|
|
|
fmt.Println("GLOBAL FLAGS")
|
|
flagSet.PrintDefaults()
|
|
fmt.Println("")
|
|
}
|
|
|
|
bare := flagSet.Bool("bare", false, "If set then dehub will expect to be working with a bare repo")
|
|
if err := flagSet.Parse(os.Args[1:]); err != nil {
|
|
exitErr(err)
|
|
return
|
|
}
|
|
|
|
args := flagSet.Args()
|
|
if len(args) < 1 {
|
|
flagSet.Usage()
|
|
exitErr(errors.New("no command selected"))
|
|
}
|
|
|
|
subCmdName, args := strings.ToLower(args[0]), args[1:]
|
|
for _, subCmd = range subCmds {
|
|
if pickedSubCmd = subCmd.name == subCmdName; pickedSubCmd {
|
|
break
|
|
}
|
|
}
|
|
|
|
if !pickedSubCmd {
|
|
flagSet.Usage()
|
|
exitErr(fmt.Errorf("unknown command %q", subCmdName))
|
|
}
|
|
|
|
subFlagSet := flag.NewFlagSet(subCmd.name, flag.ExitOnError)
|
|
subFlagSet.Usage = func() {
|
|
flagSet.Usage()
|
|
fmt.Println("COMMAND FLAGS")
|
|
subFlagSet.PrintDefaults()
|
|
fmt.Println("")
|
|
}
|
|
|
|
var r *dehub.Repo
|
|
repoFn := func() *dehub.Repo {
|
|
if r != nil {
|
|
return r
|
|
}
|
|
var err error
|
|
if r, err = dehub.OpenRepo(".", dehub.OpenBare(*bare)); err != nil {
|
|
wd, _ := os.Getwd()
|
|
err = fmt.Errorf("failed to OpenRepo at %q: %w", wd, err)
|
|
exitErr(err)
|
|
}
|
|
return r
|
|
}
|
|
|
|
err := subCmd.body(subCmdCtx{
|
|
repo: repoFn,
|
|
flag: subFlagSet,
|
|
args: args,
|
|
})
|
|
if err != nil {
|
|
exitErr(err)
|
|
}
|
|
}
|
|
|