bug.go

  1package bug
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"github.com/MichaelMure/git-bug/repository"
  7	"github.com/MichaelMure/git-bug/util"
  8	"github.com/kevinburke/go.uuid"
  9)
 10
 11const BugsRefPattern = "refs/bugs/"
 12const BugsRemoteRefPattern = "refs/remote/%s/bugs/"
 13const OpsEntryName = "ops"
 14const RootEntryName = "root"
 15
 16// Bug hold the data of a bug thread, organized in a way close to
 17// how it will be persisted inside Git. This is the datastructure
 18// used for merge of two different version.
 19type Bug struct {
 20	// Id used as unique identifier
 21	id uuid.UUID
 22
 23	lastCommit util.Hash
 24	root       util.Hash
 25
 26	// TODO: need a way to order bugs, probably a Lamport clock
 27
 28	packs []OperationPack
 29
 30	staging OperationPack
 31}
 32
 33// Create a new Bug
 34func NewBug() (*Bug, error) {
 35	// Creating UUID Version 4
 36	id, err := uuid.ID4()
 37
 38	if err != nil {
 39		return nil, err
 40	}
 41
 42	return &Bug{
 43		id:         id,
 44		lastCommit: "",
 45	}, nil
 46}
 47
 48// Read and parse a Bug from git
 49func ReadBug(repo repository.Repo, id string) (*Bug, error) {
 50	hashes, err := repo.ListCommits(BugsRefPattern + id)
 51
 52	if err != nil {
 53		return nil, err
 54	}
 55
 56	parsedId, err := uuid.FromString(id)
 57
 58	if err != nil {
 59		return nil, err
 60	}
 61
 62	bug := Bug{
 63		id: parsedId,
 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.String())
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 fmt.Sprintf("%x", 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}