package dehub import ( "bytes" "errors" "fmt" "sort" "strings" "dehub.dev/src/dehub.git/fs" "dehub.dev/src/dehub.git/sigcred" "dehub.dev/src/dehub.git/yamlutil" "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 } changedFiles, err := ChangedFilesBetweenTrees(headTree, stagedTree) if err != nil { return Commit{}, fmt.Errorf("calculating diff between HEAD and staged changes: %w", err) } cc := CommitChange{Message: msg} if cc.ChangeHash, err = cc.ExpectedHash(changedFiles); 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 } // ExpectedHash implements the method for the CommitInterface interface. func (cc CommitChange) ExpectedHash(changedFiles []ChangedFile) ([]byte, error) { return genChangeHash(nil, cc.Message, changedFiles), nil } // StoredHash implements the method for the CommitInterface interface. func (cc CommitChange) StoredHash() []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(commits []GitCommit, onto plumbing.ReferenceName) (GitCommit, error) { info, err := r.changeRangeInfo(commits) if err != nil { return GitCommit{}, err } authors := make([]string, 0, len(info.authors)) for author := range info.authors { authors = append(authors, author) } sort.Strings(authors) 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) } ontoEndChangedFiles, err := ChangedFilesBetweenTrees(ontoCommit.GitTree, info.endTree) if err != nil { return GitCommit{}, fmt.Errorf("calculating file changes between %q and %q: %w", ontoCommit.GitCommit.Hash, commits[len(commits)-1].GitCommit.Hash, err) } ontoEndChangeHash := genChangeHash(nil, info.msg, ontoEndChangedFiles) 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("combining onto %q would cause the change hash to change, aborting combine", onto.Short()) } var creds []sigcred.Credential for _, commit := range commits { if bytes.Equal(commit.Interface.StoredHash(), info.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: info.msg, ChangeHash: info.changeHash, }, Common: CommitCommon{Credentials: creds}, } gitCommit, err := r.CommitBare(CommitBareParams{ Commit: commit, Author: strings.Join(authors, ","), ParentHash: ontoCommit.GitCommit.Hash, GitTree: info.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 }