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"
17
18// Bug hold the data of a bug thread, organized in a way close to
19// how it will be persisted inside Git. This is the datastructure
20// used for merge of two different version.
21type Bug struct {
22 // Id used as unique identifier
23 id string
24
25 lastCommit util.Hash
26 root util.Hash
27
28 // TODO: need a way to order bugs, probably a Lamport clock
29
30 packs []OperationPack
31
32 staging OperationPack
33}
34
35// Create a new Bug
36func NewBug() (*Bug, error) {
37 // TODO: replace with commit hash of (first commit + some random)
38
39 // Creating UUID Version 4
40 unique, err := uuid.ID4()
41
42 if err != nil {
43 return nil, err
44 }
45
46 // Use it as source of uniqueness
47 hash := sha256.New().Sum(unique.Bytes())
48
49 // format in hex and truncate to 40 char
50 id := fmt.Sprintf("%.40s", fmt.Sprintf("%x", hash))
51
52 return &Bug{
53 id: id,
54 }, nil
55}
56
57// Find an existing Bug matching a prefix
58func FindBug(repo repository.Repo, prefix string) (*Bug, error) {
59 refs, err := repo.ListRefs(BugsRefPattern)
60
61 if err != nil {
62 return nil, err
63 }
64
65 // preallocate but empty
66 matching := make([]string, 0, 5)
67
68 for _, ref := range refs {
69 if strings.HasPrefix(ref, prefix) {
70 matching = append(matching, ref)
71 }
72 }
73
74 if len(matching) == 0 {
75 return nil, errors.New("No matching bug found.")
76 }
77
78 if len(matching) > 1 {
79 return nil, fmt.Errorf("Multiple matching bug found:\n%s", strings.Join(matching, "\n"))
80 }
81
82 return ReadBug(repo, matching[0])
83}
84
85// Read and parse a Bug from git
86func ReadBug(repo repository.Repo, id string) (*Bug, error) {
87 hashes, err := repo.ListCommits(BugsRefPattern + id)
88
89 if err != nil {
90 return nil, err
91 }
92
93 bug := Bug{
94 id: id,
95 }
96
97 for _, hash := range hashes {
98 entries, err := repo.ListEntries(hash)
99
100 bug.lastCommit = hash
101
102 if err != nil {
103 return nil, err
104 }
105
106 var opsEntry repository.TreeEntry
107 opsFound := false
108 var rootEntry repository.TreeEntry
109 rootFound := false
110
111 for _, entry := range entries {
112 if entry.Name == OpsEntryName {
113 opsEntry = entry
114 opsFound = true
115 continue
116 }
117 if entry.Name == RootEntryName {
118 rootEntry = entry
119 rootFound = true
120 }
121 }
122
123 if !opsFound {
124 return nil, errors.New("Invalid tree, missing the ops entry")
125 }
126
127 if !rootFound {
128 return nil, errors.New("Invalid tree, missing the root entry")
129 }
130
131 if bug.root == "" {
132 bug.root = rootEntry.Hash
133 }
134
135 data, err := repo.ReadData(opsEntry.Hash)
136
137 if err != nil {
138 return nil, err
139 }
140
141 op, err := ParseOperationPack(data)
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 CREATE
175 firstOp := bug.firstOp()
176 if firstOp == nil || firstOp.OpType() != CREATE {
177 return false
178 }
179
180 // Check that there is no more CREATE op
181 it := NewOperationIterator(bug)
182 createCount := 0
183 for it.Next() {
184 if it.Value().OpType() == CREATE {
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 nil
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 root := bug.root
213 if root == "" {
214 root = hash
215 bug.root = hash
216 }
217
218 // Write a Git tree referencing this blob
219 hash, err = repo.StoreTree([]repository.TreeEntry{
220 {repository.Blob, hash, OpsEntryName}, // the last pack of ops
221 {repository.Blob, root, RootEntryName}, // always the first pack of ops (might be the same)
222 })
223 if err != nil {
224 return err
225 }
226
227 // Write a Git commit referencing the tree, with the previous commit as parent
228 if bug.lastCommit != "" {
229 hash, err = repo.StoreCommitWithParent(hash, bug.lastCommit)
230 } else {
231 hash, err = repo.StoreCommit(hash)
232 }
233
234 if err != nil {
235 return err
236 }
237
238 bug.lastCommit = hash
239
240 // Create or update the Git reference for this bug
241 ref := fmt.Sprintf("%s%s", BugsRefPattern, bug.id)
242 err = repo.UpdateRef(ref, hash)
243
244 if err != nil {
245 return err
246 }
247
248 bug.packs = append(bug.packs, bug.staging)
249 bug.staging = OperationPack{}
250
251 return nil
252}
253
254func (bug *Bug) Id() string {
255 return bug.id
256}
257
258func (bug *Bug) HumanId() string {
259 return fmt.Sprintf("%.8s", bug.id)
260}
261
262func (bug *Bug) firstOp() Operation {
263 for _, pack := range bug.packs {
264 for _, op := range pack.Operations {
265 return op
266 }
267 }
268
269 if !bug.staging.IsEmpty() {
270 return bug.staging.Operations[0]
271 }
272
273 return nil
274}
275
276// Compile a bug in a easily usable snapshot
277func (bug *Bug) Compile() Snapshot {
278 snap := Snapshot{}
279
280 it := NewOperationIterator(bug)
281
282 for it.Next() {
283 snap = it.Value().Apply(snap)
284 }
285
286 return snap
287}