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 commit hash of (first commit + some random) // 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 }