operation_pack.go

  1package entity
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"strconv"
  7	"strings"
  8
  9	"github.com/pkg/errors"
 10
 11	"github.com/MichaelMure/git-bug/repository"
 12	"github.com/MichaelMure/git-bug/util/lamport"
 13)
 14
 15// TODO: extra data tree
 16const extraEntryName = "extra"
 17
 18const opsEntryName = "ops"
 19const versionEntryPrefix = "version-"
 20const createClockEntryPrefix = "create-clock-"
 21const editClockEntryPrefix = "edit-clock-"
 22const packClockEntryPrefix = "pack-clock-"
 23
 24type operationPack struct {
 25	Operations []Operation
 26	// Encode the entity's logical time of creation across all entities of the same type.
 27	// Only exist on the root operationPack
 28	CreateTime lamport.Time
 29	// Encode the entity's logical time of last edition across all entities of the same type.
 30	// Exist on all operationPack
 31	EditTime lamport.Time
 32	// Encode the operationPack's logical time of creation withing this entity.
 33	// Exist on all operationPack
 34	PackTime lamport.Time
 35}
 36
 37func (opp operationPack) write(def Definition, repo repository.RepoData) (repository.Hash, error) {
 38	// For different reason, we store the clocks and format version directly in the git tree.
 39	// Version has to be accessible before any attempt to decode to return early with a unique error.
 40	// Clocks could possibly be stored in the git blob but it's nice to separate data and metadata, and
 41	// we are storing something directly in the tree already so why not.
 42	//
 43	// To have a valid Tree, we point the "fake" entries to always the same value, the empty blob.
 44	emptyBlobHash, err := repo.StoreData([]byte{})
 45	if err != nil {
 46		return "", err
 47	}
 48
 49	// Write the Ops as a Git blob containing the serialized array
 50	data, err := json.Marshal(struct {
 51		Operations []Operation `json:"ops"`
 52	}{
 53		Operations: opp.Operations,
 54	})
 55	if err != nil {
 56		return "", err
 57	}
 58	hash, err := repo.StoreData(data)
 59	if err != nil {
 60		return "", err
 61	}
 62
 63	// Make a Git tree referencing this blob and encoding the other values:
 64	// - format version
 65	// - clocks
 66	tree := []repository.TreeEntry{
 67		{ObjectType: repository.Blob, Hash: emptyBlobHash,
 68			Name: fmt.Sprintf(versionEntryPrefix+"%d", def.formatVersion)},
 69		{ObjectType: repository.Blob, Hash: hash,
 70			Name: opsEntryName},
 71		{ObjectType: repository.Blob, Hash: emptyBlobHash,
 72			Name: fmt.Sprintf(editClockEntryPrefix+"%d", opp.EditTime)},
 73		{ObjectType: repository.Blob, Hash: emptyBlobHash,
 74			Name: fmt.Sprintf(packClockEntryPrefix+"%d", opp.PackTime)},
 75	}
 76	if opp.CreateTime > 0 {
 77		tree = append(tree, repository.TreeEntry{
 78			ObjectType: repository.Blob,
 79			Hash:       emptyBlobHash,
 80			Name:       fmt.Sprintf(createClockEntryPrefix+"%d", opp.CreateTime),
 81		})
 82	}
 83
 84	// Store the tree
 85	return repo.StoreTree(tree)
 86}
 87
 88// readOperationPack read the operationPack encoded in git at the given Tree hash.
 89//
 90// Validity of the Lamport clocks is left for the caller to decide.
 91func readOperationPack(def Definition, repo repository.RepoData, treeHash repository.Hash) (*operationPack, error) {
 92	entries, err := repo.ReadTree(treeHash)
 93	if err != nil {
 94		return nil, err
 95	}
 96
 97	// check the format version first, fail early instead of trying to read something
 98	var version uint
 99	for _, entry := range entries {
100		if strings.HasPrefix(entry.Name, versionEntryPrefix) {
101			v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, versionEntryPrefix), 10, 64)
102			if err != nil {
103				return nil, errors.Wrap(err, "can't read format version")
104			}
105			if v > 1<<12 {
106				return nil, fmt.Errorf("format version too big")
107			}
108			version = uint(v)
109			break
110		}
111	}
112	if version == 0 {
113		return nil, NewErrUnknowFormat(def.formatVersion)
114	}
115	if version != def.formatVersion {
116		return nil, NewErrInvalidFormat(version, def.formatVersion)
117	}
118
119	var ops []Operation
120	var createTime lamport.Time
121	var editTime lamport.Time
122	var packTime lamport.Time
123
124	for _, entry := range entries {
125		if entry.Name == opsEntryName {
126			data, err := repo.ReadData(entry.Hash)
127			if err != nil {
128				return nil, errors.Wrap(err, "failed to read git blob data")
129			}
130
131			ops, err = unmarshallOperations(def, data)
132			if err != nil {
133				return nil, err
134			}
135			continue
136		}
137
138		if strings.HasPrefix(entry.Name, createClockEntryPrefix) {
139			v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, createClockEntryPrefix), 10, 64)
140			if err != nil {
141				return nil, errors.Wrap(err, "can't read creation lamport time")
142			}
143			createTime = lamport.Time(v)
144			continue
145		}
146
147		if strings.HasPrefix(entry.Name, editClockEntryPrefix) {
148			v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, editClockEntryPrefix), 10, 64)
149			if err != nil {
150				return nil, errors.Wrap(err, "can't read edit lamport time")
151			}
152			editTime = lamport.Time(v)
153			continue
154		}
155
156		if strings.HasPrefix(entry.Name, packClockEntryPrefix) {
157			v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, packClockEntryPrefix), 10, 64)
158			if err != nil {
159				return nil, errors.Wrap(err, "can't read pack lamport time")
160			}
161			packTime = lamport.Time(v)
162			continue
163		}
164	}
165
166	return &operationPack{
167		Operations: ops,
168		CreateTime: createTime,
169		EditTime:   editTime,
170		PackTime:   packTime,
171	}, nil
172}
173
174// unmarshallOperations delegate the unmarshalling of the Operation's JSON to the decoding
175// function provided by the concrete entity. This gives access to the concrete type of each
176// Operation.
177func unmarshallOperations(def Definition, data []byte) ([]Operation, error) {
178	aux := struct {
179		Operations []json.RawMessage `json:"ops"`
180	}{}
181
182	if err := json.Unmarshal(data, &aux); err != nil {
183		return nil, err
184	}
185
186	ops := make([]Operation, 0, len(aux.Operations))
187
188	for _, raw := range aux.Operations {
189		// delegate to specialized unmarshal function
190		op, err := def.operationUnmarshaler(raw)
191		if err != nil {
192			return nil, err
193		}
194
195		ops = append(ops, op)
196	}
197
198	return ops, nil
199}