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