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
319 if bug.id != other.id {
320 return false, errors.New("merging unrelated bugs is not supported")
321 }
322
323 if len(other.staging.Operations) > 0 {
324 return false, errors.New("merging a bug with a non-empty staging is not supported")
325 }
326
327 if bug.lastCommit == "" || other.lastCommit == "" {
328 return false, errors.New("can't merge a bug that has never been stored")
329 }
330
331 ancestor, err := repo.FindCommonAncestor(bug.lastCommit, other.lastCommit)
332
333 if err != nil {
334 return false, err
335 }
336
337 rebaseStarted := false
338 updated := false
339
340 for i, pack := range bug.packs {
341 if pack.commitHash == ancestor {
342 rebaseStarted = true
343
344 // get other bug's extra pack
345 for j := i + 1; j < len(other.packs); j++ {
346 // clone is probably not necessary
347 newPack := other.packs[j].Clone()
348
349 bug.packs = append(bug.packs, newPack)
350 bug.lastCommit = newPack.commitHash
351 updated = true
352 }
353
354 continue
355 }
356
357 if !rebaseStarted {
358 continue
359 }
360
361 updated = true
362
363 // get the referenced git tree
364 treeHash, err := repo.GetTreeHash(pack.commitHash)
365
366 if err != nil {
367 return false, err
368 }
369
370 // create a new commit with the correct ancestor
371 hash, err := repo.StoreCommitWithParent(treeHash, bug.lastCommit)
372
373 // replace the pack
374 bug.packs[i] = pack.Clone()
375 bug.packs[i].commitHash = hash
376
377 // update the bug
378 bug.lastCommit = hash
379 }
380
381 // Update the git ref
382 if updated {
383 err := repo.UpdateRef(bugsRefPattern+bug.id, bug.lastCommit)
384 if err != nil {
385 return false, err
386 }
387 }
388
389 return updated, nil
390}
391
392// Return the Bug identifier
393func (bug *Bug) Id() string {
394 if bug.id == "" {
395 // simply panic as it would be a coding error
396 // (using an id of a bug not stored yet)
397 panic("no id yet")
398 }
399 return bug.id
400}
401
402// Return the Bug identifier truncated for human consumption
403func (bug *Bug) HumanId() string {
404 return formatHumanId(bug.Id())
405}
406
407func formatHumanId(id string) string {
408 format := fmt.Sprintf("%%.%ds", humanIdLength)
409 return fmt.Sprintf(format, id)
410}
411
412// Lookup for the very first operation of the bug.
413// For a valid Bug, this operation should be a CreateOp
414func (bug *Bug) firstOp() Operation {
415 for _, pack := range bug.packs {
416 for _, op := range pack.Operations {
417 return op
418 }
419 }
420
421 if !bug.staging.IsEmpty() {
422 return bug.staging.Operations[0]
423 }
424
425 return nil
426}
427
428// Compile a bug in a easily usable snapshot
429func (bug *Bug) Compile() Snapshot {
430 snap := Snapshot{
431 id: bug.id,
432 Status: OpenStatus,
433 }
434
435 it := NewOperationIterator(bug)
436
437 for it.Next() {
438 op := it.Value()
439 snap = op.Apply(snap)
440 snap.Operations = append(snap.Operations, op)
441 }
442
443 return snap
444}