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)
11
12const BugsRefPattern = "refs/bugs/"
13const BugsRemoteRefPattern = "refs/remote/%s/bugs/"
14const OpsEntryName = "ops"
15const RootEntryName = "root"
16
17// Bug hold the data of a bug thread, organized in a way close to
18// how it will be persisted inside Git. This is the datastructure
19// used for merge of two different version.
20type Bug struct {
21 // Id used as unique identifier
22 id string
23
24 lastCommit util.Hash
25 root util.Hash
26
27 // TODO: need a way to order bugs, probably a Lamport clock
28
29 packs []OperationPack
30
31 staging OperationPack
32}
33
34// Create a new Bug
35func NewBug() (*Bug, error) {
36 // Creating UUID Version 4
37 unique, err := uuid.ID4()
38
39 if err != nil {
40 return nil, err
41 }
42
43 // Use it as source of uniqueness
44 hash := sha256.New().Sum(unique.Bytes())
45
46 // format in hex and truncate to 40 char
47 id := fmt.Sprintf("%.40s", fmt.Sprintf("%x", hash))
48
49 return &Bug{
50 id: id,
51 }, nil
52}
53
54// Read and parse a Bug from git
55func ReadBug(repo repository.Repo, id string) (*Bug, error) {
56 hashes, err := repo.ListCommits(BugsRefPattern + id)
57
58 if err != nil {
59 return nil, err
60 }
61
62 bug := Bug{
63 id: id,
64 }
65
66 for _, hash := range hashes {
67 entries, err := repo.ListEntries(hash)
68
69 bug.lastCommit = hash
70
71 if err != nil {
72 return nil, err
73 }
74
75 var opsEntry repository.TreeEntry
76 opsFound := false
77 var rootEntry repository.TreeEntry
78 rootFound := false
79
80 for _, entry := range entries {
81 if entry.Name == OpsEntryName {
82 opsEntry = entry
83 opsFound = true
84 continue
85 }
86 if entry.Name == RootEntryName {
87 rootEntry = entry
88 rootFound = true
89 }
90 }
91
92 if !opsFound {
93 return nil, errors.New("Invalid tree, missing the ops entry")
94 }
95
96 if !rootFound {
97 return nil, errors.New("Invalid tree, missing the root entry")
98 }
99
100 if bug.root == "" {
101 bug.root = rootEntry.Hash
102 }
103
104 data, err := repo.ReadData(opsEntry.Hash)
105
106 if err != nil {
107 return nil, err
108 }
109
110 op, err := ParseOperationPack(data)
111
112 if err != nil {
113 return nil, err
114 }
115
116 bug.packs = append(bug.packs, *op)
117 }
118
119 return &bug, nil
120}
121
122// IsValid check if the Bug data is valid
123func (bug *Bug) IsValid() bool {
124 // non-empty
125 if len(bug.packs) == 0 && bug.staging.IsEmpty() {
126 return false
127 }
128
129 // check if each pack is valid
130 for _, pack := range bug.packs {
131 if !pack.IsValid() {
132 return false
133 }
134 }
135
136 // check if staging is valid if needed
137 if !bug.staging.IsEmpty() {
138 if !bug.staging.IsValid() {
139 return false
140 }
141 }
142
143 // The very first Op should be a CREATE
144 firstOp := bug.firstOp()
145 if firstOp == nil || firstOp.OpType() != CREATE {
146 return false
147 }
148
149 // Check that there is no more CREATE op
150 it := NewOperationIterator(bug)
151 createCount := 0
152 for it.Next() {
153 if it.Value().OpType() == CREATE {
154 createCount++
155 }
156 }
157
158 if createCount != 1 {
159 return false
160 }
161
162 return true
163}
164
165func (bug *Bug) Append(op Operation) {
166 bug.staging.Append(op)
167}
168
169// Write the staging area in Git and move the operations to the packs
170func (bug *Bug) Commit(repo repository.Repo) error {
171 if bug.staging.IsEmpty() {
172 return nil
173 }
174
175 // Write the Ops as a Git blob containing the serialized array
176 hash, err := bug.staging.Write(repo)
177 if err != nil {
178 return err
179 }
180
181 root := bug.root
182 if root == "" {
183 root = hash
184 bug.root = hash
185 }
186
187 // Write a Git tree referencing this blob
188 hash, err = repo.StoreTree([]repository.TreeEntry{
189 {repository.Blob, hash, OpsEntryName}, // the last pack of ops
190 {repository.Blob, root, RootEntryName}, // always the first pack of ops (might be the same)
191 })
192 if err != nil {
193 return err
194 }
195
196 // Write a Git commit referencing the tree, with the previous commit as parent
197 if bug.lastCommit != "" {
198 hash, err = repo.StoreCommitWithParent(hash, bug.lastCommit)
199 } else {
200 hash, err = repo.StoreCommit(hash)
201 }
202
203 if err != nil {
204 return err
205 }
206
207 bug.lastCommit = hash
208
209 // Create or update the Git reference for this bug
210 ref := fmt.Sprintf("%s%s", BugsRefPattern, bug.id)
211 err = repo.UpdateRef(ref, hash)
212
213 if err != nil {
214 return err
215 }
216
217 bug.packs = append(bug.packs, bug.staging)
218 bug.staging = OperationPack{}
219
220 return nil
221}
222
223func (bug *Bug) Id() string {
224 return bug.id
225}
226
227func (bug *Bug) HumanId() string {
228 return fmt.Sprintf("%.8s", bug.id)
229}
230
231func (bug *Bug) firstOp() Operation {
232 for _, pack := range bug.packs {
233 for _, op := range pack.Operations {
234 return op
235 }
236 }
237
238 if !bug.staging.IsEmpty() {
239 return bug.staging.Operations[0]
240 }
241
242 return nil
243}
244
245// Compile a bug in a easily usable snapshot
246func (bug *Bug) Compile() Snapshot {
247 snap := Snapshot{}
248
249 it := NewOperationIterator(bug)
250
251 for it.Next() {
252 snap = it.Value().Apply(snap)
253 }
254
255 return snap
256}