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}