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