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