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