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