diff --git a/cmd/dehub/cmd_commit.go b/cmd/dehub/cmd_commit.go index 790d6db..38e11bf 100644 --- a/cmd/dehub/cmd_commit.go +++ b/cmd/dehub/cmd_commit.go @@ -102,23 +102,38 @@ func cmdCommit(ctx context.Context, cmd *dcmd.Cmd) { cmd.SubCmd("credential", "Commit credential of a different commit", func(ctx context.Context, cmd *dcmd.Cmd) { flag := cmd.FlagSet() - rev := flag.String("rev", "", "Revision of commit to accredit") + startRev := flag.String("start", "", "Revision of the starting commit to accredit (when accrediting a range of changes)") + endRev := flag.String("end", "HEAD", "Revision of the ending commit to accredit (when accrediting a range of changes)") + rev := flag.String("rev", "", "Revision of commit to accredit (when accrediting a single commit)") cmd.Run(func() (context.Context, error) { - if *rev == "" { - return nil, errors.New("-rev is required") + 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") } - gitCommit, err := repo.GetGitRevision(plumbing.Revision(*rev)) - if err != nil { - return nil, fmt.Errorf("resolving revision %q: %w", *rev, err) + var credCommit dehub.Commit + if *rev != "" { + gitCommit, err := repo.GetGitRevision(plumbing.Revision(*rev)) + if err != nil { + return nil, fmt.Errorf("resolving revision %q: %w", *rev, err) + } else if credCommit, err = repo.NewCommitCredential(gitCommit.Interface.GetHash()); err != nil { + return nil, fmt.Errorf("constructing credential commit: %w", err) + } + } else { + gitCommits, err := repo.GetGitRevisionRange( + plumbing.Revision(*startRev), + plumbing.Revision(*endRev), + ) + if err != nil { + return nil, fmt.Errorf("resolving revisions %q to %q: %w", + *startRev, *endRev, err) + } else if credCommit, err = repo.NewCommitCredentialFromChanges(gitCommits); err != nil { + return nil, fmt.Errorf("constructing credential commit: %w", err) + } } - credCommit, err := repo.NewCommitCredential(gitCommit.Interface.GetHash()) - if err != nil { - return nil, fmt.Errorf("constructing credential commit: %w", err) - } else if err := accreditAndCommit(credCommit); err != nil { + if err := accreditAndCommit(credCommit); err != nil { return nil, err } return nil, nil diff --git a/commit.go b/commit.go index b8dd8eb..4bcd7c1 100644 --- a/commit.go +++ b/commit.go @@ -439,3 +439,46 @@ func (r *Repo) verifyCommit(branch plumbing.ReferenceName, gitCommit GitCommit, return nil } + +type changeRangeInfo struct { + lastChangeCommit GitCommit + authors map[string]struct{} + msg string + startTree, endTree *object.Tree + changeHash []byte +} + +// changeRangeInfo returns various pieces of information about a range of +// commits' changes. +func (r *Repo) changeRangeInfo(commits []GitCommit) (changeRangeInfo, error) { + info := changeRangeInfo{ + authors: map[string]struct{}{}, + } + + var lastChangeCommitOk bool + for _, commit := range commits { + if _, ok := commit.Interface.(*CommitChange); ok { + info.lastChangeCommit = commit + lastChangeCommitOk = true + for _, cred := range commit.Commit.Common.Credentials { + info.authors[cred.AccountID] = struct{}{} + } + } + } + if !lastChangeCommitOk { + return changeRangeInfo{}, errors.New("no change commits found") + } + + // startTree has to be the tree of the parent of the first commit, which + // isn't included in commits. Determine it the hard way. + var err error + if info.startTree, err = r.parentTree(commits[0].GitCommit); err != nil { + return changeRangeInfo{}, fmt.Errorf("getting tree of parent of %q: %w", + commits[0].GitCommit.Hash, err) + } + + info.msg = info.lastChangeCommit.Commit.Change.Message + info.endTree = info.lastChangeCommit.GitTree + info.changeHash = genChangeHash(nil, info.msg, info.startTree, info.endTree) + return info, nil +} diff --git a/commit_change.go b/commit_change.go index 8b005f0..8ff815b 100644 --- a/commit_change.go +++ b/commit_change.go @@ -74,40 +74,17 @@ func (cc CommitChange) GetHash() []byte { // start/end will be different than the one which needs to be accredited in // onto/end. func (r *Repo) CombineCommitChanges(commits []GitCommit, onto plumbing.ReferenceName) (GitCommit, error) { - authorsSet := map[string]struct{}{} - var lastChangeCommit GitCommit - var lastChangeCommitOk bool - for _, commit := range commits { - if _, ok := commit.Interface.(*CommitChange); ok { - lastChangeCommit = commit - lastChangeCommitOk = true - for _, cred := range commit.Commit.Common.Credentials { - authorsSet[cred.AccountID] = struct{}{} - } - } - } - if !lastChangeCommitOk { - return GitCommit{}, errors.New("no change commits in range") + info, err := r.changeRangeInfo(commits) + if err != nil { + return GitCommit{}, err } - authors := make([]string, 0, len(authorsSet)) - for author := range authorsSet { + authors := make([]string, 0, len(info.authors)) + for author := range info.authors { authors = append(authors, author) } sort.Strings(authors) - // startTree has to be the tree of startRev, which isn't included in - // commits. Determine it the hard way. - startTree, err := r.parentTree(commits[0].GitCommit) - if err != nil { - return GitCommit{}, fmt.Errorf("getting tree of parent of %q: %w", - commits[0].GitCommit.Hash, err) - } - - msg := lastChangeCommit.Commit.Change.Message - endTree := lastChangeCommit.GitTree - changeHash := genChangeHash(nil, msg, startTree, endTree) - ontoBranchName, err := r.ReferenceToBranchName(onto) if err != nil { return GitCommit{}, fmt.Errorf("resolving %q into a branch name: %w", onto, err) @@ -120,8 +97,8 @@ func (r *Repo) CombineCommitChanges(commits []GitCommit, onto plumbing.Reference return GitCommit{}, fmt.Errorf("resolving revision %q: %w", onto, err) } ontoTree := ontoCommit.GitTree - ontoEndChangeHash := genChangeHash(nil, msg, ontoTree, endTree) - if !bytes.Equal(ontoEndChangeHash, changeHash) { + ontoEndChangeHash := genChangeHash(nil, info.msg, ontoTree, info.endTree) + if !bytes.Equal(ontoEndChangeHash, info.changeHash) { // TODO figure out what files to show as being the "problem files" in // the error message return GitCommit{}, fmt.Errorf("rebasing onto %q would cause the change hash to change, aborting combine", onto) @@ -129,7 +106,7 @@ func (r *Repo) CombineCommitChanges(commits []GitCommit, onto plumbing.Reference var creds []sigcred.Credential for _, commit := range commits { - if bytes.Equal(commit.Interface.GetHash(), changeHash) { + if bytes.Equal(commit.Interface.GetHash(), info.changeHash) { creds = append(creds, commit.Commit.Common.Credentials...) } } @@ -141,8 +118,8 @@ func (r *Repo) CombineCommitChanges(commits []GitCommit, onto plumbing.Reference commit := Commit{ Change: &CommitChange{ - Message: msg, - ChangeHash: changeHash, + Message: info.msg, + ChangeHash: info.changeHash, }, Common: CommitCommon{Credentials: creds}, } @@ -151,7 +128,7 @@ func (r *Repo) CombineCommitChanges(commits []GitCommit, onto plumbing.Reference Commit: commit, Author: strings.Join(authors, ","), ParentHash: ontoCommit.GitCommit.Hash, - GitTree: endTree, + GitTree: info.endTree, }) if err != nil { return GitCommit{}, fmt.Errorf("storing commit: %w", err) diff --git a/commit_credential.go b/commit_credential.go index ea7acb4..6224e8c 100644 --- a/commit_credential.go +++ b/commit_credential.go @@ -27,6 +27,18 @@ func (r *Repo) NewCommitCredential(hash []byte) (Commit, error) { }, nil } +// NewCommitCredentialFromChanges constructs and returns a Comit populated with +// a CommitCredential for all changes in the given range of GitCommits. The +// message of the last change commit in the range is used when generating the +// hash. +func (r *Repo) NewCommitCredentialFromChanges(commits []GitCommit) (Commit, error) { + info, err := r.changeRangeInfo(commits) + if err != nil { + return Commit{}, err + } + return r.NewCommitCredential(info.changeHash) +} + // MessageHead implements the method for the CommitInterface interface. func (cc CommitCredential) MessageHead(common CommitCommon) (string, error) { hash64 := base64.StdEncoding.EncodeToString(cc.CredentialedHash)