aboutsummaryrefslogblamecommitdiffstats
path: root/bug/bug.go
blob: c5268cc23a78680892a26c4bbade46488f9b0207 (plain) (tree)
1
2
3
4
5
6
7
8
9

           
        
                       
                


                                                   
                                       
                 

 
                                   
                                                   

                            
 




                                                                 
                 

                            
                            
 
                                                                   
 
                             
 
                             
 
 

                             
                                                                         
 
                                  
                                 




                               





                                                           
                    
                       


              

                                                                 
                                                 







                                        


                                                       










                                                                                                        
                                                        

 
                                

                                                              




                               


                                              
                   
                       

         
                                  

















































                                                                                      






                                                    





                                                  


                                         
                                                         



                                      
                                        




                                    


                                              



                                    
                                               

                                                         



















                                                  
                              

 
                                                                     










                                                                      


                           
                               

         
                                                 


                                                                                                           















                                                                                      

                             
                                                          
                                                          









                                                  

 














































































                                                                                                   
                             
                     

 
                                                            
                                  
                                          
 
 

                                                     
                                     
                                        




                                                    

                                                



                  












                                             
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) {
	ids, err := repo.ListRefs(BugsRefPattern)

	if err != nil {
		return nil, err
	}

	// preallocate but empty
	matching := make([]string, 0, 5)

	for _, id := range ids {
		if strings.HasPrefix(id, prefix) {
			matching = append(matching, id)
		}
	}

	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, BugsRefPattern+matching[0])
}

// Read and parse a Bug from git
func ReadBug(repo repository.Repo, ref string) (*Bug, error) {
	hashes, err := repo.ListCommits(ref)

	if err != nil {
		return nil, err
	}

	refSplitted := strings.Split(ref, "/")
	id := refSplitted[len(refSplitted)-1]

	bug := Bug{
		id: id,
	}

	// Load each OperationPack
	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
		}

		// tag the pack with the commit hash
		op.commitHash = hash

		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
}

// Merge a different version of the same bug by rebasing operations of this bug
// that are not present in the other on top of the chain of operations of the
// other version.
func (bug *Bug) Merge(repo repository.Repo, other *Bug) (bool, error) {

	if bug.id != other.id {
		return false, errors.New("merging unrelated bugs is not supported")
	}

	if len(other.staging.Operations) > 0 {
		return false, errors.New("merging a bug with a non-empty staging is not supported")
	}

	if bug.lastCommit == "" || other.lastCommit == "" {
		return false, errors.New("can't merge a bug that has never been stored")
	}

	ancestor, err := repo.FindCommonAncestor(bug.lastCommit, other.lastCommit)

	if err != nil {
		return false, err
	}

	rebaseStarted := false
	updated := false

	for i, pack := range bug.packs {
		if pack.commitHash == ancestor {
			rebaseStarted = true

			// get other bug's extra pack
			for j := i + 1; j < len(other.packs); j++ {
				// clone is probably not necessary
				newPack := other.packs[j].Clone()

				bug.packs = append(bug.packs, newPack)
				bug.lastCommit = newPack.commitHash
				updated = true
			}

			continue
		}

		if !rebaseStarted {
			continue
		}

		updated = true

		// get the referenced git tree
		treeHash, err := repo.GetTreeHash(pack.commitHash)

		if err != nil {
			return false, err
		}

		// create a new commit with the correct ancestor
		hash, err := repo.StoreCommitWithParent(treeHash, bug.lastCommit)

		// replace the pack
		bug.packs[i] = pack.Clone()
		bug.packs[i].commitHash = hash

		// update the bug
		bug.lastCommit = hash
	}

	// Update the git ref
	if updated {
		err := repo.UpdateRef(BugsRefPattern+bug.id, bug.lastCommit)
		if err != nil {
			return false, err
		}
	}

	return updated, nil
}

// Return the Bug identifier
func (bug *Bug) Id() string {
	return bug.id
}

// Return the Bug identifier truncated for human consumption
func (bug *Bug) HumanId() string {
	return fmt.Sprintf("%.8s", bug.id)
}

// Lookup for the very first operation of the bug.
// For a valid Bug, this operation should be a CREATE
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
}