refactor how file changes and hashes are handled, and add tests for hashes
--- type: change message: |- refactor how file changes and hashes are handled, and add tests for hashes This involved a few different changes that all worked together: * Making fileChanged public as ChangedFile, and reorganizing how it's created/used a bit. * Renaming the CommitInterface methods to be clearer, and passing in a `[]ChangedFile` into `ExpectedHash` (previously just `Hash`) rather than trees. * Adding tests for the gen*Hash functions. change_hash: AGCdt2k6FARuf1CBao7zxiuDVMOAdeL5vL8Whnr9KjMu credentials: - type: pgp_signature pub_key_id: 95C46FA6A41148AC body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl6bVGIACgkQlcRvpqQRSKwkmhAAqAuUXKIpr1caE7yuM09udiW51G0TtSe0tLItoZ6ROnSe6lsISZ3hkmOWjSyfeeZFggGIAlK3Uipb42pbSO3Usf0fqDWdNekPLRtTd6qpB65Ye9kDgC3Nz6Z0EhFSMDc6sQzNcisWqAAry6CyEy4ucAm3aK1frSTH/6i/x9DQZXY7WVLM5NewegTr/fatf57Fh6kky7o7yiALeOmQR/oAh7x4AADJgmJMs2aBLsO958GwrNOlQI/RhC83TfoXM7YcTkFv+xz2LucIyBZYYxpM327UTmUk2vuOf3p2OFbiISfIZxFeI3KhpUH05TVXyzohQTnrwyWJDKFGKO8iqxaWHRxdH0INUKsAcBTA8dzTGoRyFF5CUKYX0ZnorlT5NV2HeObmFaDpUHNQKHCcQAAAFoSOGVNNqfGS1IqUNwwZkln+Nzr7YrxmCJ98fDopF0C9ZWhNzNF3+oQbOCIOj3+1kqxsQrcqgK3dwefCL46u244qLDGyqF9s8Aqp4rtrVT+4V2hTB7psdR/EDwu5xZDtXEPEktY+6z0RyYwqxUKBkSklOd+dgmqMYw7YrrNKCW433HjHUEf8Qk/x8CjRANkNYMgOAQqND0pegaAHRJJJbD4xMBrMBO8QlAX5+/ocFfoyEXTKlUuNJXVOh/2TDtOWXhmbVhOBsx7TOhTjUhtAGwE= account: mediocregopher
This commit is contained in:
parent
1f892070bd
commit
84399603cf
50
commit.go
50
commit.go
@ -22,23 +22,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// CommitInterface describes the methods which must be implemented by the
|
// CommitInterface describes the methods which must be implemented by the
|
||||||
// different commit types.
|
// different commit types. None of the methods should modify the underlying
|
||||||
|
// object.
|
||||||
type CommitInterface interface {
|
type CommitInterface interface {
|
||||||
// MessageHead returns the head of the commit message (i.e. the first line).
|
// MessageHead returns the head of the commit message (i.e. the first line).
|
||||||
// The CommitCommon of the outer Commit is passed in for added context, if
|
// The CommitCommon of the outer Commit is passed in for added context, if
|
||||||
// necessary.
|
// necessary.
|
||||||
MessageHead(CommitCommon) (string, error)
|
MessageHead(CommitCommon) (string, error)
|
||||||
|
|
||||||
// Hash returns the raw hash which Signifiers can sign to accredit this
|
// ExpectedHash returns the raw hash which Signifiers can sign to accredit
|
||||||
// commit. The tree objects given describe the filesystem state of the
|
// this commit. The ChangedFile objects given describe the file changes
|
||||||
// parent commit, and the filesystem state of this commit.
|
// between the parent commit and this commit.
|
||||||
//
|
ExpectedHash([]ChangedFile) ([]byte, error)
|
||||||
// This method should _not_ change any fields on the commit.
|
|
||||||
Hash(parent, this *object.Tree) ([]byte, error)
|
|
||||||
|
|
||||||
// GetHash returns the signable Hash embedded in the commit, which should
|
// StoredHash returns the signable Hash embedded in the commit, which should
|
||||||
// hopefully correspond to the Commit's Credentials.
|
// hopefully correspond to the ExpectedHash.
|
||||||
GetHash() []byte
|
StoredHash() []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommitCommon describes the fields common to all Commit objects.
|
// CommitCommon describes the fields common to all Commit objects.
|
||||||
@ -173,7 +172,7 @@ func (r *Repo) AccreditCommit(commit Commit, sigInt sigcred.SignifierInterface)
|
|||||||
return commit, fmt.Errorf("could not grab snapshot of HEAD fs: %w", err)
|
return commit, fmt.Errorf("could not grab snapshot of HEAD fs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cred, err := sigInt.Sign(headFS, commitInt.GetHash())
|
cred, err := sigInt.Sign(headFS, commitInt.StoredHash())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return commit, fmt.Errorf("could not accredit change commit: %w", err)
|
return commit, fmt.Errorf("could not accredit change commit: %w", err)
|
||||||
}
|
}
|
||||||
@ -388,18 +387,18 @@ func (r *Repo) verifyCommit(branch plumbing.ReferenceName, gitCommit GitCommit,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// assert access controls
|
// assert access controls
|
||||||
filesChanged, err := calcDiff(parentTree, gitCommit.GitTree)
|
changedFiles, err := ChangedFilesBetweenTrees(parentTree, gitCommit.GitTree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("calculating diff from tree %q to tree %q: %w",
|
return fmt.Errorf("calculating diff from tree %q to tree %q: %w",
|
||||||
parentTree.Hash, gitCommit.GitTree.Hash, err)
|
parentTree.Hash, gitCommit.GitTree.Hash, err)
|
||||||
|
|
||||||
} else if len(filesChanged) > 0 && gitCommit.Commit.Change == nil {
|
} else if len(changedFiles) > 0 && gitCommit.Commit.Change == nil {
|
||||||
return errors.New("files changes but commit is not a change commit")
|
return errors.New("files changes but commit is not a change commit")
|
||||||
}
|
}
|
||||||
|
|
||||||
pathsChanged := make([]string, len(filesChanged))
|
pathsChanged := make([]string, len(changedFiles))
|
||||||
for i := range filesChanged {
|
for i := range changedFiles {
|
||||||
pathsChanged[i] = filesChanged[i].path
|
pathsChanged[i] = changedFiles[i].Path
|
||||||
}
|
}
|
||||||
|
|
||||||
commitType, err := gitCommit.Commit.Type()
|
commitType, err := gitCommit.Commit.Type()
|
||||||
@ -418,14 +417,14 @@ func (r *Repo) verifyCommit(branch plumbing.ReferenceName, gitCommit GitCommit,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ensure the hash is what it's expected to be
|
// ensure the hash is what it's expected to be
|
||||||
commitHash := gitCommit.Interface.GetHash()
|
storedCommitHash := gitCommit.Interface.StoredHash()
|
||||||
expectedCommitHash, err := gitCommit.Interface.Hash(parentTree, gitCommit.GitTree)
|
expectedCommitHash, err := gitCommit.Interface.ExpectedHash(changedFiles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("calculating expected commit hash: %w", err)
|
return fmt.Errorf("calculating expected commit hash: %w", err)
|
||||||
} else if !bytes.Equal(commitHash, expectedCommitHash) {
|
} else if !bytes.Equal(storedCommitHash, expectedCommitHash) {
|
||||||
return fmt.Errorf("unexpected hash in commit body, is %s but should be %s",
|
return fmt.Errorf("unexpected hash in commit body, is %s but should be %s",
|
||||||
base64.StdEncoding.EncodeToString(expectedCommitHash),
|
base64.StdEncoding.EncodeToString(storedCommitHash),
|
||||||
base64.StdEncoding.EncodeToString(commitHash))
|
base64.StdEncoding.EncodeToString(expectedCommitHash))
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify all credentials
|
// verify all credentials
|
||||||
@ -485,6 +484,13 @@ func (r *Repo) changeRangeInfo(commits []GitCommit) (changeRangeInfo, error) {
|
|||||||
lastChangeCommit := info.changeCommits[len(info.changeCommits)-1]
|
lastChangeCommit := info.changeCommits[len(info.changeCommits)-1]
|
||||||
info.msg = lastChangeCommit.Commit.Change.Message
|
info.msg = lastChangeCommit.Commit.Change.Message
|
||||||
info.endTree = lastChangeCommit.GitTree
|
info.endTree = lastChangeCommit.GitTree
|
||||||
info.changeHash = genChangeHash(nil, info.msg, info.startTree, info.endTree)
|
|
||||||
|
changedFiles, err := ChangedFilesBetweenTrees(info.startTree, info.endTree)
|
||||||
|
if err != nil {
|
||||||
|
return changeRangeInfo{}, fmt.Errorf("calculating diff of commit trees %q and %q: %w",
|
||||||
|
info.startTree.Hash, info.endTree.Hash, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
info.changeHash = genChangeHash(nil, info.msg, changedFiles)
|
||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,15 @@ package dehub
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"dehub.dev/src/dehub.git/fs"
|
|
||||||
"dehub.dev/src/dehub.git/sigcred"
|
|
||||||
"dehub.dev/src/dehub.git/yamlutil"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"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"
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
||||||
)
|
)
|
||||||
@ -38,8 +39,13 @@ func (r *Repo) NewCommitChange(msg string) (Commit, error) {
|
|||||||
return Commit{}, err
|
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}
|
cc := CommitChange{Message: msg}
|
||||||
if cc.ChangeHash, err = cc.Hash(headTree, stagedTree); err != nil {
|
if cc.ChangeHash, err = cc.ExpectedHash(changedFiles); err != nil {
|
||||||
return Commit{}, err
|
return Commit{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,13 +59,13 @@ func (cc CommitChange) MessageHead(CommitCommon) (string, error) {
|
|||||||
return abbrevCommitMessage(cc.Message), nil
|
return abbrevCommitMessage(cc.Message), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash implements the method for the CommitInterface interface.
|
// ExpectedHash implements the method for the CommitInterface interface.
|
||||||
func (cc CommitChange) Hash(parent, this *object.Tree) ([]byte, error) {
|
func (cc CommitChange) ExpectedHash(changedFiles []ChangedFile) ([]byte, error) {
|
||||||
return genChangeHash(nil, cc.Message, parent, this), nil
|
return genChangeHash(nil, cc.Message, changedFiles), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHash implements the method for the CommitInterface interface.
|
// StoredHash implements the method for the CommitInterface interface.
|
||||||
func (cc CommitChange) GetHash() []byte {
|
func (cc CommitChange) StoredHash() []byte {
|
||||||
return cc.ChangeHash
|
return cc.ChangeHash
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,17 +102,23 @@ func (r *Repo) CombineCommitChanges(commits []GitCommit, onto plumbing.Reference
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return GitCommit{}, fmt.Errorf("resolving revision %q: %w", onto, err)
|
return GitCommit{}, fmt.Errorf("resolving revision %q: %w", onto, err)
|
||||||
}
|
}
|
||||||
ontoTree := ontoCommit.GitTree
|
|
||||||
ontoEndChangeHash := genChangeHash(nil, info.msg, ontoTree, info.endTree)
|
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) {
|
if !bytes.Equal(ontoEndChangeHash, info.changeHash) {
|
||||||
// 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 GitCommit{}, fmt.Errorf("rebasing onto %q would cause the change hash to change, aborting combine", onto)
|
return GitCommit{}, fmt.Errorf("combining onto %q would cause the change hash to change, aborting combine", onto.Short())
|
||||||
}
|
}
|
||||||
|
|
||||||
var creds []sigcred.Credential
|
var creds []sigcred.Credential
|
||||||
for _, commit := range commits {
|
for _, commit := range commits {
|
||||||
if bytes.Equal(commit.Interface.GetHash(), info.changeHash) {
|
if bytes.Equal(commit.Interface.StoredHash(), info.changeHash) {
|
||||||
creds = append(creds, commit.Commit.Common.Credentials...)
|
creds = append(creds, commit.Commit.Common.Credentials...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,7 +141,7 @@ func TestCombineCommitChanges(t *testing.T) {
|
|||||||
fooCommit := h.assertCommitChange(true, "add foo file", rootSig)
|
fooCommit := h.assertCommitChange(true, "add foo file", rootSig)
|
||||||
|
|
||||||
// now adding a credential commit from toot should work
|
// now adding a credential commit from toot should work
|
||||||
credCommitObj, err := h.repo.NewCommitCredential(fooCommit.Interface.GetHash())
|
credCommitObj, err := h.repo.NewCommitCredential(fooCommit.Interface.StoredHash())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
package dehub
|
package dehub
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"dehub.dev/src/dehub.git/yamlutil"
|
"dehub.dev/src/dehub.git/yamlutil"
|
||||||
|
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CommitComment describes the structure of a comment commit message.
|
// CommitComment describes the structure of a comment commit message.
|
||||||
@ -17,13 +16,13 @@ type CommitComment struct {
|
|||||||
|
|
||||||
var _ CommitInterface = CommitComment{}
|
var _ CommitInterface = CommitComment{}
|
||||||
|
|
||||||
// NewCommitComment constructs and returns a COmmit populated with a
|
// NewCommitComment constructs and returns a Commit populated with a
|
||||||
// CommitComment encompassing the given message. The Credentials of the returned
|
// CommitComment encompassing the given message. The Credentials of the returned
|
||||||
// Commit will _not_ be filled in.
|
// Commit will _not_ be filled in.
|
||||||
func (r *Repo) NewCommitComment(msg string) (Commit, error) {
|
func (r *Repo) NewCommitComment(msg string) (Commit, error) {
|
||||||
cc := CommitComment{Message: msg}
|
cc := CommitComment{Message: msg}
|
||||||
var err error
|
var err error
|
||||||
if cc.MessageHash, err = cc.Hash(nil, nil); err != nil {
|
if cc.MessageHash, err = cc.ExpectedHash(nil); err != nil {
|
||||||
return Commit{}, fmt.Errorf("calculating comment hash: %w", err)
|
return Commit{}, fmt.Errorf("calculating comment hash: %w", err)
|
||||||
}
|
}
|
||||||
return Commit{Comment: &cc}, nil
|
return Commit{Comment: &cc}, nil
|
||||||
@ -36,12 +35,15 @@ func (cc CommitComment) MessageHead(common CommitCommon) (string, error) {
|
|||||||
return fmt.Sprintf("Comment by %s: %s", credIDs, msgAbbrev), nil
|
return fmt.Sprintf("Comment by %s: %s", credIDs, msgAbbrev), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash implements the method for the CommitInterface.
|
// ExpectedHash implements the method for the CommitInterface.
|
||||||
func (cc CommitComment) Hash(_, _ *object.Tree) ([]byte, error) {
|
func (cc CommitComment) ExpectedHash(changes []ChangedFile) ([]byte, error) {
|
||||||
|
if len(changes) > 0 {
|
||||||
|
return nil, errors.New("CommitComment cannot have any changed files")
|
||||||
|
}
|
||||||
return genCommentHash(nil, cc.Message), nil
|
return genCommentHash(nil, cc.Message), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHash implements the method for the CommitInterface.
|
// StoredHash implements the method for the CommitInterface.
|
||||||
func (cc CommitComment) GetHash() []byte {
|
func (cc CommitComment) StoredHash() []byte {
|
||||||
return cc.MessageHash
|
return cc.MessageHash
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,11 @@ package dehub
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"dehub.dev/src/dehub.git/yamlutil"
|
"dehub.dev/src/dehub.git/yamlutil"
|
||||||
|
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CommitCredential describes the structure of a credential commit message.
|
// CommitCredential describes the structure of a credential commit message.
|
||||||
@ -33,7 +32,7 @@ func (r *Repo) NewCommitCredential(hash []byte) (Commit, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCommitCredentialFromChanges constructs and returns a Comit populated with
|
// NewCommitCredentialFromChanges constructs and returns a Commit populated with
|
||||||
// a CommitCredential for all changes in the given range of GitCommits. The
|
// a CommitCredential for all changes in the given range of GitCommits. The
|
||||||
// message of the last change commit in the range is used when generating the
|
// message of the last change commit in the range is used when generating the
|
||||||
// hash.
|
// hash.
|
||||||
@ -68,12 +67,15 @@ func (cc CommitCredential) MessageHead(common CommitCommon) (string, error) {
|
|||||||
return fmt.Sprintf("Credential of hash %s by %s", hash64, credIDs), nil
|
return fmt.Sprintf("Credential of hash %s by %s", hash64, credIDs), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash implements the method for the CommitInterface.
|
// ExpectedHash implements the method for the CommitInterface.
|
||||||
func (cc CommitCredential) Hash(_, _ *object.Tree) ([]byte, error) {
|
func (cc CommitCredential) ExpectedHash(changes []ChangedFile) ([]byte, error) {
|
||||||
|
if len(changes) > 0 {
|
||||||
|
return nil, errors.New("CommitCredential cannot have any changed files")
|
||||||
|
}
|
||||||
return cc.CredentialedHash, nil
|
return cc.CredentialedHash, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHash implements the method for the CommitInterface.
|
// StoredHash implements the method for the CommitInterface.
|
||||||
func (cc CommitCredential) GetHash() []byte {
|
func (cc CommitCredential) StoredHash() []byte {
|
||||||
return cc.CredentialedHash
|
return cc.CredentialedHash
|
||||||
}
|
}
|
||||||
|
36
diff.go
36
diff.go
@ -8,34 +8,38 @@ import (
|
|||||||
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fileChanged struct {
|
// ChangedFile describes a single file which has been changed in some way
|
||||||
path string
|
// between two object.Trees. If the From fields are empty then the file was
|
||||||
fromMode, toMode filemode.FileMode
|
// created, if the To fields are empty then the file was deleted.
|
||||||
fromHash, toHash plumbing.Hash
|
type ChangedFile struct {
|
||||||
|
Path string
|
||||||
|
FromMode, ToMode filemode.FileMode
|
||||||
|
FromHash, ToHash plumbing.Hash
|
||||||
}
|
}
|
||||||
|
|
||||||
func calcDiff(from, to *object.Tree) ([]fileChanged, error) {
|
// ChangedFilesBetweenTrees returns the ChangedFile objects which represent the
|
||||||
|
// difference between the two given trees.
|
||||||
|
func ChangedFilesBetweenTrees(from, to *object.Tree) ([]ChangedFile, error) {
|
||||||
changes, err := object.DiffTree(from, to)
|
changes, err := object.DiffTree(from, to)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not calculate tree diff: %w", err)
|
return nil, fmt.Errorf("could not calculate tree diff: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
filesChanged := make([]fileChanged, len(changes))
|
changedFiles := make([]ChangedFile, len(changes))
|
||||||
for i, change := range changes {
|
for i, change := range changes {
|
||||||
if from := change.From; from.Name != "" {
|
if from := change.From; from.Name != "" {
|
||||||
filesChanged[i].path = from.Name
|
changedFiles[i].Path = from.Name
|
||||||
filesChanged[i].fromMode = from.TreeEntry.Mode
|
changedFiles[i].FromMode = from.TreeEntry.Mode
|
||||||
filesChanged[i].fromHash = from.TreeEntry.Hash
|
changedFiles[i].FromHash = from.TreeEntry.Hash
|
||||||
}
|
}
|
||||||
if to := change.To; to.Name != "" {
|
if to := change.To; to.Name != "" {
|
||||||
if exPath := filesChanged[i].path; exPath != "" && exPath != to.Name {
|
if exPath := changedFiles[i].Path; exPath != "" && exPath != to.Name {
|
||||||
panic(fmt.Sprintf("DiffTree entry changed path from %q to %q", exPath, to.Name))
|
panic(fmt.Sprintf("unexpected changed path from %q to %q", exPath, to.Name))
|
||||||
}
|
}
|
||||||
filesChanged[i].path = to.Name
|
changedFiles[i].Path = to.Name
|
||||||
filesChanged[i].toMode = to.TreeEntry.Mode
|
changedFiles[i].ToMode = to.TreeEntry.Mode
|
||||||
filesChanged[i].toHash = to.TreeEntry.Hash
|
changedFiles[i].ToHash = to.TreeEntry.Hash
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return filesChanged, nil
|
return changedFiles, nil
|
||||||
}
|
}
|
||||||
|
46
hash.go
46
hash.go
@ -6,8 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -15,7 +13,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type hashHelper struct {
|
type hashHelper struct {
|
||||||
hash.Hash
|
hash hash.Hash
|
||||||
varintBuf []byte
|
varintBuf []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,49 +23,43 @@ func newHashHelper(h hash.Hash) *hashHelper {
|
|||||||
h = defaultHashHelperAlgo()
|
h = defaultHashHelperAlgo()
|
||||||
}
|
}
|
||||||
s := &hashHelper{
|
s := &hashHelper{
|
||||||
Hash: h,
|
hash: h,
|
||||||
varintBuf: make([]byte, binary.MaxVarintLen64),
|
varintBuf: make([]byte, binary.MaxVarintLen64),
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *hashHelper) sum(prefix []byte) []byte {
|
func (s *hashHelper) sum(prefix []byte) []byte {
|
||||||
out := make([]byte, len(prefix), len(prefix)+s.Hash.Size())
|
out := make([]byte, len(prefix), len(prefix)+s.hash.Size())
|
||||||
copy(out, prefix)
|
copy(out, prefix)
|
||||||
return s.Hash.Sum(out)
|
return s.hash.Sum(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *hashHelper) writeUint(i uint64) {
|
func (s *hashHelper) writeUint(i uint64) {
|
||||||
n := binary.PutUvarint(s.varintBuf, i)
|
n := binary.PutUvarint(s.varintBuf, i)
|
||||||
if _, err := s.Write(s.varintBuf[:n]); err != nil {
|
if _, err := s.hash.Write(s.varintBuf[:n]); err != nil {
|
||||||
panic(fmt.Sprintf("error writing %x to sha256 sum: %v", s.varintBuf[:n], err))
|
panic(fmt.Sprintf("error writing %x to %T: %v", s.varintBuf[:n], s.hash, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *hashHelper) writeStr(str string) {
|
func (s *hashHelper) writeStr(str string) {
|
||||||
s.writeUint(uint64(len(str)))
|
s.writeUint(uint64(len(str)))
|
||||||
s.Write([]byte(str))
|
s.hash.Write([]byte(str))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *hashHelper) writeTreeDiff(from, to *object.Tree) {
|
func (s *hashHelper) writeChangedFiles(changedFiles []ChangedFile) {
|
||||||
filesChanged, err := calcDiff(from, to)
|
sort.Slice(changedFiles, func(i, j int) bool {
|
||||||
if err != nil {
|
return changedFiles[i].Path < changedFiles[j].Path
|
||||||
panic(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(filesChanged, func(i, j int) bool {
|
|
||||||
return filesChanged[i].path < filesChanged[j].path
|
|
||||||
})
|
})
|
||||||
|
|
||||||
s.writeUint(uint64(len(filesChanged)))
|
s.writeUint(uint64(len(changedFiles)))
|
||||||
for _, fileChanged := range filesChanged {
|
for _, fileChanged := range changedFiles {
|
||||||
s.writeStr(fileChanged.path)
|
s.writeStr(fileChanged.Path)
|
||||||
s.Write(fileChanged.fromMode.Bytes())
|
s.hash.Write(fileChanged.FromMode.Bytes())
|
||||||
s.Write(fileChanged.fromHash[:])
|
s.hash.Write(fileChanged.FromHash[:])
|
||||||
s.Write(fileChanged.toMode.Bytes())
|
s.hash.Write(fileChanged.ToMode.Bytes())
|
||||||
s.Write(fileChanged.toHash[:])
|
s.hash.Write(fileChanged.ToHash[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -76,10 +68,10 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// if h is nil it then defaultHashHelperAlgo will be used
|
// if h is nil it then defaultHashHelperAlgo will be used
|
||||||
func genChangeHash(h hash.Hash, msg string, from, to *object.Tree) []byte {
|
func genChangeHash(h hash.Hash, msg string, changedFiles []ChangedFile) []byte {
|
||||||
s := newHashHelper(h)
|
s := newHashHelper(h)
|
||||||
s.writeStr(msg)
|
s.writeStr(msg)
|
||||||
s.writeTreeDiff(from, to)
|
s.writeChangedFiles(changedFiles)
|
||||||
return s.sum(changeHashVersion)
|
return s.sum(changeHashVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
237
hash_test.go
Normal file
237
hash_test.go
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
package dehub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"hash"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gopkg.in/src-d/go-git.v4/plumbing"
|
||||||
|
"gopkg.in/src-d/go-git.v4/plumbing/filemode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testHash struct {
|
||||||
|
bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ hash.Hash = new(testHash)
|
||||||
|
|
||||||
|
func (th *testHash) Sum(b []byte) []byte {
|
||||||
|
return append(b, th.Buffer.Bytes()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (th *testHash) Size() int {
|
||||||
|
return th.Buffer.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (th *testHash) BlockSize() int {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (th *testHash) assertContents(t *testing.T, parts [][]byte) {
|
||||||
|
b := th.Buffer.Bytes()
|
||||||
|
for _, part := range parts {
|
||||||
|
if len(part) > len(b) || !bytes.Equal(part, b[:len(part)]) {
|
||||||
|
t.Fatalf("expected %q but only found %q", part, b)
|
||||||
|
}
|
||||||
|
b = b[len(part):]
|
||||||
|
}
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatalf("unexpected extra bytes written to testHash: %q", b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func uvarint(i uint64) []byte {
|
||||||
|
buf := make([]byte, binary.MaxVarintLen64)
|
||||||
|
n := binary.PutUvarint(buf, i)
|
||||||
|
return buf[:n]
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenCommentHash(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
descr string
|
||||||
|
comment string
|
||||||
|
exp [][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []test{
|
||||||
|
{
|
||||||
|
descr: "empty comment",
|
||||||
|
comment: "",
|
||||||
|
exp: [][]byte{uvarint(0)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
descr: "normal comment",
|
||||||
|
comment: "this is a normal comment",
|
||||||
|
exp: [][]byte{uvarint(24), []byte("this is a normal comment")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
descr: "comment with unicode",
|
||||||
|
comment: "sick comment ⚡",
|
||||||
|
exp: [][]byte{uvarint(16), []byte("sick comment ⚡")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.descr, func(t *testing.T) {
|
||||||
|
th := new(testHash)
|
||||||
|
genCommentHash(th, test.comment)
|
||||||
|
th.assertContents(t, test.exp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenChangeHash(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
descr string
|
||||||
|
msg string
|
||||||
|
changedFiles []ChangedFile
|
||||||
|
exp [][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := func(i byte) plumbing.Hash {
|
||||||
|
var h plumbing.Hash
|
||||||
|
h[0] = i
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
hashB := func(i byte) []byte {
|
||||||
|
h := hash(i)
|
||||||
|
return h[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []test{
|
||||||
|
{
|
||||||
|
descr: "empty",
|
||||||
|
msg: "",
|
||||||
|
changedFiles: nil,
|
||||||
|
exp: [][]byte{uvarint(0), uvarint(0)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
descr: "empty changes",
|
||||||
|
msg: "some msg",
|
||||||
|
changedFiles: nil,
|
||||||
|
exp: [][]byte{uvarint(8), []byte("some msg"), uvarint(0)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
descr: "empty msg",
|
||||||
|
msg: "",
|
||||||
|
changedFiles: []ChangedFile{{
|
||||||
|
Path: "foo",
|
||||||
|
ToMode: filemode.Regular, ToHash: hash(1),
|
||||||
|
}},
|
||||||
|
exp: [][]byte{uvarint(0), uvarint(1),
|
||||||
|
uvarint(3), []byte("foo"),
|
||||||
|
filemode.Empty.Bytes(), hashB(0),
|
||||||
|
filemode.Regular.Bytes(), hashB(1)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
descr: "files added",
|
||||||
|
msg: "a",
|
||||||
|
changedFiles: []ChangedFile{
|
||||||
|
{
|
||||||
|
Path: "foo",
|
||||||
|
ToMode: filemode.Regular, ToHash: hash(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "somedir/bar",
|
||||||
|
ToMode: filemode.Executable, ToHash: hash(2),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exp: [][]byte{uvarint(1), []byte("a"), uvarint(2),
|
||||||
|
uvarint(3), []byte("foo"),
|
||||||
|
filemode.Empty.Bytes(), hashB(0),
|
||||||
|
filemode.Regular.Bytes(), hashB(1),
|
||||||
|
uvarint(11), []byte("somedir/bar"),
|
||||||
|
filemode.Empty.Bytes(), hashB(0),
|
||||||
|
filemode.Executable.Bytes(), hashB(2),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
descr: "files added (unordered)",
|
||||||
|
msg: "a",
|
||||||
|
changedFiles: []ChangedFile{
|
||||||
|
{
|
||||||
|
Path: "somedir/bar",
|
||||||
|
ToMode: filemode.Executable, ToHash: hash(2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "foo",
|
||||||
|
ToMode: filemode.Regular, ToHash: hash(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exp: [][]byte{uvarint(1), []byte("a"), uvarint(2),
|
||||||
|
uvarint(3), []byte("foo"),
|
||||||
|
filemode.Empty.Bytes(), hashB(0),
|
||||||
|
filemode.Regular.Bytes(), hashB(1),
|
||||||
|
uvarint(11), []byte("somedir/bar"),
|
||||||
|
filemode.Empty.Bytes(), hashB(0),
|
||||||
|
filemode.Executable.Bytes(), hashB(2),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
descr: "file modified",
|
||||||
|
msg: "a",
|
||||||
|
changedFiles: []ChangedFile{{
|
||||||
|
Path: "foo",
|
||||||
|
FromMode: filemode.Regular, FromHash: hash(1),
|
||||||
|
ToMode: filemode.Executable, ToHash: hash(2),
|
||||||
|
}},
|
||||||
|
exp: [][]byte{uvarint(1), []byte("a"), uvarint(1),
|
||||||
|
uvarint(3), []byte("foo"),
|
||||||
|
filemode.Regular.Bytes(), hashB(1),
|
||||||
|
filemode.Executable.Bytes(), hashB(2),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
descr: "file removed",
|
||||||
|
msg: "a",
|
||||||
|
changedFiles: []ChangedFile{{
|
||||||
|
Path: "foo",
|
||||||
|
FromMode: filemode.Regular, FromHash: hash(1),
|
||||||
|
}},
|
||||||
|
exp: [][]byte{uvarint(1), []byte("a"), uvarint(1),
|
||||||
|
uvarint(3), []byte("foo"),
|
||||||
|
filemode.Regular.Bytes(), hashB(1),
|
||||||
|
filemode.Empty.Bytes(), hashB(0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
descr: "files added, modified, and removed",
|
||||||
|
msg: "aaa",
|
||||||
|
changedFiles: []ChangedFile{
|
||||||
|
{
|
||||||
|
Path: "foo",
|
||||||
|
ToMode: filemode.Regular, ToHash: hash(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "bar",
|
||||||
|
FromMode: filemode.Regular, FromHash: hash(2),
|
||||||
|
ToMode: filemode.Regular, ToHash: hash(3),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "baz",
|
||||||
|
FromMode: filemode.Executable, FromHash: hash(4),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exp: [][]byte{uvarint(3), []byte("aaa"), uvarint(3),
|
||||||
|
uvarint(3), []byte("bar"),
|
||||||
|
filemode.Regular.Bytes(), hashB(2),
|
||||||
|
filemode.Regular.Bytes(), hashB(3),
|
||||||
|
uvarint(3), []byte("baz"),
|
||||||
|
filemode.Executable.Bytes(), hashB(4),
|
||||||
|
filemode.Empty.Bytes(), hashB(0),
|
||||||
|
uvarint(3), []byte("foo"),
|
||||||
|
filemode.Empty.Bytes(), hashB(0),
|
||||||
|
filemode.Regular.Bytes(), hashB(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.descr, func(t *testing.T) {
|
||||||
|
th := new(testHash)
|
||||||
|
genChangeHash(th, test.msg, test.changedFiles)
|
||||||
|
th.assertContents(t, test.exp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user