bug.go

  1package bug
  2
  3import (
  4	"crypto/sha256"
  5	"errors"
  6	"fmt"
  7	"github.com/MichaelMure/git-bug/repository"
  8	"github.com/MichaelMure/git-bug/util"
  9	"github.com/kevinburke/go.uuid"
 10	"strings"
 11)
 12
 13const BugsRefPattern = "refs/bugs/"
 14const BugsRemoteRefPattern = "refs/remote/%s/bugs/"
 15const OpsEntryName = "ops"
 16const RootEntryName = "root"
 17
 18// Bug hold the data of a bug thread, organized in a way close to
 19// how it will be persisted inside Git. This is the datastructure
 20// used for merge of two different version.
 21type Bug struct {
 22	// Id used as unique identifier
 23	id string
 24
 25	lastCommit util.Hash
 26	root       util.Hash
 27
 28	// TODO: need a way to order bugs, probably a Lamport clock
 29
 30	packs []OperationPack
 31
 32	staging OperationPack
 33}
 34
 35// Create a new Bug
 36func NewBug() (*Bug, error) {
 37	// TODO: replace with commit hash of (first commit + some random)
 38
 39	// Creating UUID Version 4
 40	unique, err := uuid.ID4()
 41
 42	if err != nil {
 43		return nil, err
 44	}
 45
 46	// Use it as source of uniqueness
 47	hash := sha256.New().Sum(unique.Bytes())
 48
 49	// format in hex and truncate to 40 char
 50	id := fmt.Sprintf("%.40s", fmt.Sprintf("%x", hash))
 51
 52	return &Bug{
 53		id: id,
 54	}, nil
 55}
 56
 57// Find an existing Bug matching a prefix
 58func FindBug(repo repository.Repo, prefix string) (*Bug, error) {
 59	ids, err := repo.ListRefs(BugsRefPattern)
 60
 61	if err != nil {
 62		return nil, err
 63	}
 64
 65	// preallocate but empty
 66	matching := make([]string, 0, 5)
 67
 68	for _, id := range ids {
 69		if strings.HasPrefix(id, prefix) {
 70			matching = append(matching, id)
 71		}
 72	}
 73
 74	if len(matching) == 0 {
 75		return nil, errors.New("No matching bug found.")
 76	}
 77
 78	if len(matching) > 1 {
 79		return nil, fmt.Errorf("Multiple matching bug found:\n%s", strings.Join(matching, "\n"))
 80	}
 81
 82	return ReadBug(repo, BugsRefPattern+matching[0])
 83}
 84
 85// Read and parse a Bug from git
 86func ReadBug(repo repository.Repo, ref string) (*Bug, error) {
 87	hashes, err := repo.ListCommits(ref)
 88
 89	if err != nil {
 90		return nil, err
 91	}
 92
 93	refSplitted := strings.Split(ref, "/")
 94	id := refSplitted[len(refSplitted)-1]
 95
 96	bug := Bug{
 97		id: id,
 98	}
 99
100	// Load each OperationPack
101	for _, hash := range hashes {
102		entries, err := repo.ListEntries(hash)
103
104		bug.lastCommit = hash
105
106		if err != nil {
107			return nil, err
108		}
109
110		var opsEntry repository.TreeEntry
111		opsFound := false
112		var rootEntry repository.TreeEntry
113		rootFound := false
114
115		for _, entry := range entries {
116			if entry.Name == OpsEntryName {
117				opsEntry = entry
118				opsFound = true
119				continue
120			}
121			if entry.Name == RootEntryName {
122				rootEntry = entry
123				rootFound = true
124			}
125		}
126
127		if !opsFound {
128			return nil, errors.New("Invalid tree, missing the ops entry")
129		}
130
131		if !rootFound {
132			return nil, errors.New("Invalid tree, missing the root entry")
133		}
134
135		if bug.root == "" {
136			bug.root = rootEntry.Hash
137		}
138
139		data, err := repo.ReadData(opsEntry.Hash)
140
141		if err != nil {
142			return nil, err
143		}
144
145		op, err := ParseOperationPack(data)
146
147		if err != nil {
148			return nil, err
149		}
150
151		// tag the pack with the commit hash
152		op.commitHash = hash
153
154		if err != nil {
155			return nil, err
156		}
157
158		bug.packs = append(bug.packs, *op)
159	}
160
161	return &bug, nil
162}
163
164// IsValid check if the Bug data is valid
165func (bug *Bug) IsValid() bool {
166	// non-empty
167	if len(bug.packs) == 0 && bug.staging.IsEmpty() {
168		return false
169	}
170
171	// check if each pack is valid
172	for _, pack := range bug.packs {
173		if !pack.IsValid() {
174			return false
175		}
176	}
177
178	// check if staging is valid if needed
179	if !bug.staging.IsEmpty() {
180		if !bug.staging.IsValid() {
181			return false
182		}
183	}
184
185	// The very first Op should be a CREATE
186	firstOp := bug.firstOp()
187	if firstOp == nil || firstOp.OpType() != CREATE {
188		return false
189	}
190
191	// Check that there is no more CREATE op
192	it := NewOperationIterator(bug)
193	createCount := 0
194	for it.Next() {
195		if it.Value().OpType() == CREATE {
196			createCount++
197		}
198	}
199
200	if createCount != 1 {
201		return false
202	}
203
204	return true
205}
206
207func (bug *Bug) Append(op Operation) {
208	bug.staging.Append(op)
209}
210
211// Write the staging area in Git and move the operations to the packs
212func (bug *Bug) Commit(repo repository.Repo) error {
213	if bug.staging.IsEmpty() {
214		return nil
215	}
216
217	// Write the Ops as a Git blob containing the serialized array
218	hash, err := bug.staging.Write(repo)
219	if err != nil {
220		return err
221	}
222
223	root := bug.root
224	if root == "" {
225		root = hash
226		bug.root = hash
227	}
228
229	// Write a Git tree referencing this blob
230	hash, err = repo.StoreTree([]repository.TreeEntry{
231		{repository.Blob, hash, OpsEntryName},  // the last pack of ops
232		{repository.Blob, root, RootEntryName}, // always the first pack of ops (might be the same)
233	})
234	if err != nil {
235		return err
236	}
237
238	// Write a Git commit referencing the tree, with the previous commit as parent
239	if bug.lastCommit != "" {
240		hash, err = repo.StoreCommitWithParent(hash, bug.lastCommit)
241	} else {
242		hash, err = repo.StoreCommit(hash)
243	}
244
245	if err != nil {
246		return err
247	}
248
249	bug.lastCommit = hash
250
251	// Create or update the Git reference for this bug
252	ref := fmt.Sprintf("%s%s", BugsRefPattern, bug.id)
253	err = repo.UpdateRef(ref, hash)
254
255	if err != nil {
256		return err
257	}
258
259	bug.packs = append(bug.packs, bug.staging)
260	bug.staging = OperationPack{}
261
262	return nil
263}
264
265// Merge a different version of the same bug by rebasing operations of this bug
266// that are not present in the other on top of the chain of operations of the
267// other version.
268func (bug *Bug) Merge(repo repository.Repo, other *Bug) (bool, error) {
269
270	if bug.id != other.id {
271		return false, errors.New("merging unrelated bugs is not supported")
272	}
273
274	if len(other.staging.Operations) > 0 {
275		return false, errors.New("merging a bug with a non-empty staging is not supported")
276	}
277
278	if bug.lastCommit == "" || other.lastCommit == "" {
279		return false, errors.New("can't merge a bug that has never been stored")
280	}
281
282	ancestor, err := repo.FindCommonAncestor(bug.lastCommit, other.lastCommit)
283
284	if err != nil {
285		return false, err
286	}
287
288	rebaseStarted := false
289	updated := false
290
291	for i, pack := range bug.packs {
292		if pack.commitHash == ancestor {
293			rebaseStarted = true
294
295			// get other bug's extra pack
296			for j := i + 1; j < len(other.packs); j++ {
297				// clone is probably not necessary
298				newPack := other.packs[j].Clone()
299
300				bug.packs = append(bug.packs, newPack)
301				bug.lastCommit = newPack.commitHash
302				updated = true
303			}
304
305			continue
306		}
307
308		if !rebaseStarted {
309			continue
310		}
311
312		updated = true
313
314		// get the referenced git tree
315		treeHash, err := repo.GetTreeHash(pack.commitHash)
316
317		if err != nil {
318			return false, err
319		}
320
321		// create a new commit with the correct ancestor
322		hash, err := repo.StoreCommitWithParent(treeHash, bug.lastCommit)
323
324		// replace the pack
325		bug.packs[i] = pack.Clone()
326		bug.packs[i].commitHash = hash
327
328		// update the bug
329		bug.lastCommit = hash
330	}
331
332	// Update the git ref
333	if updated {
334		err := repo.UpdateRef(BugsRefPattern+bug.id, bug.lastCommit)
335		if err != nil {
336			return false, err
337		}
338	}
339
340	return updated, nil
341}
342
343// Return the Bug identifier
344func (bug *Bug) Id() string {
345	return bug.id
346}
347
348// Return the Bug identifier truncated for human consumption
349func (bug *Bug) HumanId() string {
350	return fmt.Sprintf("%.8s", bug.id)
351}
352
353// Lookup for the very first operation of the bug.
354// For a valid Bug, this operation should be a CREATE
355func (bug *Bug) firstOp() Operation {
356	for _, pack := range bug.packs {
357		for _, op := range pack.Operations {
358			return op
359		}
360	}
361
362	if !bug.staging.IsEmpty() {
363		return bug.staging.Operations[0]
364	}
365
366	return nil
367}
368
369// Compile a bug in a easily usable snapshot
370func (bug *Bug) Compile() Snapshot {
371	snap := Snapshot{}
372
373	it := NewOperationIterator(bug)
374
375	for it.Next() {
376		snap = it.Value().Apply(snap)
377	}
378
379	return snap
380}