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.

176 lines
5.5 KiB

package dehub
import (
// 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)
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