add change_description field to credential commits

---
type: change
description: |-
  add change_description field to credential commits

  This also involved changing how commit range change fingerprints are handled, so
  that the description can be more variable, and using that flexibility to allow
  the cli tool the ability to ask for the change description prior to creating a
  credential commit. Credential commit creation via the cli tool has been further
  improved in that it will check if it's been told to make a cred based on a
  specific credential commit, and if so just append the new credential and
  re-commit, speeding things up a bit for everyone.
fingerprint: AGWXqPM1Lxf3kvV/V3w65jNiUQyrQfemqfCcImSWjHJl
credentials:
- type: pgp_signature
  pub_key_id: 95C46FA6A41148AC
  body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl68xrgACgkQlcRvpqQRSKx6Ng//Qm/8dCqBe/8R7KgzW2naP352jegW5m9KoIMrVsm1kmgQfAGYs0easnks1+DadD79TEUacTrPYxDR50eScU58uybRUPPbJLXLjdoSbUYCl9bSdr6LVlZF9GVcMbH4iWhTN3YGQFHX7pEqwJ9Qzw/pbheCGPkS3KJFJZ3tC+TVM7QysdkGpxlKn8GiSIfN3d4QaHWFJW9FOc1K4mAFfmM/5QqU5j5OydlLeScJu+na7B8Lcmy4Jz+mdwIZCMG9h4qv1qD0/XcsY8Wv6cptMIBUZgg2SIyrl0KDau8pHX45PHlAaViCjzW2LjU2wMn+zKX6s3hSWje5sE95Z1a7l0ubytHGKUdlfTQDnkVYzs9hCqv3iAERChGl6E1qQWSNQMhhUcwVsZ7+Iw2M6ESrP/MvdsdO+N0t2oakbJnzUliGx8pQcY4DJPaosUadLqwKPMYiBOiSsfN2e+Vw2ZOoiHL2QUEd2VV1JfImZBl5oLFhzuLfR6h+eMkwcHjs+rtgMEh9TnA6qVzn7BzZ9p1ZFBHlw3Yrx22tG2Ehri4eMPa+5+vcuG7xlSlq4FOGvANi3E3lieeIiE61EztoX3iXeY4bdtaFlMEk6i+JU3jHdwXd006Kv0/5jd7n+OAba8aYjthNN2WuhTxj2/VSKn9iPjaCD3GJtkFQajf57v2nbK0QDx4=
  account: mediocregopher
This commit is contained in:
mediocregopher 2020-05-13 22:19:04 -06:00
parent f085f13fa8
commit 6176b9ffbc
6 changed files with 92 additions and 26 deletions

View File

@ -174,10 +174,14 @@ credentials: [...]
# This field is not required, but can be helpful in situations where the # This field is not required, but can be helpful in situations where the
# fingerprint was generated based on multiple change payloads # fingerprint was generated based on multiple change payloads
fingerprint_commits: commits:
- commit hash - commit hash
- commit hash - commit hash
- commit hash - commit hash
# This field is not required, but can be helpful to clarify which description
# was used when generating a change fingerprint.
change_description: blah blah blah
``` ```
## Project Configuration ## Project Configuration

View File

@ -136,25 +136,43 @@ func cmdCommit(ctx context.Context, cmd *dcmd.Cmd) {
return nil, errors.New("credential commit cannot have staged changes") return nil, errors.New("credential commit cannot have staged changes")
} }
var credPayUn dehub.PayloadUnion var commits []dehub.Commit
if *rev != "" { if *rev != "" {
commit, err := proj.GetCommitByRevision(plumbing.Revision(*rev)) commit, err := proj.GetCommitByRevision(plumbing.Revision(*rev))
if err != nil { if err != nil {
return nil, fmt.Errorf("resolving revision %q: %w", *rev, err) return nil, fmt.Errorf("resolving revision %q: %w", *rev, err)
} }
commits = []dehub.Commit{commit}
if credPayUn, err = proj.NewPayloadCredentialFromChanges([]dehub.Commit{commit}); err != nil {
return nil, fmt.Errorf("constructing credential commit: %w", err)
}
} else { } else {
commits, err := proj.GetCommitRangeByRevision( var err error
commits, err = proj.GetCommitRangeByRevision(
plumbing.Revision(*startRev), plumbing.Revision(*startRev),
plumbing.Revision(*endRev), plumbing.Revision(*endRev),
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("resolving revisions %q to %q: %w", return nil, fmt.Errorf("resolving revisions %q to %q: %w",
*startRev, *endRev, err) *startRev, *endRev, err)
} else if credPayUn, err = proj.NewPayloadCredentialFromChanges(commits); err != nil { }
}
var credPayUn dehub.PayloadUnion
if len(commits) == 0 {
return nil, errors.New("cannot create credential based on empty range of commits")
} else if len(commits) == 1 && commits[0].Payload.Credential != nil {
credPayUn = commits[0].Payload
} else {
lastDescr, err := dehub.LastChangeDescription(commits)
if err != nil {
return nil, fmt.Errorf("determining change description of commit(s): %w", err)
}
descr, err := tmpFileMsg(defaultCommitFileMsgTpl, lastDescr)
if err != nil {
return nil, fmt.Errorf("collecting credential description from user: %w", err)
}
credPayUn, err = proj.NewPayloadCredentialFromChanges(descr, commits)
if err != nil {
return nil, fmt.Errorf("constructing credential commit: %w", err) return nil, fmt.Errorf("constructing credential commit: %w", err)
} }
} }

View File

@ -14,8 +14,8 @@ import (
const defaultCommitFileMsgTpl = `%s const defaultCommitFileMsgTpl = `%s
# Please enter the commit message for your commit. Lines starting # Please enter the description for your commit(s). Lines starting with '#' will
# with '#' will be ignored, and an empty message aborts the commit.` # be ignored, and an empty message aborts the commit.`
func tmpFileMsg(tpl string, args ...interface{}) (string, error) { func tmpFileMsg(tpl string, args ...interface{}) (string, error) {
editor := os.Getenv("EDITOR") editor := os.Getenv("EDITOR")

View File

@ -490,12 +490,31 @@ func (proj *Project) verifyCommit(
return nil return nil
} }
// LastChangeDescription iterates over the given commits in reverse order and
// returns the first change description it comes across. A change description
// may come from a change payload or a credential payload which covers a set of
// changes.
//
// This function will return an error if no given commits contain a change
// description.
func LastChangeDescription(commits []Commit) (string, error) {
for i := range commits {
i = len(commits) - 1 - i
payUn := commits[i].Payload
if payUn.Change != nil {
return payUn.Change.Description, nil
} else if payUn.Credential != nil && payUn.Credential.ChangeDescription != "" {
return payUn.Credential.ChangeDescription, nil
}
}
return "", errors.New("no commits in range contain a change description")
}
type changeRangeInfo struct { type changeRangeInfo struct {
changeCommits []Commit changeCommits []Commit
authors map[string]struct{} authors map[string]struct{}
msg string
startTree, endTree *object.Tree startTree, endTree *object.Tree
changeFingerprint []byte changeDescription string
} }
// changeRangeInfo returns various pieces of information about a range of // changeRangeInfo returns various pieces of information about a range of
@ -523,20 +542,23 @@ func (proj *Project) changeRangeInfo(commits []Commit) (changeRangeInfo, error)
if info.startTree, err = proj.parentTree(commits[0].Object); err != nil { if info.startTree, err = proj.parentTree(commits[0].Object); err != nil {
return changeRangeInfo{}, fmt.Errorf("getting tree of parent of %q: %w", return changeRangeInfo{}, fmt.Errorf("getting tree of parent of %q: %w",
commits[0].Hash, err) commits[0].Hash, err)
} else if info.changeDescription, err = LastChangeDescription(commits); err != nil {
return changeRangeInfo{}, err
} }
lastChangeCommit := info.changeCommits[len(info.changeCommits)-1] lastChangeCommit := info.changeCommits[len(info.changeCommits)-1]
info.msg = lastChangeCommit.Payload.Change.Description
info.endTree = lastChangeCommit.TreeObject info.endTree = lastChangeCommit.TreeObject
return info, nil
}
func (info changeRangeInfo) changeFingerprint(descr string) ([]byte, error) {
changedFiles, err := ChangedFilesBetweenTrees(info.startTree, info.endTree) changedFiles, err := ChangedFilesBetweenTrees(info.startTree, info.endTree)
if err != nil { if err != nil {
return changeRangeInfo{}, fmt.Errorf("calculating diff of commit trees %q and %q: %w", return nil, fmt.Errorf("calculating diff of commit trees %q and %q: %w",
info.startTree.Hash, info.endTree.Hash, err) info.startTree.Hash, info.endTree.Hash, err)
} }
info.changeFingerprint = genChangeFingerprint(nil, info.msg, changedFiles) return genChangeFingerprint(nil, descr, changedFiles), nil
return info, nil
} }
// VerifyCanSetBranchHEADTo is used to verify that a branch's HEAD can be set to // VerifyCanSetBranchHEADTo is used to verify that a branch's HEAD can be set to

View File

@ -98,6 +98,11 @@ func (proj *Project) CombinePayloadChanges(commits []Commit, onto plumbing.Refer
return Commit{}, err return Commit{}, err
} }
commitsFingerprint, err := info.changeFingerprint(info.changeDescription)
if err != nil {
return Commit{}, err
}
authors := make([]string, 0, len(info.authors)) authors := make([]string, 0, len(info.authors))
for author := range info.authors { for author := range info.authors {
authors = append(authors, author) authors = append(authors, author)
@ -122,8 +127,8 @@ func (proj *Project) CombinePayloadChanges(commits []Commit, onto plumbing.Refer
ontoCommit.Hash, commits[len(commits)-1].Hash, err) ontoCommit.Hash, commits[len(commits)-1].Hash, err)
} }
ontoEndChangeFingerprint := genChangeFingerprint(nil, info.msg, ontoEndChangedFiles) ontoEndChangeFingerprint := genChangeFingerprint(nil, info.changeDescription, ontoEndChangedFiles)
if !bytes.Equal(ontoEndChangeFingerprint, info.changeFingerprint) { if !bytes.Equal(ontoEndChangeFingerprint, commitsFingerprint) {
// TODO figure out what files to show as being the "problem files" in // TODO figure out what files to show as being the "problem files" in
// the error message // the error message
return Commit{}, fmt.Errorf("combining onto %q would produce a different change fingerprint, aborting combine", onto.Short()) return Commit{}, fmt.Errorf("combining onto %q would produce a different change fingerprint, aborting combine", onto.Short())
@ -131,7 +136,7 @@ func (proj *Project) CombinePayloadChanges(commits []Commit, onto plumbing.Refer
var creds []sigcred.CredentialUnion var creds []sigcred.CredentialUnion
for _, commit := range commits { for _, commit := range commits {
if bytes.Equal(commit.Payload.Common.Fingerprint, info.changeFingerprint) { if bytes.Equal(commit.Payload.Common.Fingerprint, commitsFingerprint) {
creds = append(creds, commit.Payload.Common.Credentials...) creds = append(creds, commit.Payload.Common.Credentials...)
} }
} }
@ -143,10 +148,10 @@ func (proj *Project) CombinePayloadChanges(commits []Commit, onto plumbing.Refer
payUn := PayloadUnion{ payUn := PayloadUnion{
Change: &PayloadChange{ Change: &PayloadChange{
Description: info.msg, Description: info.changeDescription,
}, },
Common: PayloadCommon{ Common: PayloadCommon{
Fingerprint: info.changeFingerprint, Fingerprint: commitsFingerprint,
Credentials: creds, Credentials: creds,
}, },
} }

View File

@ -10,6 +10,11 @@ type PayloadCredential struct {
// It is only present for informational purposes, as commits don't not have // It is only present for informational purposes, as commits don't not have
// any bearing on the CredentialedHash itself. // any bearing on the CredentialedHash itself.
CommitHashes []string `yaml:"commits,omitempty"` CommitHashes []string `yaml:"commits,omitempty"`
// ChangeDescription represents the description which has been credentialed.
// This field is only relevant if the Credential in the payload is for a
// change set.
ChangeDescription string `yaml:"change_description"`
} }
var _ Payload = PayloadCredential{} var _ Payload = PayloadCredential{}
@ -26,26 +31,38 @@ func (proj *Project) NewPayloadCredential(fingerprint []byte) (PayloadUnion, err
// NewPayloadCredentialFromChanges constructs and returns a PayloadUnion // NewPayloadCredentialFromChanges constructs and returns a PayloadUnion
// populated with a PayloadCredential. The fingerprint of the payload will be a // populated with a PayloadCredential. The fingerprint of the payload will be a
// change fingerprint encompassing all changes in the given range of Commits. // change fingerprint generated from the given description and all changes in
// The description of the last change payload in the range is used when // the given range of Commits.
// generating the fingerprint. //
func (proj *Project) NewPayloadCredentialFromChanges(commits []Commit) (PayloadUnion, error) { // If an empty description is given then the description of the last change
// payload in the range is used when generating the fingerprint.
func (proj *Project) NewPayloadCredentialFromChanges(descr string, commits []Commit) (PayloadUnion, error) {
info, err := proj.changeRangeInfo(commits) info, err := proj.changeRangeInfo(commits)
if err != nil { if err != nil {
return PayloadUnion{}, err return PayloadUnion{}, err
} }
payCred, err := proj.NewPayloadCredential(info.changeFingerprint) if descr == "" {
descr = info.changeDescription
}
fingerprint, err := info.changeFingerprint(descr)
if err != nil { if err != nil {
return PayloadUnion{}, err return PayloadUnion{}, err
} }
payCred, err := proj.NewPayloadCredential(fingerprint)
if err != nil {
return PayloadUnion{}, err
}
payCred.Credential.ChangeDescription = descr
for _, commit := range info.changeCommits { for _, commit := range info.changeCommits {
payCred.Credential.CommitHashes = append( payCred.Credential.CommitHashes = append(
payCred.Credential.CommitHashes, payCred.Credential.CommitHashes,
commit.Hash.String(), commit.Hash.String(),
) )
} }
return payCred, nil return payCred, nil
} }