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.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 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// IsValid check if the Bug data is valid
208func (bug *Bug) IsValid() bool {
209 // non-empty
210 if len(bug.packs) == 0 && bug.staging.IsEmpty() {
211 return false
212 }
213
214 // check if each pack is valid
215 for _, pack := range bug.packs {
216 if !pack.IsValid() {
217 return false
218 }
219 }
220
221 // check if staging is valid if needed
222 if !bug.staging.IsEmpty() {
223 if !bug.staging.IsValid() {
224 return false
225 }
226 }
227
228 // The very first Op should be a CreateOp
229 firstOp := bug.firstOp()
230 if firstOp == nil || firstOp.OpType() != CreateOp {
231 return false
232 }
233
234 // Check that there is no more CreateOp op
235 it := NewOperationIterator(bug)
236 createCount := 0
237 for it.Next() {
238 if it.Value().OpType() == CreateOp {
239 createCount++
240 }
241 }
242
243 if createCount != 1 {
244 return false
245 }
246
247 return true
248}
249
250func (bug *Bug) Append(op Operation) {
251 bug.staging.Append(op)
252}
253
254// Write the staging area in Git and move the operations to the packs
255func (bug *Bug) Commit(repo repository.Repo) error {
256 if bug.staging.IsEmpty() {
257 return fmt.Errorf("can't commit an empty bug")
258 }
259
260 // Write the Ops as a Git blob containing the serialized array
261 hash, err := bug.staging.Write(repo)
262 if err != nil {
263 return err
264 }
265
266 if bug.rootPack == "" {
267 bug.rootPack = hash
268 }
269
270 // Write a Git tree referencing this blob
271 hash, err = repo.StoreTree([]repository.TreeEntry{
272 // the last pack of ops
273 {ObjectType: repository.Blob, Hash: hash, Name: opsEntryName},
274 // always the first pack of ops (might be the same)
275 {ObjectType: repository.Blob, Hash: bug.rootPack, Name: rootEntryName},
276 })
277
278 if err != nil {
279 return err
280 }
281
282 // Write a Git commit referencing the tree, with the previous commit as parent
283 if bug.lastCommit != "" {
284 hash, err = repo.StoreCommitWithParent(hash, bug.lastCommit)
285 } else {
286 hash, err = repo.StoreCommit(hash)
287 }
288
289 if err != nil {
290 return err
291 }
292
293 bug.lastCommit = hash
294
295 // if it was the first commit, use the commit hash as bug id
296 if bug.id == "" {
297 bug.id = string(hash)
298 }
299
300 // Create or update the Git reference for this bug
301 ref := fmt.Sprintf("%s%s", bugsRefPattern, bug.id)
302 err = repo.UpdateRef(ref, hash)
303
304 if err != nil {
305 return err
306 }
307
308 bug.packs = append(bug.packs, bug.staging)
309 bug.staging = OperationPack{}
310
311 return nil
312}
313
314// Merge a different version of the same bug by rebasing operations of this bug
315// that are not present in the other on top of the chain of operations of the
316// other version.
317func (bug *Bug) Merge(repo repository.Repo, other *Bug) (bool, error) {
318 // Note: a faster merge should be possible without actually reading and parsing
319 // all operations pack of our side.
320 // Reading the other side is still necessary to validate remote data, at least
321 // for new operations
322
323 if bug.id != other.id {
324 return false, errors.New("merging unrelated bugs is not supported")
325 }
326
327 if len(other.staging.Operations) > 0 {
328 return false, errors.New("merging a bug with a non-empty staging is not supported")
329 }
330
331 if bug.lastCommit == "" || other.lastCommit == "" {
332 return false, errors.New("can't merge a bug that has never been stored")
333 }
334
335 ancestor, err := repo.FindCommonAncestor(bug.lastCommit, other.lastCommit)
336
337 if err != nil {
338 return false, err
339 }
340
341 ancestorIndex := 0
342 newPacks := make([]OperationPack, 0, len(bug.packs))
343
344 // Find the root of the rebase
345 for i, pack := range bug.packs {
346 newPacks = append(newPacks, pack)
347
348 if pack.commitHash == ancestor {
349 ancestorIndex = i
350 break
351 }
352 }
353
354 if len(other.packs) == ancestorIndex+1 {
355 // Nothing to rebase, return early
356 return false, nil
357 }
358
359 // get other bug's extra packs
360 for i := ancestorIndex + 1; i < len(other.packs); i++ {
361 // clone is probably not necessary
362 newPack := other.packs[i].Clone()
363
364 newPacks = append(newPacks, newPack)
365 bug.lastCommit = newPack.commitHash
366 }
367
368 // rebase our extra packs
369 for i := ancestorIndex + 1; i < len(bug.packs); i++ {
370 pack := bug.packs[i]
371
372 // get the referenced git tree
373 treeHash, err := repo.GetTreeHash(pack.commitHash)
374
375 if err != nil {
376 return false, err
377 }
378
379 // create a new commit with the correct ancestor
380 hash, err := repo.StoreCommitWithParent(treeHash, bug.lastCommit)
381
382 // replace the pack
383 newPack := pack.Clone()
384 newPack.commitHash = hash
385 newPacks = append(newPacks, newPack)
386
387 // update the bug
388 bug.lastCommit = hash
389 }
390
391 // Update the git ref
392 err = repo.UpdateRef(bugsRefPattern+bug.id, bug.lastCommit)
393 if err != nil {
394 return false, err
395 }
396
397 return true, nil
398}
399
400// Return the Bug identifier
401func (bug *Bug) Id() string {
402 if bug.id == "" {
403 // simply panic as it would be a coding error
404 // (using an id of a bug not stored yet)
405 panic("no id yet")
406 }
407 return bug.id
408}
409
410// Return the Bug identifier truncated for human consumption
411func (bug *Bug) HumanId() string {
412 return formatHumanId(bug.Id())
413}
414
415func formatHumanId(id string) string {
416 format := fmt.Sprintf("%%.%ds", humanIdLength)
417 return fmt.Sprintf(format, id)
418}
419
420// Lookup for the very first operation of the bug.
421// For a valid Bug, this operation should be a CreateOp
422func (bug *Bug) firstOp() Operation {
423 for _, pack := range bug.packs {
424 for _, op := range pack.Operations {
425 return op
426 }
427 }
428
429 if !bug.staging.IsEmpty() {
430 return bug.staging.Operations[0]
431 }
432
433 return nil
434}
435
436// Compile a bug in a easily usable snapshot
437func (bug *Bug) Compile() Snapshot {
438 snap := Snapshot{
439 id: bug.id,
440 Status: OpenStatus,
441 }
442
443 it := NewOperationIterator(bug)
444
445 for it.Next() {
446 op := it.Value()
447 snap = op.Apply(snap)
448 snap.Operations = append(snap.Operations, op)
449 }
450
451 return snap
452}