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 }