bug.go

  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() {}