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