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/remote/%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.ListRefs(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// IsValid check if the Bug data is valid
208func (bug *Bug) IsValid() bool {
209	// non-empty
210	if len(bug.packs) == 0 && bug.staging.IsEmpty() {
211		return false
212	}
213
214	// check if each pack is valid
215	for _, pack := range bug.packs {
216		if !pack.IsValid() {
217			return false
218		}
219	}
220
221	// check if staging is valid if needed
222	if !bug.staging.IsEmpty() {
223		if !bug.staging.IsValid() {
224			return false
225		}
226	}
227
228	// The very first Op should be a CreateOp
229	firstOp := bug.firstOp()
230	if firstOp == nil || firstOp.OpType() != CreateOp {
231		return false
232	}
233
234	// Check that there is no more CreateOp op
235	it := NewOperationIterator(bug)
236	createCount := 0
237	for it.Next() {
238		if it.Value().OpType() == CreateOp {
239			createCount++
240		}
241	}
242
243	if createCount != 1 {
244		return false
245	}
246
247	return true
248}
249
250func (bug *Bug) Append(op Operation) {
251	bug.staging.Append(op)
252}
253
254// Write the staging area in Git and move the operations to the packs
255func (bug *Bug) Commit(repo repository.Repo) error {
256	if bug.staging.IsEmpty() {
257		return fmt.Errorf("can't commit an empty bug")
258	}
259
260	// Write the Ops as a Git blob containing the serialized array
261	hash, err := bug.staging.Write(repo)
262	if err != nil {
263		return err
264	}
265
266	if bug.rootPack == "" {
267		bug.rootPack = hash
268	}
269
270	// Write a Git tree referencing this blob
271	hash, err = repo.StoreTree([]repository.TreeEntry{
272		// the last pack of ops
273		{ObjectType: repository.Blob, Hash: hash, Name: opsEntryName},
274		// always the first pack of ops (might be the same)
275		{ObjectType: repository.Blob, Hash: bug.rootPack, Name: rootEntryName},
276	})
277
278	if err != nil {
279		return err
280	}
281
282	// Write a Git commit referencing the tree, with the previous commit as parent
283	if bug.lastCommit != "" {
284		hash, err = repo.StoreCommitWithParent(hash, bug.lastCommit)
285	} else {
286		hash, err = repo.StoreCommit(hash)
287	}
288
289	if err != nil {
290		return err
291	}
292
293	bug.lastCommit = hash
294
295	// if it was the first commit, use the commit hash as bug id
296	if bug.id == "" {
297		bug.id = string(hash)
298	}
299
300	// Create or update the Git reference for this bug
301	ref := fmt.Sprintf("%s%s", bugsRefPattern, bug.id)
302	err = repo.UpdateRef(ref, hash)
303
304	if err != nil {
305		return err
306	}
307
308	bug.packs = append(bug.packs, bug.staging)
309	bug.staging = OperationPack{}
310
311	return nil
312}
313
314// Merge a different version of the same bug by rebasing operations of this bug
315// that are not present in the other on top of the chain of operations of the
316// other version.
317func (bug *Bug) Merge(repo repository.Repo, other *Bug) (bool, error) {
318
319	if bug.id != other.id {
320		return false, errors.New("merging unrelated bugs is not supported")
321	}
322
323	if len(other.staging.Operations) > 0 {
324		return false, errors.New("merging a bug with a non-empty staging is not supported")
325	}
326
327	if bug.lastCommit == "" || other.lastCommit == "" {
328		return false, errors.New("can't merge a bug that has never been stored")
329	}
330
331	ancestor, err := repo.FindCommonAncestor(bug.lastCommit, other.lastCommit)
332
333	if err != nil {
334		return false, err
335	}
336
337	rebaseStarted := false
338	updated := false
339
340	for i, pack := range bug.packs {
341		if pack.commitHash == ancestor {
342			rebaseStarted = true
343
344			// get other bug's extra pack
345			for j := i + 1; j < len(other.packs); j++ {
346				// clone is probably not necessary
347				newPack := other.packs[j].Clone()
348
349				bug.packs = append(bug.packs, newPack)
350				bug.lastCommit = newPack.commitHash
351				updated = true
352			}
353
354			continue
355		}
356
357		if !rebaseStarted {
358			continue
359		}
360
361		updated = true
362
363		// get the referenced git tree
364		treeHash, err := repo.GetTreeHash(pack.commitHash)
365
366		if err != nil {
367			return false, err
368		}
369
370		// create a new commit with the correct ancestor
371		hash, err := repo.StoreCommitWithParent(treeHash, bug.lastCommit)
372
373		// replace the pack
374		bug.packs[i] = pack.Clone()
375		bug.packs[i].commitHash = hash
376
377		// update the bug
378		bug.lastCommit = hash
379	}
380
381	// Update the git ref
382	if updated {
383		err := repo.UpdateRef(bugsRefPattern+bug.id, bug.lastCommit)
384		if err != nil {
385			return false, err
386		}
387	}
388
389	return updated, nil
390}
391
392// Return the Bug identifier
393func (bug *Bug) Id() string {
394	if bug.id == "" {
395		// simply panic as it would be a coding error
396		// (using an id of a bug not stored yet)
397		panic("no id yet")
398	}
399	return bug.id
400}
401
402// Return the Bug identifier truncated for human consumption
403func (bug *Bug) HumanId() string {
404	return formatHumanId(bug.Id())
405}
406
407func formatHumanId(id string) string {
408	format := fmt.Sprintf("%%.%ds", humanIdLength)
409	return fmt.Sprintf(format, id)
410}
411
412// Lookup for the very first operation of the bug.
413// For a valid Bug, this operation should be a CreateOp
414func (bug *Bug) firstOp() Operation {
415	for _, pack := range bug.packs {
416		for _, op := range pack.Operations {
417			return op
418		}
419	}
420
421	if !bug.staging.IsEmpty() {
422		return bug.staging.Operations[0]
423	}
424
425	return nil
426}
427
428// Compile a bug in a easily usable snapshot
429func (bug *Bug) Compile() Snapshot {
430	snap := Snapshot{
431		id:     bug.id,
432		Status: OpenStatus,
433	}
434
435	it := NewOperationIterator(bug)
436
437	for it.Next() {
438		op := it.Value()
439		snap = op.Apply(snap)
440		snap.Operations = append(snap.Operations, op)
441	}
442
443	return snap
444}