package dehub import ( "bytes" "errors" "fmt" "sort" "strings" "dehub.dev/src/dehub.git/fs" "dehub.dev/src/dehub.git/sigcred" "gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/src-d/go-git.v4/plumbing/object" ) // PayloadChange describes the structure of a change payload. type PayloadChange struct { Description string `yaml:"description"` // LegacyMessage is no longer used, use Description instead LegacyMessage string `yaml:"message,omitempty"` } var _ Payload = PayloadChange{} // NewPayloadChange constructs a PayloadUnion populated with a PayloadChange // encompassing the currently staged file changes. The Credentials of the // returned PayloadUnion will _not_ be filled in. func (proj *Project) NewPayloadChange(description string) (PayloadUnion, error) { headTree := new(object.Tree) if head, err := proj.GetHeadCommit(); err != nil && !errors.Is(err, ErrHeadIsZero) { return PayloadUnion{}, fmt.Errorf("getting HEAD commit: %w", err) } else if err == nil { headTree = head.TreeObject } _, stagedTree, err := fs.FromStagedChangesTree(proj.GitRepo) if err != nil { return PayloadUnion{}, err } changedFiles, err := ChangedFilesBetweenTrees(headTree, stagedTree) if err != nil { return PayloadUnion{}, fmt.Errorf("calculating diff between HEAD and staged changes: %w", err) } payCh := PayloadChange{Description: description} fingerprint, err := payCh.Fingerprint(changedFiles) if err != nil { return PayloadUnion{}, err } return PayloadUnion{ Change: &payCh, Common: PayloadCommon{Fingerprint: fingerprint}, }, nil } // MessageHead implements the method for the Payload interface. func (payCh PayloadChange) MessageHead(PayloadCommon) string { return payCh.Description } // Fingerprint implements the method for the Payload interface. func (payCh PayloadChange) Fingerprint(changedFiles []ChangedFile) ([]byte, error) { return genChangeFingerprint(nil, payCh.Description, changedFiles), nil } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (payCh *PayloadChange) UnmarshalYAML(unmarshal func(interface{}) error) error { var wrap struct { Inner PayloadChange `yaml:",inline"` } if err := unmarshal(&wrap); err != nil { return err } *payCh = wrap.Inner if payCh.LegacyMessage != "" { payCh.Description = payCh.LegacyMessage payCh.LegacyMessage = "" } return nil } // CombinePayloadChanges takes all changes in the given range, combines them // into a single PayloadChange, and commits it. The resulting payload will have // the same message as the latest change payload in the range. If the // fingerprint of the PayloadChange produced by this method has any matching // Credentials in the range, those will be included in the payload as well. // // The combined commit is committed to the project with the given revision as // its parent. If the diff across the given range and the diff from onto to the // end of the range are different then this will return an error. func (proj *Project) CombinePayloadChanges(commits []Commit, onto plumbing.ReferenceName) (Commit, error) { info, err := proj.changeRangeInfo(commits) if err != nil { return Commit{}, err } commitsFingerprint, err := info.changeFingerprint(info.changeDescription) if err != nil { return Commit{}, err } authors := make([]string, 0, len(info.authors)) for author := range info.authors { authors = append(authors, author) } sort.Strings(authors) ontoBranchName, err := proj.ReferenceToBranchName(onto) if err != nil { return Commit{}, 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 := proj.GetCommitByRevision(plumbing.Revision(onto)) if err != nil { return Commit{}, fmt.Errorf("resolving revision %q: %w", onto, err) } ontoEndChangedFiles, err := ChangedFilesBetweenTrees(ontoCommit.TreeObject, info.endTree) if err != nil { return Commit{}, fmt.Errorf("calculating file changes between %q and %q: %w", ontoCommit.Hash, commits[len(commits)-1].Hash, err) } ontoEndChangeFingerprint := genChangeFingerprint(nil, info.changeDescription, ontoEndChangedFiles) if !bytes.Equal(ontoEndChangeFingerprint, commitsFingerprint) { // TODO figure out what files to show as being the "problem files" in // the error message return Commit{}, fmt.Errorf("combining onto %q would produce a different change fingerprint, aborting combine", onto.Short()) } var creds []sigcred.CredentialUnion for _, commit := range commits { if bytes.Equal(commit.Payload.Common.Fingerprint, commitsFingerprint) { creds = append(creds, commit.Payload.Common.Credentials...) } } // this is mostly to make tests easier sort.Slice(creds, func(i, j int) bool { return creds[i].AccountID < creds[j].AccountID }) payUn := PayloadUnion{ Change: &PayloadChange{ Description: info.changeDescription, }, Common: PayloadCommon{ Fingerprint: commitsFingerprint, Credentials: creds, }, } commit, err := proj.CommitDirect(CommitDirectParams{ PayloadUnion: payUn, Author: strings.Join(authors, ","), ParentHash: ontoCommit.Hash, GitTree: info.endTree, }) if err != nil { return Commit{}, fmt.Errorf("storing commit: %w", err) } // set the onto branch to this new commit newHeadRef := plumbing.NewHashReference(ontoBranchName, commit.Hash) if err := proj.GitRepo.Storer.SetReference(newHeadRef); err != nil { return Commit{}, fmt.Errorf("setting reference %q to new commit hash %q: %w", ontoBranchName, commit.Hash, err) } return commit, nil }