bug.go

  1package bug
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"github.com/MichaelMure/git-bug/repository"
  7	"github.com/MichaelMure/git-bug/util"
  8	"strings"
  9)
 10
 11const bugsRefPattern = "refs/bugs/"
 12const bugsRemoteRefPattern = "refs/remotes/%s/bugs/"
 13const opsEntryName = "ops"
 14const rootEntryName = "root"
 15
 16const idLength = 40
 17const humanIdLength = 7
 18
 19// Bug hold the data of a bug thread, organized in a way close to
 20// how it will be persisted inside Git. This is the data structure
 21// used for merge of two different version.
 22type Bug struct {
 23	// Id used as unique identifier
 24	id string
 25
 26	lastCommit util.Hash
 27	rootPack   util.Hash
 28
 29	// TODO: need a way to order bugs, probably a Lamport clock
 30
 31	packs []OperationPack
 32
 33	staging OperationPack
 34}
 35
 36// Create a new Bug
 37func NewBug() *Bug {
 38	// No id yet
 39	return &Bug{}
 40}
 41
 42// Find an existing Bug matching a prefix
 43func FindLocalBug(repo repository.Repo, prefix string) (*Bug, error) {
 44	ids, err := repo.ListIds(bugsRefPattern)
 45
 46	if err != nil {
 47		return nil, err
 48	}
 49
 50	// preallocate but empty
 51	matching := make([]string, 0, 5)
 52
 53	for _, id := range ids {
 54		if strings.HasPrefix(id, prefix) {
 55			matching = append(matching, id)
 56		}
 57	}
 58
 59	if len(matching) == 0 {
 60		return nil, errors.New("No matching bug found.")
 61	}
 62
 63	if len(matching) > 1 {
 64		return nil, fmt.Errorf("Multiple matching bug found:\n%s", strings.Join(matching, "\n"))
 65	}
 66
 67	return ReadLocalBug(repo, matching[0])
 68}
 69
 70func ReadLocalBug(repo repository.Repo, id string) (*Bug, error) {
 71	ref := bugsRefPattern + id
 72	return readBug(repo, ref)
 73}
 74
 75func ReadRemoteBug(repo repository.Repo, remote string, id string) (*Bug, error) {
 76	ref := fmt.Sprintf(bugsRemoteRefPattern, remote) + id
 77	return readBug(repo, ref)
 78}
 79
 80// Read and parse a Bug from git
 81func readBug(repo repository.Repo, ref string) (*Bug, error) {
 82	hashes, err := repo.ListCommits(ref)
 83
 84	if err != nil {
 85		return nil, err
 86	}
 87
 88	refSplitted := strings.Split(ref, "/")
 89	id := refSplitted[len(refSplitted)-1]
 90
 91	if len(id) != idLength {
 92		return nil, fmt.Errorf("Invalid ref length")
 93	}
 94
 95	bug := Bug{
 96		id: id,
 97	}
 98
 99	// Load each OperationPack
100	for _, hash := range hashes {
101		entries, err := repo.ListEntries(hash)
102
103		bug.lastCommit = hash
104
105		if err != nil {
106			return nil, err
107		}
108
109		var opsEntry repository.TreeEntry
110		opsFound := false
111		var rootEntry repository.TreeEntry
112		rootFound := false
113
114		for _, entry := range entries {
115			if entry.Name == opsEntryName {
116				opsEntry = entry
117				opsFound = true
118				continue
119			}
120			if entry.Name == rootEntryName {
121				rootEntry = entry
122				rootFound = true
123			}
124		}
125
126		if !opsFound {
127			return nil, errors.New("Invalid tree, missing the ops entry")
128		}
129
130		if !rootFound {
131			return nil, errors.New("Invalid tree, missing the root entry")
132		}
133
134		if bug.rootPack == "" {
135			bug.rootPack = rootEntry.Hash
136		}
137
138		data, err := repo.ReadData(opsEntry.Hash)
139
140		if err != nil {
141			return nil, err
142		}
143
144		op, err := ParseOperationPack(data)
145
146		if err != nil {
147			return nil, err
148		}
149
150		// tag the pack with the commit hash
151		op.commitHash = hash
152
153		if err != nil {
154			return nil, err
155		}
156
157		bug.packs = append(bug.packs, *op)
158	}
159
160	return &bug, nil
161}
162
163type StreamedBug struct {
164	Bug *Bug
165	Err error
166}
167
168// Read and parse all local bugs
169func ReadAllLocalBugs(repo repository.Repo) <-chan StreamedBug {
170	return readAllBugs(repo, bugsRefPattern)
171}
172
173// Read and parse all remote bugs for a given remote
174func ReadAllRemoteBugs(repo repository.Repo, remote string) <-chan StreamedBug {
175	refPrefix := fmt.Sprintf(bugsRemoteRefPattern, remote)
176	return readAllBugs(repo, refPrefix)
177}
178
179// Read and parse all available bug with a given ref prefix
180func readAllBugs(repo repository.Repo, refPrefix string) <-chan StreamedBug {
181	out := make(chan StreamedBug)
182
183	go func() {
184		defer close(out)
185
186		refs, err := repo.ListRefs(refPrefix)
187		if err != nil {
188			out <- StreamedBug{Err: err}
189			return
190		}
191
192		for _, ref := range refs {
193			b, err := readBug(repo, ref)
194
195			if err != nil {
196				out <- StreamedBug{Err: err}
197				return
198			}
199
200			out <- StreamedBug{Bug: b}
201		}
202	}()
203
204	return out
205}
206
207// List all the available local bug ids
208func ListLocalIds(repo repository.Repo) ([]string, error) {
209	return repo.ListIds(bugsRefPattern)
210}
211
212// IsValid check if the Bug data is valid
213func (bug *Bug) IsValid() bool {
214	// non-empty
215	if len(bug.packs) == 0 && bug.staging.IsEmpty() {
216		return false
217	}
218
219	// check if each pack is valid
220	for _, pack := range bug.packs {
221		if !pack.IsValid() {
222			return false
223		}
224	}
225
226	// check if staging is valid if needed
227	if !bug.staging.IsEmpty() {
228		if !bug.staging.IsValid() {
229			return false
230		}
231	}
232
233	// The very first Op should be a CreateOp
234	firstOp := bug.firstOp()
235	if firstOp == nil || firstOp.OpType() != CreateOp {
236		return false
237	}
238
239	// Check that there is no more CreateOp op
240	it := NewOperationIterator(bug)
241	createCount := 0
242	for it.Next() {
243		if it.Value().OpType() == CreateOp {
244			createCount++
245		}
246	}
247
248	if createCount != 1 {
249		return false
250	}
251
252	return true
253}
254
255func (bug *Bug) Append(op Operation) {
256	bug.staging.Append(op)
257}
258
259// Write the staging area in Git and move the operations to the packs
260func (bug *Bug) Commit(repo repository.Repo) error {
261	if bug.staging.IsEmpty() {
262		return fmt.Errorf("can't commit a bug with no pending operation")
263	}
264
265	// Write the Ops as a Git blob containing the serialized array
266	hash, err := bug.staging.Write(repo)
267	if err != nil {
268		return err
269	}
270
271	if bug.rootPack == "" {
272		bug.rootPack = hash
273	}
274
275	// Make a Git tree referencing this blob and all needed files
276	tree := []repository.TreeEntry{
277		// the last pack of ops
278		{ObjectType: repository.Blob, Hash: hash, Name: opsEntryName},
279		// always the first pack of ops (might be the same)
280		{ObjectType: repository.Blob, Hash: bug.rootPack, Name: rootEntryName},
281	}
282
283	counter := 0
284	added := make(map[util.Hash]interface{})
285	for _, ops := range bug.staging.Operations {
286		for _, file := range ops.Files() {
287			if _, has := added[file]; !has {
288				tree = append(tree, repository.TreeEntry{
289					ObjectType: repository.Blob,
290					Hash:       file,
291					Name:       fmt.Sprintf("file%d", counter),
292				})
293				counter++
294				added[file] = struct{}{}
295			}
296		}
297	}
298
299	hash, err = repo.StoreTree(tree)
300	if err != nil {
301		return err
302	}
303
304	// Write a Git commit referencing the tree, with the previous commit as parent
305	if bug.lastCommit != "" {
306		hash, err = repo.StoreCommitWithParent(hash, bug.lastCommit)
307	} else {
308		hash, err = repo.StoreCommit(hash)
309	}
310
311	if err != nil {
312		return err
313	}
314
315	bug.lastCommit = hash
316
317	// if it was the first commit, use the commit hash as bug id
318	if bug.id == "" {
319		bug.id = string(hash)
320	}
321
322	// Create or update the Git reference for this bug
323	ref := fmt.Sprintf("%s%s", bugsRefPattern, bug.id)
324	err = repo.UpdateRef(ref, hash)
325
326	if err != nil {
327		return err
328	}
329
330	bug.packs = append(bug.packs, bug.staging)
331	bug.staging = OperationPack{}
332
333	return nil
334}
335
336// Merge a different version of the same bug by rebasing operations of this bug
337// that are not present in the other on top of the chain of operations of the
338// other version.
339func (bug *Bug) Merge(repo repository.Repo, other *Bug) (bool, error) {
340	// Note: a faster merge should be possible without actually reading and parsing
341	// all operations pack of our side.
342	// Reading the other side is still necessary to validate remote data, at least
343	// for new operations
344
345	if bug.id != other.id {
346		return false, errors.New("merging unrelated bugs is not supported")
347	}
348
349	if len(other.staging.Operations) > 0 {
350		return false, errors.New("merging a bug with a non-empty staging is not supported")
351	}
352
353	if bug.lastCommit == "" || other.lastCommit == "" {
354		return false, errors.New("can't merge a bug that has never been stored")
355	}
356
357	ancestor, err := repo.FindCommonAncestor(bug.lastCommit, other.lastCommit)
358
359	if err != nil {
360		return false, err
361	}
362
363	ancestorIndex := 0
364	newPacks := make([]OperationPack, 0, len(bug.packs))
365
366	// Find the root of the rebase
367	for i, pack := range bug.packs {
368		newPacks = append(newPacks, pack)
369
370		if pack.commitHash == ancestor {
371			ancestorIndex = i
372			break
373		}
374	}
375
376	if len(other.packs) == ancestorIndex+1 {
377		// Nothing to rebase, return early
378		return false, nil
379	}
380
381	// get other bug's extra packs
382	for i := ancestorIndex + 1; i < len(other.packs); i++ {
383		// clone is probably not necessary
384		newPack := other.packs[i].Clone()
385
386		newPacks = append(newPacks, newPack)
387		bug.lastCommit = newPack.commitHash
388	}
389
390	// rebase our extra packs
391	for i := ancestorIndex + 1; i < len(bug.packs); i++ {
392		pack := bug.packs[i]
393
394		// get the referenced git tree
395		treeHash, err := repo.GetTreeHash(pack.commitHash)
396
397		if err != nil {
398			return false, err
399		}
400
401		// create a new commit with the correct ancestor
402		hash, err := repo.StoreCommitWithParent(treeHash, bug.lastCommit)
403
404		// replace the pack
405		newPack := pack.Clone()
406		newPack.commitHash = hash
407		newPacks = append(newPacks, newPack)
408
409		// update the bug
410		bug.lastCommit = hash
411	}
412
413	// Update the git ref
414	err = repo.UpdateRef(bugsRefPattern+bug.id, bug.lastCommit)
415	if err != nil {
416		return false, err
417	}
418
419	return true, nil
420}
421
422// Return the Bug identifier
423func (bug *Bug) Id() string {
424	if bug.id == "" {
425		// simply panic as it would be a coding error
426		// (using an id of a bug not stored yet)
427		panic("no id yet")
428	}
429	return bug.id
430}
431
432// Return the Bug identifier truncated for human consumption
433func (bug *Bug) HumanId() string {
434	return formatHumanId(bug.Id())
435}
436
437func formatHumanId(id string) string {
438	format := fmt.Sprintf("%%.%ds", humanIdLength)
439	return fmt.Sprintf(format, id)
440}
441
442// Lookup for the very first operation of the bug.
443// For a valid Bug, this operation should be a CreateOp
444func (bug *Bug) firstOp() Operation {
445	for _, pack := range bug.packs {
446		for _, op := range pack.Operations {
447			return op
448		}
449	}
450
451	if !bug.staging.IsEmpty() {
452		return bug.staging.Operations[0]
453	}
454
455	return nil
456}
457
458// Compile a bug in a easily usable snapshot
459func (bug *Bug) Compile() Snapshot {
460	snap := Snapshot{
461		id:     bug.id,
462		Status: OpenStatus,
463	}
464
465	it := NewOperationIterator(bug)
466
467	for it.Next() {
468		op := it.Value()
469		snap = op.Apply(snap)
470		snap.Operations = append(snap.Operations, op)
471	}
472
473	return snap
474}