implement amend flag for change and comment commit types

---
type: change
message: |-
  implement amend flag for change and comment commit types

  This involved a custom implementation of soft reset, since go-git's doesn't
  correctly handle the behavior around the initial commit of a repo. There is also
  a small fix to HasStagedChanges, since it wasn't properly handling untracked
  files.
change_hash: AJxjg7EN6l2UBcqGXLAqS4HGCfMgD3xCmQ7uZMSOScBc
credentials:
- type: pgp_signature
  pub_key_id: 95C46FA6A41148AC
  body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl6R9X0ACgkQlcRvpqQRSKwtGA/+Ow53J6A8al29hl4wQwpM5mLFI1krBrYcaZd6p/Sz4fhA2/YFhVAmmFCjFbI3QKQyb7DFijrT+8DSoUpgA83b8KIgHPXvUdBq12AFZeYKAfWB7USHWdmQPI4J7Z4rT7qP1o8T9fnWAAInKJPz3VC/RzEFvUdCFBgHHrYYX0S9twt4joEE7K/VlKJrjMQDhjUM+dFYEzQrcUHW5prSI5iHyJs/F+fpI2J7TSFPSCPCzPpAUyJ3Gt1aml9wBctzYOtuHslo5rw1nv3GweIrFBplekjEugxm0bvg95eVdsex/xltcY9KbKkl9iPVurmSRdkE2HDoQtc1c0Pl6XtssdA4k8rmjtO0jBHhKRA+b6ewVr5xRMDKRu2Jj5iPZlvbSMaldLhs/4QSfvQBvmyeVgX+R3zK7Y7JWdqxyOEgYihAI+/4jYxmHIu4QSTntJQw849do2tLqGgIjRcoQpjG35YUE+BZw1WjUVoT9GfPodPd75b5avwVYNuJ+3tKEPmwwz3HcMnwgPSaUEVIyfBlhdOEesSClIjk6rRJzB6WjpvooE9MJEF8AkaTxMGNcbkg/hSjXgt6vVW71YttdgRPoDuct6yU7aeoLwdDswCuqzEvaEdjB8elGcmwYlejnFt6xmLp0cUsPyRLSHQhpDFsdqJklmEXKVUFAgP6UEzxTxmGevY=
  account: mediocregopher
This commit is contained in:
mediocregopher 2020-04-11 10:51:09 -06:00
parent e78efcce74
commit 03459d4b20
6 changed files with 96 additions and 14 deletions

View File

@ -10,7 +10,6 @@ Must be able to feel good about showing the project publicly, as well as be able
to accept help from people asking to help. to accept help from people asking to help.
* Fast-forward perms on branches (so they can be deleted) * Fast-forward perms on branches (so they can be deleted)
* Ammending commits.
* Figure out commit range syntax, use that everywhere. * Figure out commit range syntax, use that everywhere.
* Ability to specify a pgp key manually, even if it's not in the project. * Ability to specify a pgp key manually, even if it's not in the project.
* Ability to require _any_ signature on a commit, even if it's not in the * Ability to require _any_ signature on a commit, even if it's not in the
@ -65,6 +64,9 @@ to accept help from people asking to help.
These tasks aren't necessarily scheduled for any particular milestone, but they These tasks aren't necessarily scheduled for any particular milestone, but they
are things that could use doing anyway. are things that could use doing anyway.
* Config validation. Every interface used by the config should have a
`Validate() error` method, and Config itself should as well.
* Maybe coalesce the `accessctl`, `fs`, and `sigcred` packages back into the * Maybe coalesce the `accessctl`, `fs`, and `sigcred` packages back into the
root "dehub" package. root "dehub" package.

View File

@ -78,14 +78,24 @@ func cmdCommit(ctx context.Context, cmd *dcmd.Cmd) {
func(ctx context.Context, cmd *dcmd.Cmd) { func(ctx context.Context, cmd *dcmd.Cmd) {
flag := cmd.FlagSet() flag := cmd.FlagSet()
msg := flag.String("msg", "", "Commit message") msg := flag.String("msg", "", "Commit message")
amend := flag.Bool("amend", false, "Add changes to HEAD commit, amend its message, and re-accredit it")
cmd.Run(func() (context.Context, error) { cmd.Run(func() (context.Context, error) {
if !hasStaged { if !hasStaged && !*amend {
return nil, errors.New("no changes have been staged for commit") return nil, errors.New("no changes have been staged for commit")
} }
var prevMsg string
if *amend {
oldHead, err := repo.softReset("change")
if err != nil {
return nil, err
}
prevMsg = oldHead.Commit.Change.Message
}
if *msg == "" { if *msg == "" {
var err error var err error
if *msg, err = tmpFileMsg(); err != nil { if *msg, err = tmpFileMsg(defaultCommitFileMsgTpl, prevMsg); err != nil {
return nil, fmt.Errorf("error collecting commit message from user: %w", err) return nil, fmt.Errorf("error collecting commit message from user: %w", err)
} else if *msg == "" { } else if *msg == "" {
@ -115,7 +125,7 @@ func cmdCommit(ctx context.Context, cmd *dcmd.Cmd) {
if *rev == "" && *startRev == "" { if *rev == "" && *startRev == "" {
return nil, errors.New("-rev or -start is required") return nil, errors.New("-rev or -start is required")
} else if hasStaged { } else if hasStaged {
return nil, errors.New("credential commit cannot have any files changed") return nil, errors.New("credential commit cannot have staged changes")
} }
var credCommit dehub.Commit var credCommit dehub.Commit
@ -154,10 +164,24 @@ func cmdCommit(ctx context.Context, cmd *dcmd.Cmd) {
func(ctx context.Context, cmd *dcmd.Cmd) { func(ctx context.Context, cmd *dcmd.Cmd) {
flag := cmd.FlagSet() flag := cmd.FlagSet()
msg := flag.String("msg", "", "Comment message") msg := flag.String("msg", "", "Comment message")
amend := flag.Bool("amend", false, "Amend the comment message currently in HEAD")
cmd.Run(func() (context.Context, error) { cmd.Run(func() (context.Context, error) {
if hasStaged {
return nil, errors.New("comment commit cannot have staged changes")
}
var prevMsg string
if *amend {
oldHead, err := repo.softReset("comment")
if err != nil {
return nil, err
}
prevMsg = oldHead.Commit.Comment.Message
}
if *msg == "" { if *msg == "" {
var err error var err error
if *msg, err = tmpFileMsg(); err != nil { if *msg, err = tmpFileMsg(defaultCommitFileMsgTpl, prevMsg); err != nil {
return nil, fmt.Errorf("collecting comment message from user: %w", err) return nil, fmt.Errorf("collecting comment message from user: %w", err)
} else if *msg == "" { } else if *msg == "" {

View File

@ -1,11 +1,13 @@
package main package main
import ( import (
"errors"
"flag" "flag"
"fmt" "fmt"
"os" "os"
"dehub.dev/src/dehub.git" "dehub.dev/src/dehub.git"
"gopkg.in/src-d/go-git.v4/plumbing"
) )
type repo struct { type repo struct {
@ -26,3 +28,50 @@ func (r *repo) openRepo() error {
} }
return nil return nil
} }
// softReset resets to HEAD^ (or to an orphaned index, if HEAD has no parents),
// returning the old HEAD.
func (r *repo) softReset(expType string) (dehub.GitCommit, error) {
head, err := r.GetGitHead()
if err != nil {
return head, fmt.Errorf("getting HEAD commit: %w", err)
} else if typ, err := head.Commit.Type(); err != nil {
return head, fmt.Errorf("determining commit type of HEAD:% w", err)
} else if expType != "" && typ != expType {
return head, fmt.Errorf("expected HEAD to be a %q commit, but found %q",
expType, typ)
}
branchName, branchErr := r.ReferenceToBranchName(plumbing.HEAD)
numParents := head.GitCommit.NumParents()
if numParents > 1 {
return head, errors.New("cannot reset to parent of a commit with multiple parents")
} else if numParents == 0 {
// if there are no parents then HEAD is the only commit in the branch.
// Don't handle ErrNoBranchReference because there's not really anything
// which can be done for that; we can't set head to "no commit".
// Otherwise, just remove the branch reference, HEAD will still point to
// it and all of HEAD's changes will be in the index.
if branchErr != nil {
return head, branchErr
} else if err := r.GitRepo.Storer.RemoveReference(branchName); err != nil {
return head, fmt.Errorf("removing reference %q: %w", branchName, err)
}
return head, nil
}
refName := branchName
if errors.Is(branchErr, dehub.ErrNoBranchReference) {
refName = plumbing.HEAD
} else if err != nil {
return head, fmt.Errorf("resolving HEAD: %w", err)
}
parentHash := head.GitCommit.ParentHashes[0]
newHeadRef := plumbing.NewHashReference(refName, parentHash)
if err := r.GitRepo.Storer.SetReference(newHeadRef); err != nil {
return head, fmt.Errorf("storing reference %q: %w", newHeadRef, err)
}
return head, nil
}

View File

@ -12,7 +12,12 @@ import (
"strings" "strings"
) )
func tmpFileMsg() (string, error) { const defaultCommitFileMsgTpl = `%s
# Please enter the commit message for your commit. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.`
func tmpFileMsg(tpl string, args ...interface{}) (string, error) {
editor := os.Getenv("EDITOR") editor := os.Getenv("EDITOR")
if editor == "" { if editor == "" {
return "", errors.New("EDITOR not set, please set it or use -msg in order to create your commit message") return "", errors.New("EDITOR not set, please set it or use -msg in order to create your commit message")
@ -27,10 +32,7 @@ func tmpFileMsg() (string, error) {
tmpfName := tmpf.Name() tmpfName := tmpf.Name()
defer os.Remove(tmpfName) defer os.Remove(tmpfName)
tmpBody := bytes.NewBufferString(` tmpBody := bytes.NewBufferString(fmt.Sprintf(tpl, args...))
# Please enter the commit message for your commit. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.`)
_, err = io.Copy(tmpf, tmpBody) _, err = io.Copy(tmpf, tmpBody)
tmpf.Close() tmpf.Close()

View File

@ -281,7 +281,8 @@ func (r *Repo) HasStagedChanges() (bool, error) {
var any bool var any bool
for _, fileStatus := range status { for _, fileStatus := range status {
if fileStatus.Staging != git.Unmodified { if fileStatus.Staging != git.Unmodified &&
fileStatus.Staging != git.Untracked {
any = true any = true
break break
} }

10
repo.go
View File

@ -267,9 +267,13 @@ func (r *Repo) TraverseReferenceChain(refName plumbing.ReferenceName, pred func(
} }
} }
// ErrNoBranchReference is returned from ReferenceToBranchName if no reference
// in the reference chain is for a branch.
var ErrNoBranchReference = errors.New("no branch reference found")
// ReferenceToBranchName traverses a chain of references looking for the first // ReferenceToBranchName traverses a chain of references looking for the first
// branch reference, and returns that name, or returns an error if no branch // branch reference, and returns that name, or returns ErrNoBranchReference if
// reference is part of the chain. // no branch reference is part of the chain.
func (r *Repo) ReferenceToBranchName(refName plumbing.ReferenceName) (plumbing.ReferenceName, error) { func (r *Repo) ReferenceToBranchName(refName plumbing.ReferenceName) (plumbing.ReferenceName, error) {
// first check if the given refName is a branch, if so just return that. // first check if the given refName is a branch, if so just return that.
if refName.IsBranch() { if refName.IsBranch() {
@ -280,7 +284,7 @@ func (r *Repo) ReferenceToBranchName(refName plumbing.ReferenceName) (plumbing.R
return ref.Target().IsBranch() return ref.Target().IsBranch()
}) })
if errors.Is(err, errTraverseRefNoMatch) { if errors.Is(err, errTraverseRefNoMatch) {
return "", errors.New("no branch in reference chain") return "", ErrNoBranchReference
} else if err != nil { } else if err != nil {
return "", fmt.Errorf("traversing reference chain: %w", err) return "", fmt.Errorf("traversing reference chain: %w", err)
} }