1// Package bug contains the bug data model and low-level related functions
2package bug
3
4import (
5 "encoding/json"
6 "fmt"
7
8 "github.com/pkg/errors"
9
10 "github.com/MichaelMure/git-bug/entity"
11 "github.com/MichaelMure/git-bug/identity"
12 "github.com/MichaelMure/git-bug/repository"
13 "github.com/MichaelMure/git-bug/util/lamport"
14)
15
16const bugsRefPattern = "refs/bugs/"
17const bugsRemoteRefPattern = "refs/remotes/%s/bugs/"
18
19const opsEntryName = "ops"
20const mediaEntryName = "media"
21
22const createClockEntryPrefix = "create-clock-"
23const createClockEntryPattern = "create-clock-%d"
24const editClockEntryPrefix = "edit-clock-"
25const editClockEntryPattern = "edit-clock-%d"
26
27const creationClockName = "bug-create"
28const editClockName = "bug-edit"
29
30var ErrBugNotExist = errors.New("bug doesn't exist")
31
32func NewErrMultipleMatchBug(matching []entity.Id) *entity.ErrMultipleMatch {
33 return entity.NewErrMultipleMatch("bug", matching)
34}
35
36func NewErrMultipleMatchOp(matching []entity.Id) *entity.ErrMultipleMatch {
37 return entity.NewErrMultipleMatch("operation", matching)
38}
39
40var _ Interface = &Bug{}
41var _ entity.Interface = &Bug{}
42
43// Bug hold the data of a bug thread, organized in a way close to
44// how it will be persisted inside Git. This is the data structure
45// used to merge two different version of the same Bug.
46type Bug struct {
47 // A Lamport clock is a logical clock that allow to order event
48 // inside a distributed system.
49 // It must be the first field in this struct due to https://github.com/golang/go/issues/599
50 createTime lamport.Time
51 editTime lamport.Time
52
53 lastCommit repository.Hash
54
55 // all the committed operations
56 packs []OperationPack
57
58 // a temporary pack of operations used for convenience to pile up new operations
59 // before a commit
60 staging OperationPack
61}
62
63// NewBug create a new Bug
64func NewBug() *Bug {
65 // No logical clock yet
66 return &Bug{}
67}
68
69// ReadLocal will read a local bug from its hash
70func ReadLocal(repo repository.ClockedRepo, id entity.Id) (*Bug, error) {
71 ref := bugsRefPattern + id.String()
72 return read(repo, identity.NewSimpleResolver(repo), ref)
73}
74
75// ReadLocalWithResolver will read a local bug from its hash
76func ReadLocalWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver, id entity.Id) (*Bug, error) {
77 ref := bugsRefPattern + id.String()
78 return read(repo, identityResolver, ref)
79}
80
81// ReadRemote will read a remote bug from its hash
82func ReadRemote(repo repository.ClockedRepo, remote string, id entity.Id) (*Bug, error) {
83 ref := fmt.Sprintf(bugsRemoteRefPattern, remote) + id.String()
84 return read(repo, identity.NewSimpleResolver(repo), ref)
85}
86
87// ReadRemoteWithResolver will read a remote bug from its hash
88func ReadRemoteWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver, remote string, id entity.Id) (*Bug, error) {
89 ref := fmt.Sprintf(bugsRemoteRefPattern, remote) + id.String()
90 return read(repo, identityResolver, ref)
91}
92
93// read will read and parse a Bug from git
94func read(repo repository.ClockedRepo, identityResolver identity.Resolver, ref string) (*Bug, error) {
95 id := entity.RefToId(ref)
96
97 if err := id.Validate(); err != nil {
98 return nil, errors.Wrap(err, "invalid ref ")
99 }
100
101 hashes, err := repo.ListCommits(ref)
102 if err != nil {
103 return nil, ErrBugNotExist
104 }
105 if len(hashes) == 0 {
106 return nil, fmt.Errorf("empty bug")
107 }
108
109 bug := Bug{}
110
111 // Load each OperationPack
112 for _, hash := range hashes {
113 tree, err := readTree(repo, hash)
114 if err != nil {
115 return nil, err
116 }
117
118 // Due to rebase, edit Lamport time are not necessarily ordered
119 if tree.editTime > bug.editTime {
120 bug.editTime = tree.editTime
121 }
122
123 // Update the clocks
124 err = repo.Witness(creationClockName, bug.createTime)
125 if err != nil {
126 return nil, errors.Wrap(err, "failed to update create lamport clock")
127 }
128 err = repo.Witness(editClockName, bug.editTime)
129 if err != nil {
130 return nil, errors.Wrap(err, "failed to update edit lamport clock")
131 }
132
133 data, err := repo.ReadData(tree.opsEntry.Hash)
134 if err != nil {
135 return nil, errors.Wrap(err, "failed to read git blob data")
136 }
137
138 opp := &OperationPack{}
139 err = json.Unmarshal(data, &opp)
140 if err != nil {
141 return nil, errors.Wrap(err, "failed to decode OperationPack json")
142 }
143
144 // tag the pack with the commit hash
145 opp.commitHash = hash
146 bug.lastCommit = hash
147
148 // if it's the first OperationPack read
149 if len(bug.packs) == 0 {
150 bug.createTime = tree.createTime
151 }
152
153 bug.packs = append(bug.packs, *opp)
154 }
155
156 // Bug Id is the Id of the first operation
157 if len(bug.packs[0].Operations) == 0 {
158 return nil, fmt.Errorf("first OperationPack is empty")
159 }
160 if id != bug.packs[0].Operations[0].Id() {
161 return nil, fmt.Errorf("bug ID doesn't match the first operation ID")
162 }
163
164 // Make sure that the identities are properly loaded
165 err = bug.EnsureIdentities(identityResolver)
166 if err != nil {
167 return nil, err
168 }
169
170 return &bug, nil
171}
172
173// RemoveBug will remove a local bug from its entity.Id
174func RemoveBug(repo repository.ClockedRepo, id entity.Id) error {
175 var fullMatches []string
176
177 refs, err := repo.ListRefs(bugsRefPattern + id.String())
178 if err != nil {
179 return err
180 }
181 if len(refs) > 1 {
182 return NewErrMultipleMatchBug(entity.RefsToIds(refs))
183 }
184 if len(refs) == 1 {
185 // we have the bug locally
186 fullMatches = append(fullMatches, refs[0])
187 }
188
189 remotes, err := repo.GetRemotes()
190 if err != nil {
191 return err
192 }
193
194 for remote := range remotes {
195 remotePrefix := fmt.Sprintf(bugsRemoteRefPattern+id.String(), remote)
196 remoteRefs, err := repo.ListRefs(remotePrefix)
197 if err != nil {
198 return err
199 }
200 if len(remoteRefs) > 1 {
201 return NewErrMultipleMatchBug(entity.RefsToIds(refs))
202 }
203 if len(remoteRefs) == 1 {
204 // found the bug in a remote
205 fullMatches = append(fullMatches, remoteRefs[0])
206 }
207 }
208
209 if len(fullMatches) == 0 {
210 return ErrBugNotExist
211 }
212
213 for _, ref := range fullMatches {
214 err = repo.RemoveRef(ref)
215 if err != nil {
216 return err
217 }
218 }
219
220 return nil
221}
222
223type StreamedBug struct {
224 Bug *Bug
225 Err error
226}
227
228// ReadAllLocal read and parse all local bugs
229func ReadAllLocal(repo repository.ClockedRepo) <-chan StreamedBug {
230 return readAll(repo, identity.NewSimpleResolver(repo), bugsRefPattern)
231}
232
233// ReadAllLocalWithResolver read and parse all local bugs
234func ReadAllLocalWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver) <-chan StreamedBug {
235 return readAll(repo, identityResolver, bugsRefPattern)
236}
237
238// ReadAllRemote read and parse all remote bugs for a given remote
239func ReadAllRemote(repo repository.ClockedRepo, remote string) <-chan StreamedBug {
240 refPrefix := fmt.Sprintf(bugsRemoteRefPattern, remote)
241 return readAll(repo, identity.NewSimpleResolver(repo), refPrefix)
242}
243
244// ReadAllRemoteWithResolver read and parse all remote bugs for a given remote
245func ReadAllRemoteWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver, remote string) <-chan StreamedBug {
246 refPrefix := fmt.Sprintf(bugsRemoteRefPattern, remote)
247 return readAll(repo, identityResolver, refPrefix)
248}
249
250// Read and parse all available bug with a given ref prefix
251func readAll(repo repository.ClockedRepo, identityResolver identity.Resolver, refPrefix string) <-chan StreamedBug {
252 out := make(chan StreamedBug)
253
254 go func() {
255 defer close(out)
256
257 refs, err := repo.ListRefs(refPrefix)
258 if err != nil {
259 out <- StreamedBug{Err: err}
260 return
261 }
262
263 for _, ref := range refs {
264 b, err := read(repo, identityResolver, ref)
265
266 if err != nil {
267 out <- StreamedBug{Err: err}
268 return
269 }
270
271 out <- StreamedBug{Bug: b}
272 }
273 }()
274
275 return out
276}
277
278// ListLocalIds list all the available local bug ids
279func ListLocalIds(repo repository.Repo) ([]entity.Id, error) {
280 refs, err := repo.ListRefs(bugsRefPattern)
281 if err != nil {
282 return nil, err
283 }
284
285 return entity.RefsToIds(refs), nil
286}
287
288// Validate check if the Bug data is valid
289func (bug *Bug) Validate() error {
290 // non-empty
291 if len(bug.packs) == 0 && bug.staging.IsEmpty() {
292 return fmt.Errorf("bug has no operations")
293 }
294
295 // check if each pack and operations are valid
296 for _, pack := range bug.packs {
297 if err := pack.Validate(); err != nil {
298 return err
299 }
300 }
301
302 // check if staging is valid if needed
303 if !bug.staging.IsEmpty() {
304 if err := bug.staging.Validate(); err != nil {
305 return errors.Wrap(err, "staging")
306 }
307 }
308
309 // The very first Op should be a CreateOp
310 firstOp := bug.FirstOp()
311 if firstOp == nil || firstOp.base().OperationType != CreateOp {
312 return fmt.Errorf("first operation should be a Create op")
313 }
314
315 // Check that there is no more CreateOp op
316 // Check that there is no colliding operation's ID
317 it := NewOperationIterator(bug)
318 createCount := 0
319 ids := make(map[entity.Id]struct{})
320 for it.Next() {
321 if it.Value().base().OperationType == CreateOp {
322 createCount++
323 }
324 if _, ok := ids[it.Value().Id()]; ok {
325 return fmt.Errorf("id collision: %s", it.Value().Id())
326 }
327 ids[it.Value().Id()] = struct{}{}
328 }
329
330 if createCount != 1 {
331 return fmt.Errorf("only one Create op allowed")
332 }
333
334 return nil
335}
336
337// Append an operation into the staging area, to be committed later
338func (bug *Bug) Append(op Operation) {
339 if len(bug.packs) == 0 && len(bug.staging.Operations) == 0 {
340 if op.base().OperationType != CreateOp {
341 panic("first operation should be a Create")
342 }
343 }
344 bug.staging.Append(op)
345}
346
347// Commit write the staging area in Git and move the operations to the packs
348func (bug *Bug) Commit(repo repository.ClockedRepo) error {
349 if !bug.NeedCommit() {
350 return fmt.Errorf("can't commit a bug with no pending operation")
351 }
352
353 if err := bug.Validate(); err != nil {
354 return errors.Wrap(err, "can't commit a bug with invalid data")
355 }
356
357 // update clocks
358 var err error
359 bug.editTime, err = repo.Increment(editClockName)
360 if err != nil {
361 return err
362 }
363 if bug.lastCommit == "" {
364 bug.createTime, err = repo.Increment(creationClockName)
365 if err != nil {
366 return err
367 }
368 }
369
370 // Write the Ops as a Git blob containing the serialized array
371 hash, err := bug.staging.Write(repo)
372 if err != nil {
373 return err
374 }
375
376 // Make a Git tree referencing this blob
377 tree := []repository.TreeEntry{
378 // the last pack of ops
379 {ObjectType: repository.Blob, Hash: hash, Name: opsEntryName},
380 }
381
382 // Store the logical clocks as well
383 // --> edit clock for each OperationPack/commits
384 // --> create clock only for the first OperationPack/commits
385 //
386 // To avoid having one blob for each clock value, clocks are serialized
387 // directly into the entry name
388 emptyBlobHash, err := repo.StoreData([]byte{})
389 if err != nil {
390 return err
391 }
392 tree = append(tree, repository.TreeEntry{
393 ObjectType: repository.Blob,
394 Hash: emptyBlobHash,
395 Name: fmt.Sprintf(editClockEntryPattern, bug.editTime),
396 })
397 if bug.lastCommit == "" {
398 tree = append(tree, repository.TreeEntry{
399 ObjectType: repository.Blob,
400 Hash: emptyBlobHash,
401 Name: fmt.Sprintf(createClockEntryPattern, bug.createTime),
402 })
403 }
404
405 // Reference, if any, all the files required by the ops
406 // Git will check that they actually exist in the storage and will make sure
407 // to push/pull them as needed.
408 mediaTree := makeMediaTree(bug.staging)
409 if len(mediaTree) > 0 {
410 mediaTreeHash, err := repo.StoreTree(mediaTree)
411 if err != nil {
412 return err
413 }
414 tree = append(tree, repository.TreeEntry{
415 ObjectType: repository.Tree,
416 Hash: mediaTreeHash,
417 Name: mediaEntryName,
418 })
419 }
420
421 // Store the tree
422 hash, err = repo.StoreTree(tree)
423 if err != nil {
424 return err
425 }
426
427 // Write a Git commit referencing the tree, with the previous commit as parent
428 if bug.lastCommit != "" {
429 hash, err = repo.StoreCommit(hash, bug.lastCommit)
430 } else {
431 hash, err = repo.StoreCommit(hash)
432 }
433 if err != nil {
434 return err
435 }
436
437 bug.lastCommit = hash
438 bug.staging.commitHash = hash
439 bug.packs = append(bug.packs, bug.staging)
440 bug.staging = OperationPack{}
441
442 // Create or update the Git reference for this bug
443 // When pushing later, the remote will ensure that this ref update
444 // is fast-forward, that is no data has been overwritten
445 ref := fmt.Sprintf("%s%s", bugsRefPattern, bug.Id().String())
446 return repo.UpdateRef(ref, hash)
447}
448
449func (bug *Bug) CommitAsNeeded(repo repository.ClockedRepo) error {
450 if !bug.NeedCommit() {
451 return nil
452 }
453 return bug.Commit(repo)
454}
455
456func (bug *Bug) NeedCommit() bool {
457 return !bug.staging.IsEmpty()
458}
459
460// Merge a different version of the same bug by rebasing operations of this bug
461// that are not present in the other on top of the chain of operations of the
462// other version.
463func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) {
464 var otherBug = bugFromInterface(other)
465
466 // Note: a faster merge should be possible without actually reading and parsing
467 // all operations pack of our side.
468 // Reading the other side is still necessary to validate remote data, at least
469 // for new operations
470
471 if bug.Id() != otherBug.Id() {
472 return false, errors.New("merging unrelated bugs is not supported")
473 }
474
475 if len(otherBug.staging.Operations) > 0 {
476 return false, errors.New("merging a bug with a non-empty staging is not supported")
477 }
478
479 if bug.lastCommit == "" || otherBug.lastCommit == "" {
480 return false, errors.New("can't merge a bug that has never been stored")
481 }
482
483 ancestor, err := repo.FindCommonAncestor(bug.lastCommit, otherBug.lastCommit)
484 if err != nil {
485 return false, errors.Wrap(err, "can't find common ancestor")
486 }
487
488 ancestorIndex := 0
489 newPacks := make([]OperationPack, 0, len(bug.packs))
490
491 // Find the root of the rebase
492 for i, pack := range bug.packs {
493 newPacks = append(newPacks, pack)
494
495 if pack.commitHash == ancestor {
496 ancestorIndex = i
497 break
498 }
499 }
500
501 if len(otherBug.packs) == ancestorIndex+1 {
502 // Nothing to rebase, return early
503 return false, nil
504 }
505
506 // get other bug's extra packs
507 for i := ancestorIndex + 1; i < len(otherBug.packs); i++ {
508 // clone is probably not necessary
509 newPack := otherBug.packs[i].Clone()
510
511 newPacks = append(newPacks, newPack)
512 bug.lastCommit = newPack.commitHash
513 }
514
515 // rebase our extra packs
516 for i := ancestorIndex + 1; i < len(bug.packs); i++ {
517 pack := bug.packs[i]
518
519 // get the referenced git tree
520 treeHash, err := repo.GetTreeHash(pack.commitHash)
521
522 if err != nil {
523 return false, err
524 }
525
526 // create a new commit with the correct ancestor
527 hash, err := repo.StoreCommit(treeHash, bug.lastCommit)
528
529 if err != nil {
530 return false, err
531 }
532
533 // replace the pack
534 newPack := pack.Clone()
535 newPack.commitHash = hash
536 newPacks = append(newPacks, newPack)
537
538 // update the bug
539 bug.lastCommit = hash
540 }
541
542 bug.packs = newPacks
543
544 // Update the git ref
545 err = repo.UpdateRef(bugsRefPattern+bug.Id().String(), bug.lastCommit)
546 if err != nil {
547 return false, err
548 }
549
550 return true, nil
551}
552
553// Id return the Bug identifier
554func (bug *Bug) Id() entity.Id {
555 // id is the id of the first operation
556 return bug.FirstOp().Id()
557}
558
559// CreateLamportTime return the Lamport time of creation
560func (bug *Bug) CreateLamportTime() lamport.Time {
561 return bug.createTime
562}
563
564// EditLamportTime return the Lamport time of the last edit
565func (bug *Bug) EditLamportTime() lamport.Time {
566 return bug.editTime
567}
568
569// Lookup for the very first operation of the bug.
570// For a valid Bug, this operation should be a CreateOp
571func (bug *Bug) FirstOp() Operation {
572 for _, pack := range bug.packs {
573 for _, op := range pack.Operations {
574 return op
575 }
576 }
577
578 if !bug.staging.IsEmpty() {
579 return bug.staging.Operations[0]
580 }
581
582 return nil
583}
584
585// Lookup for the very last operation of the bug.
586// For a valid Bug, should never be nil
587func (bug *Bug) LastOp() Operation {
588 if !bug.staging.IsEmpty() {
589 return bug.staging.Operations[len(bug.staging.Operations)-1]
590 }
591
592 if len(bug.packs) == 0 {
593 return nil
594 }
595
596 lastPack := bug.packs[len(bug.packs)-1]
597
598 if len(lastPack.Operations) == 0 {
599 return nil
600 }
601
602 return lastPack.Operations[len(lastPack.Operations)-1]
603}
604
605// Compile a bug in a easily usable snapshot
606func (bug *Bug) Compile() Snapshot {
607 snap := Snapshot{
608 id: bug.Id(),
609 Status: OpenStatus,
610 }
611
612 it := NewOperationIterator(bug)
613
614 for it.Next() {
615 op := it.Value()
616 op.Apply(&snap)
617 snap.Operations = append(snap.Operations, op)
618 }
619
620 return snap
621}