bug.go

  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}