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