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
  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}