A read-only clone of the dehub project, for until dehub.dev can be brought back online.
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.
 
 
dehub/commit_change.go

163 lines
5.3 KiB

package dehub
import (
"bytes"
"dehub/fs"
"dehub/sigcred"
"dehub/yamlutil"
"errors"
"fmt"
"sort"
"strings"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/object"
)
// CommitChange describes the structure of a change commit message.
type CommitChange struct {
Message string `yaml:"message"`
ChangeHash yamlutil.Blob `yaml:"change_hash"`
}
var _ CommitInterface = CommitChange{}
// NewCommitChange constructs a Commit populated with a CommitChange
// encompassing the currently staged file changes. The Credentials of the
// returned Commit will _not_ be filled in.
func (r *Repo) NewCommitChange(msg string) (Commit, error) {
headTree := new(object.Tree)
if head, err := r.GetGitHead(); err != nil && !errors.Is(err, ErrHeadIsZero) {
return Commit{}, fmt.Errorf("getting HEAD commit: %w", err)
} else if err == nil {
headTree = head.GitTree
}
_, stagedTree, err := fs.FromStagedChangesTree(r.GitRepo)
if err != nil {
return Commit{}, err
}
cc := CommitChange{Message: msg}
if cc.ChangeHash, err = cc.Hash(headTree, stagedTree); err != nil {
return Commit{}, err
}
return Commit{
Change: &cc,
}, nil
}
// MessageHead implements the method for the CommitInterface interface.
func (cc CommitChange) MessageHead(CommitCommon) (string, error) {
return abbrevCommitMessage(cc.Message), nil
}
// Hash implements the method for the CommitInterface interface.
func (cc CommitChange) Hash(parent, this *object.Tree) ([]byte, error) {
return genChangeHash(nil, cc.Message, parent, this), nil
}
// GetHash implements the method for the CommitInterface interface.
func (cc CommitChange) GetHash() []byte {
return cc.ChangeHash
}
// CombineCommitChanges takes all changes in the given range and combines them
// into a single change Commit. The resulting Commit will have the same message
// as the latest change commit in the range, and will contain all Credentials
// for the resulting change hash that it finds in the range as well.
//
// The combined commit is then committed to the repo with the given revision as
// its parent. If the diff between start/end and onto/end is different then this
// will return an error, as the change hash which has been accredited in
// start/end will be different than the one which needs to be accredited in
// onto/end.
func (r *Repo) CombineCommitChanges(startRev, endRev plumbing.Revision, onto plumbing.ReferenceName) (GitCommit, error) {
startEndCommits, err := r.GetGitRevisionRange(startRev, endRev)
if err != nil {
return GitCommit{}, fmt.Errorf("retrieving commits %q to %q: %w", startRev, endRev, err)
}
var lastChangeCommit GitCommit
var lastChangeCommitOk bool
for i := len(startEndCommits) - 1; i >= 0; i-- {
if _, lastChangeCommitOk = startEndCommits[i].Interface.(*CommitChange); lastChangeCommitOk {
lastChangeCommit = startEndCommits[i]
break
}
}
if !lastChangeCommitOk {
return GitCommit{}, fmt.Errorf("no change commits in range %q to %q", startRev, endRev)
}
// startTree has to be the tree of startRev, which isn't included in
// startEndCommits. Determine it the hard way.
startTree, err := r.parentTree(startEndCommits[0].GitCommit)
if err != nil {
return GitCommit{}, fmt.Errorf("getting tree of %q (parent of %q): %w",
startRev, startEndCommits[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)
}
// now determine the change hash from onto->end, to ensure that it remains
// the same as from start->end
ontoCommit, err := r.GetGitRevision(plumbing.Revision(onto))
if err != nil {
return GitCommit{}, fmt.Errorf("resolving revision %q: %w", onto, err)
}
ontoTree := ontoCommit.GitTree
ontoEndChangeHash := genChangeHash(nil, msg, ontoTree, endTree)
if !bytes.Equal(ontoEndChangeHash, 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)
}
var creds []sigcred.Credential
for _, commit := range startEndCommits {
if bytes.Equal(commit.Interface.GetHash(), changeHash) {
creds = append(creds, commit.Commit.Common.Credentials...)
}
}
// this is mostly to make tests easier
sort.Slice(creds, func(i, j int) bool {
return creds[i].AccountID < creds[j].AccountID
})
commit := Commit{
Change: &CommitChange{
Message: msg,
ChangeHash: changeHash,
},
Common: CommitCommon{Credentials: creds},
}
accountID := strings.Join(commit.Common.credAccountIDs(), ",")
gitCommit, err := r.CommitBare(CommitBareParams{
Commit: commit,
AccountID: accountID,
ParentHash: ontoCommit.GitCommit.Hash,
GitTree: endTree,
})
if err != nil {
return GitCommit{}, fmt.Errorf("storing commit: %w", err)
}
// set the onto branch to this new commit
newHeadRef := plumbing.NewHashReference(ontoBranchName, gitCommit.GitCommit.Hash)
if err := r.GitRepo.Storer.SetReference(newHeadRef); err != nil {
return GitCommit{}, fmt.Errorf("setting reference %q to new commit hash %q: %w",
ontoBranchName, gitCommit.GitCommit.Hash, err)
}
return gitCommit, nil
}