package entity
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/pkg/errors"
"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-"
type operationPack struct {
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) write(def Definition, repo repository.RepoData) (repository.Hash, error) {
// 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
data, err := json.Marshal(struct {
Operations []Operation `json:"ops"`
}{
Operations: opp.Operations,
})
if err != nil {
return "", err
}
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
return repo.StoreTree(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.
func readOperationPack(def Definition, repo repository.RepoData, treeHash repository.Hash) (*operationPack, error) {
entries, err := repo.ReadTree(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, NewErrUnknowFormat(def.formatVersion)
}
if version != def.formatVersion {
return nil, NewErrInvalidFormat(version, def.formatVersion)
}
var ops []Operation
var createTime lamport.Time
var editTime lamport.Time
var packTime lamport.Time
for _, entry := range entries {
if entry.Name == opsEntryName {
data, err := repo.ReadData(entry.Hash)
if err != nil {
return nil, errors.Wrap(err, "failed to read git blob data")
}
ops, err = unmarshallOperations(def, data)
if err != nil {
return nil, err
}
continue
}
if 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)
continue
}
if 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)
continue
}
if strings.HasPrefix(entry.Name, packClockEntryPrefix) {
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)
continue
}
}
return &operationPack{
Operations: ops,
CreateTime: createTime,
EditTime: editTime,
PackTime: packTime,
}, nil
}
// unmarshallOperations 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 unmarshallOperations(def Definition, data []byte) ([]Operation, error) {
aux := struct {
Operations []json.RawMessage `json:"ops"`
}{}
if err := json.Unmarshal(data, &aux); err != nil {
return nil, err
}
ops := make([]Operation, 0, len(aux.Operations))
for _, raw := range aux.Operations {
// delegate to specialized unmarshal function
op, err := def.operationUnmarshaler(raw)
if err != nil {
return nil, err
}
ops = append(ops, op)
}
return ops, nil
}