diff --git a/ROADMAP.md b/ROADMAP.md index bc39a47..007cc4b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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. * Fast-forward perms on branches (so they can be deleted) -* Ammending commits. * 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 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 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 root "dehub" package. diff --git a/cmd/dehub/cmd_commit.go b/cmd/dehub/cmd_commit.go index 53c9aef..0e8f65b 100644 --- a/cmd/dehub/cmd_commit.go +++ b/cmd/dehub/cmd_commit.go @@ -78,14 +78,24 @@ func cmdCommit(ctx context.Context, cmd *dcmd.Cmd) { func(ctx context.Context, cmd *dcmd.Cmd) { flag := cmd.FlagSet() 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) { - if !hasStaged { + if !hasStaged && !*amend { 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 == "" { 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) } else if *msg == "" { @@ -115,7 +125,7 @@ func cmdCommit(ctx context.Context, cmd *dcmd.Cmd) { if *rev == "" && *startRev == "" { return nil, errors.New("-rev or -start is required") } 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 @@ -154,10 +164,24 @@ func cmdCommit(ctx context.Context, cmd *dcmd.Cmd) { func(ctx context.Context, cmd *dcmd.Cmd) { flag := cmd.FlagSet() msg := flag.String("msg", "", "Comment message") + amend := flag.Bool("amend", false, "Amend the comment message currently in HEAD") 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 == "" { 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) } else if *msg == "" { diff --git a/cmd/dehub/cmd_util.go b/cmd/dehub/cmd_util.go index 60dc18f..7662f86 100644 --- a/cmd/dehub/cmd_util.go +++ b/cmd/dehub/cmd_util.go @@ -1,11 +1,13 @@ package main import ( + "errors" "flag" "fmt" "os" "dehub.dev/src/dehub.git" + "gopkg.in/src-d/go-git.v4/plumbing" ) type repo struct { @@ -26,3 +28,50 @@ func (r *repo) openRepo() error { } 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 +} diff --git a/cmd/dehub/tmp_file.go b/cmd/dehub/tmp_file.go index 51437e5..da251c7 100644 --- a/cmd/dehub/tmp_file.go +++ b/cmd/dehub/tmp_file.go @@ -12,7 +12,12 @@ import ( "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") if editor == "" { 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() defer os.Remove(tmpfName) - tmpBody := bytes.NewBufferString(` - -# Please enter the commit message for your commit. Lines starting -# with '#' will be ignored, and an empty message aborts the commit.`) + tmpBody := bytes.NewBufferString(fmt.Sprintf(tpl, args...)) _, err = io.Copy(tmpf, tmpBody) tmpf.Close() diff --git a/commit.go b/commit.go index 238d20f..9c43a0a 100644 --- a/commit.go +++ b/commit.go @@ -281,7 +281,8 @@ func (r *Repo) HasStagedChanges() (bool, error) { var any bool for _, fileStatus := range status { - if fileStatus.Staging != git.Unmodified { + if fileStatus.Staging != git.Unmodified && + fileStatus.Staging != git.Untracked { any = true break } diff --git a/repo.go b/repo.go index 620bc2e..7f5b4ab 100644 --- a/repo.go +++ b/repo.go @@ -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 -// branch reference, and returns that name, or returns an error if no branch -// reference is part of the chain. +// branch reference, and returns that name, or returns ErrNoBranchReference if +// no branch reference is part of the chain. func (r *Repo) ReferenceToBranchName(refName plumbing.ReferenceName) (plumbing.ReferenceName, error) { // first check if the given refName is a branch, if so just return that. if refName.IsBranch() { @@ -280,7 +284,7 @@ func (r *Repo) ReferenceToBranchName(refName plumbing.ReferenceName) (plumbing.R return ref.Target().IsBranch() }) if errors.Is(err, errTraverseRefNoMatch) { - return "", errors.New("no branch in reference chain") + return "", ErrNoBranchReference } else if err != nil { return "", fmt.Errorf("traversing reference chain: %w", err) }