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