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 an empty bug")
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	// Write a Git tree referencing this blob
276	hash, err = repo.StoreTree([]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	if err != nil {
284		return err
285	}
286
287	// Write a Git commit referencing the tree, with the previous commit as parent
288	if bug.lastCommit != "" {
289		hash, err = repo.StoreCommitWithParent(hash, bug.lastCommit)
290	} else {
291		hash, err = repo.StoreCommit(hash)
292	}
293
294	if err != nil {
295		return err
296	}
297
298	bug.lastCommit = hash
299
300	// if it was the first commit, use the commit hash as bug id
301	if bug.id == "" {
302		bug.id = string(hash)
303	}
304
305	// Create or update the Git reference for this bug
306	ref := fmt.Sprintf("%s%s", bugsRefPattern, bug.id)
307	err = repo.UpdateRef(ref, hash)
308
309	if err != nil {
310		return err
311	}
312
313	bug.packs = append(bug.packs, bug.staging)
314	bug.staging = OperationPack{}
315
316	return nil
317}
318
319// Merge a different version of the same bug by rebasing operations of this bug
320// that are not present in the other on top of the chain of operations of the
321// other version.
322func (bug *Bug) Merge(repo repository.Repo, other *Bug) (bool, error) {
323	// Note: a faster merge should be possible without actually reading and parsing
324	// all operations pack of our side.
325	// Reading the other side is still necessary to validate remote data, at least
326	// for new operations
327
328	if bug.id != other.id {
329		return false, errors.New("merging unrelated bugs is not supported")
330	}
331
332	if len(other.staging.Operations) > 0 {
333		return false, errors.New("merging a bug with a non-empty staging is not supported")
334	}
335
336	if bug.lastCommit == "" || other.lastCommit == "" {
337		return false, errors.New("can't merge a bug that has never been stored")
338	}
339
340	ancestor, err := repo.FindCommonAncestor(bug.lastCommit, other.lastCommit)
341
342	if err != nil {
343		return false, err
344	}
345
346	ancestorIndex := 0
347	newPacks := make([]OperationPack, 0, len(bug.packs))
348
349	// Find the root of the rebase
350	for i, pack := range bug.packs {
351		newPacks = append(newPacks, pack)
352
353		if pack.commitHash == ancestor {
354			ancestorIndex = i
355			break
356		}
357	}
358
359	if len(other.packs) == ancestorIndex+1 {
360		// Nothing to rebase, return early
361		return false, nil
362	}
363
364	// get other bug's extra packs
365	for i := ancestorIndex + 1; i < len(other.packs); i++ {
366		// clone is probably not necessary
367		newPack := other.packs[i].Clone()
368
369		newPacks = append(newPacks, newPack)
370		bug.lastCommit = newPack.commitHash
371	}
372
373	// rebase our extra packs
374	for i := ancestorIndex + 1; i < len(bug.packs); i++ {
375		pack := bug.packs[i]
376
377		// get the referenced git tree
378		treeHash, err := repo.GetTreeHash(pack.commitHash)
379
380		if err != nil {
381			return false, err
382		}
383
384		// create a new commit with the correct ancestor
385		hash, err := repo.StoreCommitWithParent(treeHash, bug.lastCommit)
386
387		// replace the pack
388		newPack := pack.Clone()
389		newPack.commitHash = hash
390		newPacks = append(newPacks, newPack)
391
392		// update the bug
393		bug.lastCommit = hash
394	}
395
396	// Update the git ref
397	err = repo.UpdateRef(bugsRefPattern+bug.id, bug.lastCommit)
398	if err != nil {
399		return false, err
400	}
401
402	return true, nil
403}
404
405// Return the Bug identifier
406func (bug *Bug) Id() string {
407	if bug.id == "" {
408		// simply panic as it would be a coding error
409		// (using an id of a bug not stored yet)
410		panic("no id yet")
411	}
412	return bug.id
413}
414
415// Return the Bug identifier truncated for human consumption
416func (bug *Bug) HumanId() string {
417	return formatHumanId(bug.Id())
418}
419
420func formatHumanId(id string) string {
421	format := fmt.Sprintf("%%.%ds", humanIdLength)
422	return fmt.Sprintf(format, id)
423}
424
425// Lookup for the very first operation of the bug.
426// For a valid Bug, this operation should be a CreateOp
427func (bug *Bug) firstOp() Operation {
428	for _, pack := range bug.packs {
429		for _, op := range pack.Operations {
430			return op
431		}
432	}
433
434	if !bug.staging.IsEmpty() {
435		return bug.staging.Operations[0]
436	}
437
438	return nil
439}
440
441// Compile a bug in a easily usable snapshot
442func (bug *Bug) Compile() Snapshot {
443	snap := Snapshot{
444		id:     bug.id,
445		Status: OpenStatus,
446	}
447
448	it := NewOperationIterator(bug)
449
450	for it.Next() {
451		op := it.Value()
452		snap = op.Apply(snap)
453		snap.Operations = append(snap.Operations, op)
454	}
455
456	return snap
457}