package bug
import (
"crypto/sha256"
"errors"
"fmt"
"github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util"
"github.com/kevinburke/go.uuid"
"strings"
)
const BugsRefPattern = "refs/bugs/"
const BugsRemoteRefPattern = "refs/remote/%s/bugs/"
const OpsEntryName = "ops"
const RootEntryName = "root"
// Bug hold the data of a bug thread, organized in a way close to
// how it will be persisted inside Git. This is the datastructure
// used for merge of two different version.
type Bug struct {
// Id used as unique identifier
id string
lastCommit util.Hash
root util.Hash
// TODO: need a way to order bugs, probably a Lamport clock
packs []OperationPack
staging OperationPack
}
// Create a new Bug
func NewBug() (*Bug, error) {
// TODO: replace with simple random bytes + hash
// Creating UUID Version 4
unique, err := uuid.ID4()
if err != nil {
return nil, err
}
// Use it as source of uniqueness
hash := sha256.New().Sum(unique.Bytes())
// format in hex and truncate to 40 char
id := fmt.Sprintf("%.40s", fmt.Sprintf("%x", hash))
return &Bug{
id: id,
}, nil
}
// Find an existing Bug matching a prefix
func FindBug(repo repository.Repo, prefix string) (*Bug, error) {
refs, err := repo.ListRefs(BugsRefPattern)
if err != nil {
return nil, err
}
// preallocate but empty
matching := make([]string, 0, 5)
for _, ref := range refs {
if strings.HasPrefix(ref, prefix) {
matching = append(matching, ref)
}
}
if len(matching) == 0 {
return nil, errors.New("No matching bug found.")
}
if len(matching) > 1 {
return nil, fmt.Errorf("Multiple matching bug found:\n%s", strings.Join(matching, "\n"))
}
return ReadBug(repo, matching[0])
}
// Read and parse a Bug from git
func ReadBug(repo repository.Repo, id string) (*Bug, error) {
hashes, err := repo.ListCommits(BugsRefPattern + id)
if err != nil {
return nil, err
}
bug := Bug{
id: id,
}
for _, hash := range hashes {
entries, err := repo.ListEntries(hash)
bug.lastCommit = hash
if err != nil {
return nil, err
}
var opsEntry repository.TreeEntry
opsFound := false
var rootEntry repository.TreeEntry
rootFound := false
for _, entry := range entries {
if entry.Name == OpsEntryName {
opsEntry = entry
opsFound = true
continue
}
if entry.Name == RootEntryName {
rootEntry = entry
rootFound = true
}
}
if !opsFound {
return nil, errors.New("Invalid tree, missing the ops entry")
}
if !rootFound {
return nil, errors.New("Invalid tree, missing the root entry")
}
if bug.root == "" {
bug.root = rootEntry.Hash
}
data, err := repo.ReadData(opsEntry.Hash)
if err != nil {
return nil, err
}
op, err := ParseOperationPack(data)
if err != nil {
return nil, err
}
bug.packs = append(bug.packs, *op)
}
return &bug, nil
}
// IsValid check if the Bug data is valid
func (bug *Bug) IsValid() bool {
// non-empty
if len(bug.packs) == 0 && bug.staging.IsEmpty() {
return false
}
// check if each pack is valid
for _, pack := range bug.packs {
if !pack.IsValid() {
return false
}
}
// check if staging is valid if needed
if !bug.staging.IsEmpty() {
if !bug.staging.IsValid() {
return false
}
}
// The very first Op should be a CREATE
firstOp := bug.firstOp()
if firstOp == nil || firstOp.OpType() != CREATE {
return false
}
// Check that there is no more CREATE op
it := NewOperationIterator(bug)
createCount := 0
for it.Next() {
if it.Value().OpType() == CREATE {
createCount++
}
}
if createCount != 1 {
return false
}
return true
}
func (bug *Bug) Append(op Operation) {
bug.staging.Append(op)
}
// Write the staging area in Git and move the operations to the packs
func (bug *Bug) Commit(repo repository.Repo) error {
if bug.staging.IsEmpty() {
return nil
}
// Write the Ops as a Git blob containing the serialized array
hash, err := bug.staging.Write(repo)
if err != nil {
return err
}
root := bug.root
if root == "" {
root = hash
bug.root = hash
}
// Write a Git tree referencing this blob
hash, err = repo.StoreTree([]repository.TreeEntry{
{repository.Blob, hash, OpsEntryName}, // the last pack of ops
{repository.Blob, root, RootEntryName}, // always the first pack of ops (might be the same)
})
if err != nil {
return err
}
// Write a Git commit referencing the tree, with the previous commit as parent
if bug.lastCommit != "" {
hash, err = repo.StoreCommitWithParent(hash, bug.lastCommit)
} else {
hash, err = repo.StoreCommit(hash)
}
if err != nil {
return err
}
bug.lastCommit = hash
// Create or update the Git reference for this bug
ref := fmt.Sprintf("%s%s", BugsRefPattern, bug.id)
err = repo.UpdateRef(ref, hash)
if err != nil {
return err
}
bug.packs = append(bug.packs, bug.staging)
bug.staging = OperationPack{}
return nil
}
func (bug *Bug) Id() string {
return bug.id
}
func (bug *Bug) HumanId() string {
return fmt.Sprintf("%.8s", bug.id)
}
func (bug *Bug) firstOp() Operation {
for _, pack := range bug.packs {
for _, op := range pack.Operations {
return op
}
}
if !bug.staging.IsEmpty() {
return bug.staging.Operations[0]
}
return nil
}
// Compile a bug in a easily usable snapshot
func (bug *Bug) Compile() Snapshot {
snap := Snapshot{}
it := NewOperationIterator(bug)
for it.Next() {
snap = it.Value().Apply(snap)
}
return snap
}