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