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