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:
mediocregopher 2020-04-18 13:26:32 -06:00
parent 1f892070bd
commit 84399603cf
8 changed files with 349 additions and 94 deletions

View File

@ -22,23 +22,22 @@ import (
)
// 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 {
// 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
// necessary.
MessageHead(CommitCommon) (string, error)
// Hash returns the raw hash which Signifiers can sign to accredit this
// commit. The tree objects given describe the filesystem state of the
// parent commit, and the filesystem state of this commit.
//
// This method should _not_ change any fields on the commit.
Hash(parent, this *object.Tree) ([]byte, error)
// ExpectedHash returns the raw hash which Signifiers can sign to accredit
// this commit. The ChangedFile objects given describe the file changes
// between the parent commit and this commit.
ExpectedHash([]ChangedFile) ([]byte, error)
// GetHash returns the signable Hash embedded in the commit, which should
// hopefully correspond to the Commit's Credentials.
GetHash() []byte
// StoredHash returns the signable Hash embedded in the commit, which should
// hopefully correspond to the ExpectedHash.
StoredHash() []byte
}
// 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)
}
cred, err := sigInt.Sign(headFS, commitInt.GetHash())
cred, err := sigInt.Sign(headFS, commitInt.StoredHash())
if err != nil {
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
filesChanged, err := calcDiff(parentTree, gitCommit.GitTree)
changedFiles, err := ChangedFilesBetweenTrees(parentTree, gitCommit.GitTree)
if err != nil {
return fmt.Errorf("calculating diff from tree %q to tree %q: %w",
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")
}
pathsChanged := make([]string, len(filesChanged))
for i := range filesChanged {
pathsChanged[i] = filesChanged[i].path
pathsChanged := make([]string, len(changedFiles))
for i := range changedFiles {
pathsChanged[i] = changedFiles[i].Path
}
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
commitHash := gitCommit.Interface.GetHash()
expectedCommitHash, err := gitCommit.Interface.Hash(parentTree, gitCommit.GitTree)
storedCommitHash := gitCommit.Interface.StoredHash()
expectedCommitHash, err := gitCommit.Interface.ExpectedHash(changedFiles)
if err != nil {
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",
base64.StdEncoding.EncodeToString(expectedCommitHash),
base64.StdEncoding.EncodeToString(commitHash))
base64.StdEncoding.EncodeToString(storedCommitHash),
base64.StdEncoding.EncodeToString(expectedCommitHash))
}
// verify all credentials
@ -485,6 +484,13 @@ func (r *Repo) changeRangeInfo(commits []GitCommit) (changeRangeInfo, error) {
lastChangeCommit := info.changeCommits[len(info.changeCommits)-1]
info.msg = lastChangeCommit.Commit.Change.Message
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
}

View File

@ -2,14 +2,15 @@ package dehub
import (
"bytes"
"dehub.dev/src/dehub.git/fs"
"dehub.dev/src/dehub.git/sigcred"
"dehub.dev/src/dehub.git/yamlutil"
"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"
)
@ -38,8 +39,13 @@ func (r *Repo) NewCommitChange(msg string) (Commit, error) {
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.Hash(headTree, stagedTree); err != nil {
if cc.ChangeHash, err = cc.ExpectedHash(changedFiles); err != nil {
return Commit{}, err
}
@ -53,13 +59,13 @@ func (cc CommitChange) MessageHead(CommitCommon) (string, error) {
return abbrevCommitMessage(cc.Message), nil
}
// Hash implements the method for the CommitInterface interface.
func (cc CommitChange) Hash(parent, this *object.Tree) ([]byte, error) {
return genChangeHash(nil, cc.Message, parent, this), nil
// ExpectedHash implements the method for the CommitInterface interface.
func (cc CommitChange) ExpectedHash(changedFiles []ChangedFile) ([]byte, error) {
return genChangeHash(nil, cc.Message, changedFiles), nil
}
// GetHash implements the method for the CommitInterface interface.
func (cc CommitChange) GetHash() []byte {
// StoredHash implements the method for the CommitInterface interface.
func (cc CommitChange) StoredHash() []byte {
return cc.ChangeHash
}
@ -96,17 +102,23 @@ func (r *Repo) CombineCommitChanges(commits []GitCommit, onto plumbing.Reference
if err != nil {
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) {
// TODO figure out what files to show as being the "problem files" in
// 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
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...)
}
}

View File

@ -141,7 +141,7 @@ func TestCombineCommitChanges(t *testing.T) {
fooCommit := h.assertCommitChange(true, "add foo file", rootSig)
// 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 {
t.Fatal(err)
}

View File

@ -1,12 +1,11 @@
package dehub
import (
"errors"
"fmt"
"strings"
"dehub.dev/src/dehub.git/yamlutil"
"gopkg.in/src-d/go-git.v4/plumbing/object"
)
// CommitComment describes the structure of a comment commit message.
@ -17,13 +16,13 @@ type CommitComment struct {
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
// Commit will _not_ be filled in.
func (r *Repo) NewCommitComment(msg string) (Commit, error) {
cc := CommitComment{Message: msg}
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{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
}
// Hash implements the method for the CommitInterface.
func (cc CommitComment) Hash(_, _ *object.Tree) ([]byte, error) {
// ExpectedHash implements the method for the CommitInterface.
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
}
// GetHash implements the method for the CommitInterface.
func (cc CommitComment) GetHash() []byte {
// StoredHash implements the method for the CommitInterface.
func (cc CommitComment) StoredHash() []byte {
return cc.MessageHash
}

View File

@ -2,12 +2,11 @@ package dehub
import (
"encoding/base64"
"errors"
"fmt"
"strings"
"dehub.dev/src/dehub.git/yamlutil"
"gopkg.in/src-d/go-git.v4/plumbing/object"
)
// CommitCredential describes the structure of a credential commit message.
@ -33,7 +32,7 @@ func (r *Repo) NewCommitCredential(hash []byte) (Commit, error) {
}, 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
// message of the last change commit in the range is used when generating the
// 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
}
// Hash implements the method for the CommitInterface.
func (cc CommitCredential) Hash(_, _ *object.Tree) ([]byte, error) {
// ExpectedHash implements the method for the CommitInterface.
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
}
// GetHash implements the method for the CommitInterface.
func (cc CommitCredential) GetHash() []byte {
// StoredHash implements the method for the CommitInterface.
func (cc CommitCredential) StoredHash() []byte {
return cc.CredentialedHash
}

36
diff.go
View File

@ -8,34 +8,38 @@ import (
"gopkg.in/src-d/go-git.v4/plumbing/object"
)
type fileChanged struct {
path string
fromMode, toMode filemode.FileMode
fromHash, toHash plumbing.Hash
// ChangedFile describes a single file which has been changed in some way
// between two object.Trees. If the From fields are empty then the file was
// created, if the To fields are empty then the file was deleted.
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)
if err != nil {
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 {
if from := change.From; from.Name != "" {
filesChanged[i].path = from.Name
filesChanged[i].fromMode = from.TreeEntry.Mode
filesChanged[i].fromHash = from.TreeEntry.Hash
changedFiles[i].Path = from.Name
changedFiles[i].FromMode = from.TreeEntry.Mode
changedFiles[i].FromHash = from.TreeEntry.Hash
}
if to := change.To; to.Name != "" {
if exPath := filesChanged[i].path; exPath != "" && exPath != to.Name {
panic(fmt.Sprintf("DiffTree entry changed path from %q to %q", exPath, to.Name))
if exPath := changedFiles[i].Path; exPath != "" && exPath != to.Name {
panic(fmt.Sprintf("unexpected changed path from %q to %q", exPath, to.Name))
}
filesChanged[i].path = to.Name
filesChanged[i].toMode = to.TreeEntry.Mode
filesChanged[i].toHash = to.TreeEntry.Hash
changedFiles[i].Path = to.Name
changedFiles[i].ToMode = to.TreeEntry.Mode
changedFiles[i].ToHash = to.TreeEntry.Hash
}
}
return filesChanged, nil
return changedFiles, nil
}

46
hash.go
View File

@ -6,8 +6,6 @@ import (
"fmt"
"hash"
"sort"
"gopkg.in/src-d/go-git.v4/plumbing/object"
)
var (
@ -15,7 +13,7 @@ var (
)
type hashHelper struct {
hash.Hash
hash hash.Hash
varintBuf []byte
}
@ -25,49 +23,43 @@ func newHashHelper(h hash.Hash) *hashHelper {
h = defaultHashHelperAlgo()
}
s := &hashHelper{
Hash: h,
hash: h,
varintBuf: make([]byte, binary.MaxVarintLen64),
}
return s
}
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)
return s.Hash.Sum(out)
return s.hash.Sum(out)
}
func (s *hashHelper) writeUint(i uint64) {
n := binary.PutUvarint(s.varintBuf, i)
if _, err := s.Write(s.varintBuf[:n]); err != nil {
panic(fmt.Sprintf("error writing %x to sha256 sum: %v", s.varintBuf[:n], err))
if _, err := s.hash.Write(s.varintBuf[:n]); err != nil {
panic(fmt.Sprintf("error writing %x to %T: %v", s.varintBuf[:n], s.hash, err))
}
}
func (s *hashHelper) writeStr(str string) {
s.writeUint(uint64(len(str)))
s.Write([]byte(str))
s.hash.Write([]byte(str))
}
func (s *hashHelper) writeTreeDiff(from, to *object.Tree) {
filesChanged, err := calcDiff(from, to)
if err != nil {
panic(err.Error())
}
sort.Slice(filesChanged, func(i, j int) bool {
return filesChanged[i].path < filesChanged[j].path
func (s *hashHelper) writeChangedFiles(changedFiles []ChangedFile) {
sort.Slice(changedFiles, func(i, j int) bool {
return changedFiles[i].Path < changedFiles[j].Path
})
s.writeUint(uint64(len(filesChanged)))
for _, fileChanged := range filesChanged {
s.writeStr(fileChanged.path)
s.Write(fileChanged.fromMode.Bytes())
s.Write(fileChanged.fromHash[:])
s.Write(fileChanged.toMode.Bytes())
s.Write(fileChanged.toHash[:])
s.writeUint(uint64(len(changedFiles)))
for _, fileChanged := range changedFiles {
s.writeStr(fileChanged.Path)
s.hash.Write(fileChanged.FromMode.Bytes())
s.hash.Write(fileChanged.FromHash[:])
s.hash.Write(fileChanged.ToMode.Bytes())
s.hash.Write(fileChanged.ToHash[:])
}
}
var (
@ -76,10 +68,10 @@ var (
)
// 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.writeStr(msg)
s.writeTreeDiff(from, to)
s.writeChangedFiles(changedFiles)
return s.sum(changeHashVersion)
}

237
hash_test.go Normal file
View 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)
})
}
}