1package bug
2
3import (
4 "crypto/sha256"
5 "errors"
6 "fmt"
7 "github.com/MichaelMure/git-bug/repository"
8 "github.com/MichaelMure/git-bug/util"
9 "github.com/kevinburke/go.uuid"
10 "strings"
11)
12
13const BugsRefPattern = "refs/bugs/"
14const BugsRemoteRefPattern = "refs/remote/%s/bugs/"
15const OpsEntryName = "ops"
16const RootEntryName = "root"
17
18// Bug hold the data of a bug thread, organized in a way close to
19// how it will be persisted inside Git. This is the datastructure
20// used for merge of two different version.
21type Bug struct {
22 // Id used as unique identifier
23 id string
24
25 lastCommit util.Hash
26 root util.Hash
27
28 // TODO: need a way to order bugs, probably a Lamport clock
29
30 packs []OperationPack
31
32 staging OperationPack
33}
34
35// Create a new Bug
36func NewBug() (*Bug, error) {
37 // TODO: replace with commit hash of (first commit + some random)
38
39 // Creating UUID Version 4
40 unique, err := uuid.ID4()
41
42 if err != nil {
43 return nil, err
44 }
45
46 // Use it as source of uniqueness
47 hash := sha256.New().Sum(unique.Bytes())
48
49 // format in hex and truncate to 40 char
50 id := fmt.Sprintf("%.40s", fmt.Sprintf("%x", hash))
51
52 return &Bug{
53 id: id,
54 }, nil
55}
56
57// Find an existing Bug matching a prefix
58func FindBug(repo repository.Repo, prefix string) (*Bug, error) {
59 ids, err := repo.ListRefs(BugsRefPattern)
60
61 if err != nil {
62 return nil, err
63 }
64
65 // preallocate but empty
66 matching := make([]string, 0, 5)
67
68 for _, id := range ids {
69 if strings.HasPrefix(id, prefix) {
70 matching = append(matching, id)
71 }
72 }
73
74 if len(matching) == 0 {
75 return nil, errors.New("No matching bug found.")
76 }
77
78 if len(matching) > 1 {
79 return nil, fmt.Errorf("Multiple matching bug found:\n%s", strings.Join(matching, "\n"))
80 }
81
82 return ReadBug(repo, BugsRefPattern+matching[0])
83}
84
85// Read and parse a Bug from git
86func ReadBug(repo repository.Repo, ref string) (*Bug, error) {
87 hashes, err := repo.ListCommits(ref)
88
89 if err != nil {
90 return nil, err
91 }
92
93 refSplitted := strings.Split(ref, "/")
94 id := refSplitted[len(refSplitted)-1]
95
96 bug := Bug{
97 id: id,
98 }
99
100 // Load each OperationPack
101 for _, hash := range hashes {
102 entries, err := repo.ListEntries(hash)
103
104 bug.lastCommit = hash
105
106 if err != nil {
107 return nil, err
108 }
109
110 var opsEntry repository.TreeEntry
111 opsFound := false
112 var rootEntry repository.TreeEntry
113 rootFound := false
114
115 for _, entry := range entries {
116 if entry.Name == OpsEntryName {
117 opsEntry = entry
118 opsFound = true
119 continue
120 }
121 if entry.Name == RootEntryName {
122 rootEntry = entry
123 rootFound = true
124 }
125 }
126
127 if !opsFound {
128 return nil, errors.New("Invalid tree, missing the ops entry")
129 }
130
131 if !rootFound {
132 return nil, errors.New("Invalid tree, missing the root entry")
133 }
134
135 if bug.root == "" {
136 bug.root = rootEntry.Hash
137 }
138
139 data, err := repo.ReadData(opsEntry.Hash)
140
141 if err != nil {
142 return nil, err
143 }
144
145 op, err := ParseOperationPack(data)
146
147 if err != nil {
148 return nil, err
149 }
150
151 // tag the pack with the commit hash
152 op.commitHash = hash
153
154 if err != nil {
155 return nil, err
156 }
157
158 bug.packs = append(bug.packs, *op)
159 }
160
161 return &bug, nil
162}
163
164// IsValid check if the Bug data is valid
165func (bug *Bug) IsValid() bool {
166 // non-empty
167 if len(bug.packs) == 0 && bug.staging.IsEmpty() {
168 return false
169 }
170
171 // check if each pack is valid
172 for _, pack := range bug.packs {
173 if !pack.IsValid() {
174 return false
175 }
176 }
177
178 // check if staging is valid if needed
179 if !bug.staging.IsEmpty() {
180 if !bug.staging.IsValid() {
181 return false
182 }
183 }
184
185 // The very first Op should be a CREATE
186 firstOp := bug.firstOp()
187 if firstOp == nil || firstOp.OpType() != CREATE {
188 return false
189 }
190
191 // Check that there is no more CREATE op
192 it := NewOperationIterator(bug)
193 createCount := 0
194 for it.Next() {
195 if it.Value().OpType() == CREATE {
196 createCount++
197 }
198 }
199
200 if createCount != 1 {
201 return false
202 }
203
204 return true
205}
206
207func (bug *Bug) Append(op Operation) {
208 bug.staging.Append(op)
209}
210
211// Write the staging area in Git and move the operations to the packs
212func (bug *Bug) Commit(repo repository.Repo) error {
213 if bug.staging.IsEmpty() {
214 return nil
215 }
216
217 // Write the Ops as a Git blob containing the serialized array
218 hash, err := bug.staging.Write(repo)
219 if err != nil {
220 return err
221 }
222
223 root := bug.root
224 if root == "" {
225 root = hash
226 bug.root = hash
227 }
228
229 // Write a Git tree referencing this blob
230 hash, err = repo.StoreTree([]repository.TreeEntry{
231 {repository.Blob, hash, OpsEntryName}, // the last pack of ops
232 {repository.Blob, root, RootEntryName}, // always the first pack of ops (might be the same)
233 })
234 if err != nil {
235 return err
236 }
237
238 // Write a Git commit referencing the tree, with the previous commit as parent
239 if bug.lastCommit != "" {
240 hash, err = repo.StoreCommitWithParent(hash, bug.lastCommit)
241 } else {
242 hash, err = repo.StoreCommit(hash)
243 }
244
245 if err != nil {
246 return err
247 }
248
249 bug.lastCommit = hash
250
251 // Create or update the Git reference for this bug
252 ref := fmt.Sprintf("%s%s", BugsRefPattern, bug.id)
253 err = repo.UpdateRef(ref, hash)
254
255 if err != nil {
256 return err
257 }
258
259 bug.packs = append(bug.packs, bug.staging)
260 bug.staging = OperationPack{}
261
262 return nil
263}
264
265// Merge a different version of the same bug by rebasing operations of this bug
266// that are not present in the other on top of the chain of operations of the
267// other version.
268func (bug *Bug) Merge(repo repository.Repo, other *Bug) (bool, error) {
269
270 if bug.id != other.id {
271 return false, errors.New("merging unrelated bugs is not supported")
272 }
273
274 if len(other.staging.Operations) > 0 {
275 return false, errors.New("merging a bug with a non-empty staging is not supported")
276 }
277
278 if bug.lastCommit == "" || other.lastCommit == "" {
279 return false, errors.New("can't merge a bug that has never been stored")
280 }
281
282 ancestor, err := repo.FindCommonAncestor(bug.lastCommit, other.lastCommit)
283
284 if err != nil {
285 return false, err
286 }
287
288 rebaseStarted := false
289 updated := false
290
291 for i, pack := range bug.packs {
292 if pack.commitHash == ancestor {
293 rebaseStarted = true
294
295 // get other bug's extra pack
296 for j := i + 1; j < len(other.packs); j++ {
297 // clone is probably not necessary
298 newPack := other.packs[j].Clone()
299
300 bug.packs = append(bug.packs, newPack)
301 bug.lastCommit = newPack.commitHash
302 updated = true
303 }
304
305 continue
306 }
307
308 if !rebaseStarted {
309 continue
310 }
311
312 updated = true
313
314 // get the referenced git tree
315 treeHash, err := repo.GetTreeHash(pack.commitHash)
316
317 if err != nil {
318 return false, err
319 }
320
321 // create a new commit with the correct ancestor
322 hash, err := repo.StoreCommitWithParent(treeHash, bug.lastCommit)
323
324 // replace the pack
325 bug.packs[i] = pack.Clone()
326 bug.packs[i].commitHash = hash
327
328 // update the bug
329 bug.lastCommit = hash
330 }
331
332 // Update the git ref
333 if updated {
334 err := repo.UpdateRef(BugsRefPattern+bug.id, bug.lastCommit)
335 if err != nil {
336 return false, err
337 }
338 }
339
340 return updated, nil
341}
342
343// Return the Bug identifier
344func (bug *Bug) Id() string {
345 return bug.id
346}
347
348// Return the Bug identifier truncated for human consumption
349func (bug *Bug) HumanId() string {
350 return fmt.Sprintf("%.8s", bug.id)
351}
352
353// Lookup for the very first operation of the bug.
354// For a valid Bug, this operation should be a CREATE
355func (bug *Bug) firstOp() Operation {
356 for _, pack := range bug.packs {
357 for _, op := range pack.Operations {
358 return op
359 }
360 }
361
362 if !bug.staging.IsEmpty() {
363 return bug.staging.Operations[0]
364 }
365
366 return nil
367}
368
369// Compile a bug in a easily usable snapshot
370func (bug *Bug) Compile() Snapshot {
371 snap := Snapshot{}
372
373 it := NewOperationIterator(bug)
374
375 for it.Next() {
376 snap = it.Value().Apply(snap)
377 }
378
379 return snap
380}