From 8d63c983c982f93cc48d3996d6bd097ddeeb327f Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 3 Jan 2021 23:59:25 +0100 Subject: WIP --- entity/dag/operation_pack.go | 294 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 entity/dag/operation_pack.go (limited to 'entity/dag/operation_pack.go') diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go new file mode 100644 index 00000000..7cf4ee58 --- /dev/null +++ b/entity/dag/operation_pack.go @@ -0,0 +1,294 @@ +package dag + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/pkg/errors" + "golang.org/x/crypto/openpgp" + + "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/lamport" +) + +// TODO: extra data tree +const extraEntryName = "extra" + +const opsEntryName = "ops" +const versionEntryPrefix = "version-" +const createClockEntryPrefix = "create-clock-" +const editClockEntryPrefix = "edit-clock-" +const packClockEntryPrefix = "pack-clock-" + +// operationPack is a wrapper structure to store multiple operations in a single git blob. +// Additionally, it holds and store the metadata for those operations. +type operationPack struct { + // An identifier, taken from a hash of the serialized Operations. + id entity.Id + + // The author of the Operations. Must be the same author for all the Operations. + Author identity.Interface + // The list of Operation stored in the operationPack + Operations []Operation + // Encode the entity's logical time of creation across all entities of the same type. + // Only exist on the root operationPack + CreateTime lamport.Time + // Encode the entity's logical time of last edition across all entities of the same type. + // Exist on all operationPack + EditTime lamport.Time + // // Encode the operationPack's logical time of creation withing this entity. + // // Exist on all operationPack + // PackTime lamport.Time +} + +func (opp *operationPack) Id() entity.Id { + if opp.id == "" || opp.id == entity.UnsetId { + // This means we are trying to get the opp's Id *before* it has been stored. + // As the Id is computed based on the actual bytes written on the disk, we are going to predict + // those and then get the Id. This is safe as it will be the exact same code writing on disk later. + + data, err := json.Marshal(opp) + if err != nil { + panic(err) + } + opp.id = entity.DeriveId(data) + } + + return opp.id +} + +func (opp *operationPack) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Author identity.Interface `json:"author"` + Operations []Operation `json:"ops"` + }{ + Author: opp.Author, + Operations: opp.Operations, + }) +} + +func (opp *operationPack) Validate() error { + if opp.Author == nil { + return fmt.Errorf("missing author") + } + for _, op := range opp.Operations { + if op.Author() != opp.Author { + return fmt.Errorf("operation has different author than the operationPack's") + } + } + if opp.EditTime == 0 { + return fmt.Errorf("lamport edit time is zero") + } + return nil +} + +func (opp *operationPack) Write(def Definition, repo repository.RepoData, parentCommit ...repository.Hash) (repository.Hash, error) { + if err := opp.Validate(); err != nil { + return "", err + } + + // For different reason, we store the clocks and format version directly in the git tree. + // Version has to be accessible before any attempt to decode to return early with a unique error. + // Clocks could possibly be stored in the git blob but it's nice to separate data and metadata, and + // we are storing something directly in the tree already so why not. + // + // To have a valid Tree, we point the "fake" entries to always the same value, the empty blob. + emptyBlobHash, err := repo.StoreData([]byte{}) + if err != nil { + return "", err + } + + // Write the Ops as a Git blob containing the serialized array of operations + data, err := json.Marshal(opp) + if err != nil { + return "", err + } + + // compute the Id while we have the serialized data + opp.id = entity.DeriveId(data) + + hash, err := repo.StoreData(data) + if err != nil { + return "", err + } + + // Make a Git tree referencing this blob and encoding the other values: + // - format version + // - clocks + tree := []repository.TreeEntry{ + {ObjectType: repository.Blob, Hash: emptyBlobHash, + Name: fmt.Sprintf(versionEntryPrefix+"%d", def.formatVersion)}, + {ObjectType: repository.Blob, Hash: hash, + Name: opsEntryName}, + {ObjectType: repository.Blob, Hash: emptyBlobHash, + Name: fmt.Sprintf(editClockEntryPrefix+"%d", opp.EditTime)}, + // {ObjectType: repository.Blob, Hash: emptyBlobHash, + // Name: fmt.Sprintf(packClockEntryPrefix+"%d", opp.PackTime)}, + } + if opp.CreateTime > 0 { + tree = append(tree, repository.TreeEntry{ + ObjectType: repository.Blob, + Hash: emptyBlobHash, + Name: fmt.Sprintf(createClockEntryPrefix+"%d", opp.CreateTime), + }) + } + + // Store the tree + treeHash, err := repo.StoreTree(tree) + if err != nil { + return "", err + } + + // Write a Git commit referencing the tree, with the previous commit as parent + // If we have keys, sign. + var commitHash repository.Hash + + // Sign the commit if we have a key + if opp.Author.SigningKey() != nil { + commitHash, err = repo.StoreSignedCommit(treeHash, opp.Author.SigningKey().PGPEntity(), parentCommit...) + } else { + commitHash, err = repo.StoreCommit(treeHash, parentCommit...) + } + + if err != nil { + return "", err + } + + return commitHash, nil +} + +// readOperationPack read the operationPack encoded in git at the given Tree hash. +// +// Validity of the Lamport clocks is left for the caller to decide. +func readOperationPack(def Definition, repo repository.RepoData, commit repository.Commit) (*operationPack, error) { + entries, err := repo.ReadTree(commit.TreeHash) + if err != nil { + return nil, err + } + + // check the format version first, fail early instead of trying to read something + var version uint + for _, entry := range entries { + if strings.HasPrefix(entry.Name, versionEntryPrefix) { + v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, versionEntryPrefix), 10, 64) + if err != nil { + return nil, errors.Wrap(err, "can't read format version") + } + if v > 1<<12 { + return nil, fmt.Errorf("format version too big") + } + version = uint(v) + break + } + } + if version == 0 { + return nil, entity.NewErrUnknowFormat(def.formatVersion) + } + if version != def.formatVersion { + return nil, entity.NewErrInvalidFormat(version, def.formatVersion) + } + + var id entity.Id + var author identity.Interface + var ops []Operation + var createTime lamport.Time + var editTime lamport.Time + // var packTime lamport.Time + + for _, entry := range entries { + switch { + case entry.Name == opsEntryName: + data, err := repo.ReadData(entry.Hash) + if err != nil { + return nil, errors.Wrap(err, "failed to read git blob data") + } + ops, author, err = unmarshallPack(def, data) + if err != nil { + return nil, err + } + id = entity.DeriveId(data) + + case strings.HasPrefix(entry.Name, createClockEntryPrefix): + v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, createClockEntryPrefix), 10, 64) + if err != nil { + return nil, errors.Wrap(err, "can't read creation lamport time") + } + createTime = lamport.Time(v) + + case strings.HasPrefix(entry.Name, editClockEntryPrefix): + v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, editClockEntryPrefix), 10, 64) + if err != nil { + return nil, errors.Wrap(err, "can't read edit lamport time") + } + editTime = lamport.Time(v) + + // case strings.HasPrefix(entry.Name, packClockEntryPrefix): + // found &= 1 << 3 + // + // v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, packClockEntryPrefix), 10, 64) + // if err != nil { + // return nil, errors.Wrap(err, "can't read pack lamport time") + // } + // packTime = lamport.Time(v) + } + } + + // Verify signature if we expect one + keys := author.ValidKeysAtTime(fmt.Sprintf(editClockPattern, def.namespace), editTime) + if len(keys) > 0 { + keyring := identity.PGPKeyring(keys) + _, err = openpgp.CheckDetachedSignature(keyring, commit.SignedData, commit.Signature) + if err != nil { + return nil, fmt.Errorf("signature failure: %v", err) + } + } + + return &operationPack{ + id: id, + Author: author, + Operations: ops, + CreateTime: createTime, + EditTime: editTime, + // PackTime: packTime, + }, nil +} + +// unmarshallPack delegate the unmarshalling of the Operation's JSON to the decoding +// function provided by the concrete entity. This gives access to the concrete type of each +// Operation. +func unmarshallPack(def Definition, data []byte) ([]Operation, identity.Interface, error) { + aux := struct { + Author identity.IdentityStub `json:"author"` + Operations []json.RawMessage `json:"ops"` + }{} + + if err := json.Unmarshal(data, &aux); err != nil { + return nil, nil, err + } + + if aux.Author.Id() == "" || aux.Author.Id() == entity.UnsetId { + return nil, nil, fmt.Errorf("missing author") + } + + author, err := def.identityResolver.ResolveIdentity(aux.Author.Id()) + if err != nil { + return nil, nil, err + } + + ops := make([]Operation, 0, len(aux.Operations)) + + for _, raw := range aux.Operations { + // delegate to specialized unmarshal function + op, err := def.operationUnmarshaler(author, raw) + if err != nil { + return nil, nil, err + } + ops = append(ops, op) + } + + return ops, author, nil +} -- cgit From dc5059bc3372941e2908739831188768335ac50b Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 24 Jan 2021 19:45:21 +0100 Subject: entity: more progress on merging and signing --- entity/dag/operation_pack.go | 50 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) (limited to 'entity/dag/operation_pack.go') diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go index 7cf4ee58..ebacdbd9 100644 --- a/entity/dag/operation_pack.go +++ b/entity/dag/operation_pack.go @@ -86,7 +86,10 @@ func (opp *operationPack) Validate() error { return nil } -func (opp *operationPack) Write(def Definition, repo repository.RepoData, parentCommit ...repository.Hash) (repository.Hash, error) { +// Write write the OperationPack in git, with zero, one or more parent commits. +// If the repository has a keypair able to sign (that is, with a private key), the resulting commit is signed with that key. +// Return the hash of the created commit. +func (opp *operationPack) Write(def Definition, repo repository.Repo, parentCommit ...repository.Hash) (repository.Hash, error) { if err := opp.Validate(); err != nil { return "", err } @@ -148,8 +151,13 @@ func (opp *operationPack) Write(def Definition, repo repository.RepoData, parent var commitHash repository.Hash // Sign the commit if we have a key - if opp.Author.SigningKey() != nil { - commitHash, err = repo.StoreSignedCommit(treeHash, opp.Author.SigningKey().PGPEntity(), parentCommit...) + signingKey, err := opp.Author.SigningKey(repo) + if err != nil { + return "", err + } + + if signingKey != nil { + commitHash, err = repo.StoreSignedCommit(treeHash, signingKey.PGPEntity(), parentCommit...) } else { commitHash, err = repo.StoreCommit(treeHash, parentCommit...) } @@ -240,7 +248,7 @@ func readOperationPack(def Definition, repo repository.RepoData, commit reposito // Verify signature if we expect one keys := author.ValidKeysAtTime(fmt.Sprintf(editClockPattern, def.namespace), editTime) if len(keys) > 0 { - keyring := identity.PGPKeyring(keys) + keyring := PGPKeyring(keys) _, err = openpgp.CheckDetachedSignature(keyring, commit.SignedData, commit.Signature) if err != nil { return nil, fmt.Errorf("signature failure: %v", err) @@ -292,3 +300,37 @@ func unmarshallPack(def Definition, data []byte) ([]Operation, identity.Interfac return ops, author, nil } + +var _ openpgp.KeyRing = &PGPKeyring{} + +// PGPKeyring implement a openpgp.KeyRing from an slice of Key +type PGPKeyring []*identity.Key + +func (pk PGPKeyring) KeysById(id uint64) []openpgp.Key { + var result []openpgp.Key + for _, key := range pk { + if key.Public().KeyId == id { + result = append(result, openpgp.Key{ + PublicKey: key.Public(), + PrivateKey: key.Private(), + }) + } + } + return result +} + +func (pk PGPKeyring) KeysByIdUsage(id uint64, requiredUsage byte) []openpgp.Key { + // the only usage we care about is the ability to sign, which all keys should already be capable of + return pk.KeysById(id) +} + +func (pk PGPKeyring) DecryptionKeys() []openpgp.Key { + result := make([]openpgp.Key, len(pk)) + for i, key := range pk { + result[i] = openpgp.Key{ + PublicKey: key.Public(), + PrivateKey: key.Private(), + } + } + return result +} -- cgit From f74166914c344329f08823770982f12966c79a77 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Tue, 9 Feb 2021 15:47:07 +0100 Subject: entity: remove the pack lamport time that doesn't bring anything actually --- entity/dag/operation_pack.go | 17 ----------------- 1 file changed, 17 deletions(-) (limited to 'entity/dag/operation_pack.go') diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go index ebacdbd9..959b1ae0 100644 --- a/entity/dag/operation_pack.go +++ b/entity/dag/operation_pack.go @@ -22,7 +22,6 @@ const opsEntryName = "ops" const versionEntryPrefix = "version-" const createClockEntryPrefix = "create-clock-" const editClockEntryPrefix = "edit-clock-" -const packClockEntryPrefix = "pack-clock-" // operationPack is a wrapper structure to store multiple operations in a single git blob. // Additionally, it holds and store the metadata for those operations. @@ -40,9 +39,6 @@ type operationPack struct { // Encode the entity's logical time of last edition across all entities of the same type. // Exist on all operationPack EditTime lamport.Time - // // Encode the operationPack's logical time of creation withing this entity. - // // Exist on all operationPack - // PackTime lamport.Time } func (opp *operationPack) Id() entity.Id { @@ -129,8 +125,6 @@ func (opp *operationPack) Write(def Definition, repo repository.Repo, parentComm Name: opsEntryName}, {ObjectType: repository.Blob, Hash: emptyBlobHash, Name: fmt.Sprintf(editClockEntryPrefix+"%d", opp.EditTime)}, - // {ObjectType: repository.Blob, Hash: emptyBlobHash, - // Name: fmt.Sprintf(packClockEntryPrefix+"%d", opp.PackTime)}, } if opp.CreateTime > 0 { tree = append(tree, repository.TreeEntry{ @@ -205,7 +199,6 @@ func readOperationPack(def Definition, repo repository.RepoData, commit reposito var ops []Operation var createTime lamport.Time var editTime lamport.Time - // var packTime lamport.Time for _, entry := range entries { switch { @@ -233,15 +226,6 @@ func readOperationPack(def Definition, repo repository.RepoData, commit reposito return nil, errors.Wrap(err, "can't read edit lamport time") } editTime = lamport.Time(v) - - // case strings.HasPrefix(entry.Name, packClockEntryPrefix): - // found &= 1 << 3 - // - // v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, packClockEntryPrefix), 10, 64) - // if err != nil { - // return nil, errors.Wrap(err, "can't read pack lamport time") - // } - // packTime = lamport.Time(v) } } @@ -261,7 +245,6 @@ func readOperationPack(def Definition, repo repository.RepoData, commit reposito Operations: ops, CreateTime: createTime, EditTime: editTime, - // PackTime: packTime, }, nil } -- cgit From 94f06cd54defa73f5e8b79345597279e454c78e6 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 14 Feb 2021 10:02:01 +0100 Subject: entity: pass the identity resolver instead of defining it once Having the resolver in Definition doesn't actually work well as the resolver is very situational. --- entity/dag/operation_pack.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'entity/dag/operation_pack.go') diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go index 959b1ae0..d6bce9f2 100644 --- a/entity/dag/operation_pack.go +++ b/entity/dag/operation_pack.go @@ -166,7 +166,7 @@ func (opp *operationPack) Write(def Definition, repo repository.Repo, parentComm // readOperationPack read the operationPack encoded in git at the given Tree hash. // // Validity of the Lamport clocks is left for the caller to decide. -func readOperationPack(def Definition, repo repository.RepoData, commit repository.Commit) (*operationPack, error) { +func readOperationPack(def Definition, repo repository.RepoData, resolver identity.Resolver, commit repository.Commit) (*operationPack, error) { entries, err := repo.ReadTree(commit.TreeHash) if err != nil { return nil, err @@ -207,7 +207,7 @@ func readOperationPack(def Definition, repo repository.RepoData, commit reposito if err != nil { return nil, errors.Wrap(err, "failed to read git blob data") } - ops, author, err = unmarshallPack(def, data) + ops, author, err = unmarshallPack(def, resolver, data) if err != nil { return nil, err } @@ -251,7 +251,7 @@ func readOperationPack(def Definition, repo repository.RepoData, commit reposito // unmarshallPack delegate the unmarshalling of the Operation's JSON to the decoding // function provided by the concrete entity. This gives access to the concrete type of each // Operation. -func unmarshallPack(def Definition, data []byte) ([]Operation, identity.Interface, error) { +func unmarshallPack(def Definition, resolver identity.Resolver, data []byte) ([]Operation, identity.Interface, error) { aux := struct { Author identity.IdentityStub `json:"author"` Operations []json.RawMessage `json:"ops"` @@ -265,7 +265,7 @@ func unmarshallPack(def Definition, data []byte) ([]Operation, identity.Interfac return nil, nil, fmt.Errorf("missing author") } - author, err := def.identityResolver.ResolveIdentity(aux.Author.Id()) + author, err := resolver.ResolveIdentity(aux.Author.Id()) if err != nil { return nil, nil, err } -- cgit From 99b9dd84cb4b0cfd3eb1fd50b07c8b826eb52d19 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 14 Feb 2021 10:06:16 +0100 Subject: entity: support different author in staging operations --- entity/dag/operation_pack.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'entity/dag/operation_pack.go') diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go index d6bce9f2..00cf2557 100644 --- a/entity/dag/operation_pack.go +++ b/entity/dag/operation_pack.go @@ -72,7 +72,7 @@ func (opp *operationPack) Validate() error { return fmt.Errorf("missing author") } for _, op := range opp.Operations { - if op.Author() != opp.Author { + if op.Author().Id() != opp.Author.Id() { return fmt.Errorf("operation has different author than the operationPack's") } } @@ -120,7 +120,7 @@ func (opp *operationPack) Write(def Definition, repo repository.Repo, parentComm // - clocks tree := []repository.TreeEntry{ {ObjectType: repository.Blob, Hash: emptyBlobHash, - Name: fmt.Sprintf(versionEntryPrefix+"%d", def.formatVersion)}, + Name: fmt.Sprintf(versionEntryPrefix+"%d", def.FormatVersion)}, {ObjectType: repository.Blob, Hash: hash, Name: opsEntryName}, {ObjectType: repository.Blob, Hash: emptyBlobHash, @@ -188,10 +188,10 @@ func readOperationPack(def Definition, repo repository.RepoData, resolver identi } } if version == 0 { - return nil, entity.NewErrUnknowFormat(def.formatVersion) + return nil, entity.NewErrUnknowFormat(def.FormatVersion) } - if version != def.formatVersion { - return nil, entity.NewErrInvalidFormat(version, def.formatVersion) + if version != def.FormatVersion { + return nil, entity.NewErrInvalidFormat(version, def.FormatVersion) } var id entity.Id @@ -230,7 +230,7 @@ func readOperationPack(def Definition, repo repository.RepoData, resolver identi } // Verify signature if we expect one - keys := author.ValidKeysAtTime(fmt.Sprintf(editClockPattern, def.namespace), editTime) + keys := author.ValidKeysAtTime(fmt.Sprintf(editClockPattern, def.Namespace), editTime) if len(keys) > 0 { keyring := PGPKeyring(keys) _, err = openpgp.CheckDetachedSignature(keyring, commit.SignedData, commit.Signature) @@ -274,7 +274,7 @@ func unmarshallPack(def Definition, resolver identity.Resolver, data []byte) ([] for _, raw := range aux.Operations { // delegate to specialized unmarshal function - op, err := def.operationUnmarshaler(author, raw) + op, err := def.OperationUnmarshaler(author, raw) if err != nil { return nil, nil, err } -- cgit From 1ced77af1a4bdbaa212a74bf0c56b2b81cdc5bd2 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 14 Feb 2021 12:24:40 +0100 Subject: fix merge --- entity/dag/operation_pack.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'entity/dag/operation_pack.go') diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go index 00cf2557..a436fd33 100644 --- a/entity/dag/operation_pack.go +++ b/entity/dag/operation_pack.go @@ -188,7 +188,7 @@ func readOperationPack(def Definition, repo repository.RepoData, resolver identi } } if version == 0 { - return nil, entity.NewErrUnknowFormat(def.FormatVersion) + return nil, entity.NewErrUnknownFormat(def.FormatVersion) } if version != def.FormatVersion { return nil, entity.NewErrInvalidFormat(version, def.FormatVersion) -- cgit From 214abe4dea1984086e45d1399538fb12aa010642 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sat, 20 Feb 2021 15:48:44 +0100 Subject: WIP operation with files --- entity/dag/operation_pack.go | 45 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) (limited to 'entity/dag/operation_pack.go') diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go index a436fd33..72063c60 100644 --- a/entity/dag/operation_pack.go +++ b/entity/dag/operation_pack.go @@ -15,10 +15,8 @@ import ( "github.com/MichaelMure/git-bug/util/lamport" ) -// TODO: extra data tree -const extraEntryName = "extra" - const opsEntryName = "ops" +const extraEntryName = "extra" const versionEntryPrefix = "version-" const createClockEntryPrefix = "create-clock-" const editClockEntryPrefix = "edit-clock-" @@ -118,6 +116,7 @@ func (opp *operationPack) Write(def Definition, repo repository.Repo, parentComm // Make a Git tree referencing this blob and encoding the other values: // - format version // - clocks + // - extra data tree := []repository.TreeEntry{ {ObjectType: repository.Blob, Hash: emptyBlobHash, Name: fmt.Sprintf(versionEntryPrefix+"%d", def.FormatVersion)}, @@ -133,6 +132,17 @@ func (opp *operationPack) Write(def Definition, repo repository.Repo, parentComm Name: fmt.Sprintf(createClockEntryPrefix+"%d", opp.CreateTime), }) } + if extraTree := opp.makeExtraTree(); len(extraTree) > 0 { + extraTreeHash, err := repo.StoreTree(extraTree) + if err != nil { + return "", err + } + tree = append(tree, repository.TreeEntry{ + ObjectType: repository.Tree, + Hash: extraTreeHash, + Name: extraEntryName, + }) + } // Store the tree treeHash, err := repo.StoreTree(tree) @@ -163,6 +173,35 @@ func (opp *operationPack) Write(def Definition, repo repository.Repo, parentComm return commitHash, nil } +func (opp *operationPack) makeExtraTree() []repository.TreeEntry { + var tree []repository.TreeEntry + counter := 0 + added := make(map[repository.Hash]interface{}) + + for _, ops := range opp.Operations { + ops, ok := ops.(OperationWithFiles) + if !ok { + continue + } + + for _, file := range ops.GetFiles() { + if _, has := added[file]; !has { + tree = append(tree, repository.TreeEntry{ + ObjectType: repository.Blob, + Hash: file, + // The name is not important here, we only need to + // reference the blob. + Name: fmt.Sprintf("file%d", counter), + }) + counter++ + added[file] = struct{}{} + } + } + } + + return tree +} + // readOperationPack read the operationPack encoded in git at the given Tree hash. // // Validity of the Lamport clocks is left for the caller to decide. -- cgit