diff options
-rw-r--r-- | bug/bug.go | 105 | ||||
-rw-r--r-- | bug/operation.go | 4 | ||||
-rw-r--r-- | bug/operation_pack.go | 30 | ||||
-rw-r--r-- | bug/operations/create.go | 6 | ||||
-rw-r--r-- | bug/operations/operation_test.go | 57 | ||||
-rw-r--r-- | bug/operations/operations.go | 10 | ||||
-rw-r--r-- | commands/new.go | 5 | ||||
-rw-r--r-- | notes | 3 | ||||
-rw-r--r-- | repository/git.go | 69 | ||||
-rw-r--r-- | repository/mock_repo.go | 118 | ||||
-rw-r--r-- | repository/repo.go | 51 | ||||
-rw-r--r-- | tests/bug_test.go | 29 | ||||
-rw-r--r-- | tests/operation_pack_test.go | 18 |
13 files changed, 400 insertions, 105 deletions
@@ -1,6 +1,7 @@ package bug import ( + "errors" "fmt" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util" @@ -9,6 +10,8 @@ import ( 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 @@ -42,6 +45,80 @@ func NewBug() (*Bug, error) { }, nil } +// 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 + } + + parsedId, err := uuid.FromString(id) + + if err != nil { + return nil, err + } + + bug := Bug{ + id: parsedId, + } + + 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 @@ -104,12 +181,13 @@ func (bug *Bug) Commit(repo repository.Repo) error { root := bug.root if root == "" { root = hash + bug.root = hash } // Write a Git tree referencing this blob - hash, err = repo.StoreTree(map[string]util.Hash{ - "ops": hash, // the last pack of ops - "root": root, // always the first pack of ops (might be the same) + 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 @@ -126,6 +204,8 @@ func (bug *Bug) Commit(repo repository.Repo) error { return err } + bug.lastCommit = hash + // Create or update the Git reference for this bug ref := fmt.Sprintf("%s%s", BugsRefPattern, bug.id.String()) err = repo.UpdateRef(ref, hash) @@ -140,8 +220,12 @@ func (bug *Bug) Commit(repo repository.Repo) error { return nil } +func (bug *Bug) Id() string { + return fmt.Sprintf("%x", bug.id) +} + func (bug *Bug) HumanId() string { - return bug.id.String() + return fmt.Sprintf("%.8s", bug.Id()) } func (bug *Bug) firstOp() Operation { @@ -157,3 +241,16 @@ func (bug *Bug) firstOp() Operation { 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 +} diff --git a/bug/operation.go b/bug/operation.go index 591c7176..4414f2ad 100644 --- a/bug/operation.go +++ b/bug/operation.go @@ -3,7 +3,7 @@ package bug type OperationType int const ( - UNKNOW OperationType = iota + UNKNOWN OperationType = iota CREATE SET_TITLE ADD_COMMENT @@ -15,7 +15,7 @@ type Operation interface { } type OpBase struct { - OperationType OperationType `json:"op"` + OperationType OperationType } func (op OpBase) OpType() OperationType { diff --git a/bug/operation_pack.go b/bug/operation_pack.go index 67a2a072..60016474 100644 --- a/bug/operation_pack.go +++ b/bug/operation_pack.go @@ -1,7 +1,8 @@ package bug import ( - "encoding/json" + "bytes" + "encoding/gob" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util" ) @@ -13,22 +14,35 @@ import ( // inside Git to form the complete ordered chain of operation to // apply to get the final state of the Bug type OperationPack struct { - Operations []Operation `json:"ops"` - hash util.Hash + Operations []Operation } -func Parse() (OperationPack, error) { - // TODO - return OperationPack{}, nil +func ParseOperationPack(data []byte) (*OperationPack, error) { + reader := bytes.NewReader(data) + decoder := gob.NewDecoder(reader) + + var opp OperationPack + + err := decoder.Decode(&opp) + + if err != nil { + return nil, err + } + + return &opp, nil } func (opp *OperationPack) Serialize() ([]byte, error) { - jsonBytes, err := json.Marshal(*opp) + var data bytes.Buffer + + encoder := gob.NewEncoder(&data) + err := encoder.Encode(*opp) + if err != nil { return nil, err } - return jsonBytes, nil + return data.Bytes(), nil } // Append a new operation to the pack diff --git a/bug/operations/create.go b/bug/operations/create.go index 1c34f85d..49b648a2 100644 --- a/bug/operations/create.go +++ b/bug/operations/create.go @@ -11,9 +11,9 @@ var _ bug.Operation = CreateOperation{} type CreateOperation struct { bug.OpBase - Title string `json:"t"` - Message string `json:"m"` - Author bug.Person `json:"a"` + Title string + Message string + Author bug.Person } func NewCreateOp(author bug.Person, title, message string) CreateOperation { diff --git a/bug/operations/operation_test.go b/bug/operations/operation_test.go deleted file mode 100644 index e53e524b..00000000 --- a/bug/operations/operation_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package operations - -import ( - "github.com/MichaelMure/git-bug/bug" - "testing" -) - -// Different type with the same fields -type CreateOperation2 struct { - Title string - Message string -} - -func (op CreateOperation2) OpType() bug.OperationType { - return bug.UNKNOW -} - -func (op CreateOperation2) Apply(snapshot bug.Snapshot) bug.Snapshot { - // no-op - return snapshot -} - -func TestOperationsEquality(t *testing.T) { - var rene = bug.Person{ - Name: "René Descartes", - Email: "rene@descartes.fr", - } - - var A bug.Operation = NewCreateOp(rene, "title", "message") - var B bug.Operation = NewCreateOp(rene, "title", "message") - var C bug.Operation = NewCreateOp(rene, "title", "different message") - - if A != B { - t.Fatal("Equal value ops should be tested equals") - } - - if A == C { - t.Fatal("Different value ops should be tested different") - } - - D := CreateOperation2{Title: "title", Message: "message"} - - if A == D { - t.Fatal("Operations equality should handle the type") - } - - var isaac = bug.Person{ - Name: "Isaac Newton", - Email: "isaac@newton.uk", - } - - var E bug.Operation = NewCreateOp(isaac, "title", "message") - - if A == E { - t.Fatal("Operation equality should handle the author") - } -} diff --git a/bug/operations/operations.go b/bug/operations/operations.go new file mode 100644 index 00000000..f42d6e9a --- /dev/null +++ b/bug/operations/operations.go @@ -0,0 +1,10 @@ +package operations + +import "encoding/gob" + +// Package initialisation used to register operation's type for (de)serialization +func init() { + gob.Register(AddCommentOperation{}) + gob.Register(CreateOperation{}) + gob.Register(SetTitleOperation{}) +} diff --git a/commands/new.go b/commands/new.go index ab237e32..d2e1ed59 100644 --- a/commands/new.go +++ b/commands/new.go @@ -59,9 +59,10 @@ func RunNewBug(repo repository.Repo, args []string) error { createOp := operations.NewCreateOp(author, title, *newMessage) newbug.Append(createOp) - newbug.Commit(repo) - return nil + err = newbug.Commit(repo) + + return err } @@ -19,6 +19,9 @@ git show-ref refs/bug git update-index --add --cacheinfo 100644 83baae61804e65cc73a7201a7252750c76066a30 test.txt +git show-ref --hash refs/bugs/4ef19f8a-2e6a-45f7-910e-52e3c639cd86 + +git for-each-ref --format="%(refname)" "refs/bugs/*" Bug operations: diff --git a/repository/git.go b/repository/git.go index a55e451c..50806778 100644 --- a/repository/git.go +++ b/repository/git.go @@ -134,15 +134,26 @@ func (repo *GitRepo) StoreData(data []byte) (util.Hash, error) { return util.Hash(stdout), err } -// StoreTree will store a mapping key-->Hash as a Git tree -func (repo *GitRepo) StoreTree(mapping map[string]util.Hash) (util.Hash, error) { - var buffer bytes.Buffer +// ReadData will attempt to read arbitrary data from the given hash +func (repo *GitRepo) ReadData(hash util.Hash) ([]byte, error) { + var stdout bytes.Buffer + var stderr bytes.Buffer - for key, hash := range mapping { - buffer.WriteString(fmt.Sprintf("100644 blob %s\t%s\n", hash, key)) + err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "cat-file", "-p", string(hash)) + + if err != nil { + return []byte{}, err } + return stdout.Bytes(), nil +} + +// StoreTree will store a mapping key-->Hash as a Git tree +func (repo *GitRepo) StoreTree(entries []TreeEntry) (util.Hash, error) { + buffer := prepareTreeEntries(entries) + stdout, err := repo.runGitCommandWithStdin(&buffer, "mktree") + if err != nil { return "", err } @@ -179,3 +190,51 @@ func (repo *GitRepo) UpdateRef(ref string, hash util.Hash) error { return err } + +// ListRefs will return a list of Git ref matching the given refspec +func (repo *GitRepo) ListRefs(refspec string) ([]string, error) { + // the format option will strip the ref name to keep only the last part (ie, the bug id) + stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname:lstrip=-1)", refspec) + + if err != nil { + return nil, err + } + + splitted := strings.Split(stdout, "\n") + + if len(splitted) == 1 && splitted[0] == "" { + return []string{}, nil + } + + return splitted, nil +} + +// ListCommits will return the list of commit hashes of a ref, in chronological order +func (repo *GitRepo) ListCommits(ref string) ([]util.Hash, error) { + stdout, err := repo.runGitCommand("rev-list", "--first-parent", "--reverse", ref) + + if err != nil { + return nil, err + } + + splitted := strings.Split(stdout, "\n") + + casted := make([]util.Hash, len(splitted)) + for i, line := range splitted { + casted[i] = util.Hash(line) + } + + return casted, nil + +} + +// ListEntries will return the list of entries in a Git tree +func (repo *GitRepo) ListEntries(hash util.Hash) ([]TreeEntry, error) { + stdout, err := repo.runGitCommand("ls-tree", string(hash)) + + if err != nil { + return nil, err + } + + return readTreeEntries(stdout) +} diff --git a/repository/mock_repo.go b/repository/mock_repo.go index f9b070b4..f526c3dc 100644 --- a/repository/mock_repo.go +++ b/repository/mock_repo.go @@ -1,14 +1,32 @@ package repository import ( + "crypto/sha1" + "fmt" "github.com/MichaelMure/git-bug/util" + "github.com/pkg/errors" ) // mockRepoForTest defines an instance of Repo that can be used for testing. -type mockRepoForTest struct{} +type mockRepoForTest struct { + blobs map[util.Hash][]byte + trees map[util.Hash]string + commits map[util.Hash]commit + refs map[string]util.Hash +} + +type commit struct { + treeHash util.Hash + parent util.Hash +} func NewMockRepoForTest() Repo { - return &mockRepoForTest{} + return &mockRepoForTest{ + blobs: make(map[util.Hash][]byte), + trees: make(map[util.Hash]string), + commits: make(map[util.Hash]commit), + refs: make(map[string]util.Hash), + } } // GetPath returns the path to the repo. @@ -39,22 +57,106 @@ func (r *mockRepoForTest) PullRefs(remote string, refPattern string, remoteRefPa return nil } -func (r *mockRepoForTest) StoreData([]byte) (util.Hash, error) { - return "", nil +func (r *mockRepoForTest) StoreData(data []byte) (util.Hash, error) { + rawHash := sha1.Sum(data) + hash := util.Hash(fmt.Sprintf("%x", rawHash)) + r.blobs[hash] = data + return hash, nil +} + +func (r *mockRepoForTest) ReadData(hash util.Hash) ([]byte, error) { + data, ok := r.blobs[hash] + + if !ok { + return nil, errors.New("unknown hash") + } + + return data, nil } -func (r *mockRepoForTest) StoreTree(mapping map[string]util.Hash) (util.Hash, error) { - return "", nil +func (r *mockRepoForTest) StoreTree(entries []TreeEntry) (util.Hash, error) { + buffer := prepareTreeEntries(entries) + rawHash := sha1.Sum(buffer.Bytes()) + hash := util.Hash(fmt.Sprintf("%x", rawHash)) + r.trees[hash] = buffer.String() + + return hash, nil } func (r *mockRepoForTest) StoreCommit(treeHash util.Hash) (util.Hash, error) { - return "", nil + rawHash := sha1.Sum([]byte(treeHash)) + hash := util.Hash(fmt.Sprintf("%x", rawHash)) + r.commits[hash] = commit{ + treeHash: treeHash, + } + return hash, nil } func (r *mockRepoForTest) StoreCommitWithParent(treeHash util.Hash, parent util.Hash) (util.Hash, error) { - return "", nil + rawHash := sha1.Sum([]byte(treeHash + parent)) + hash := util.Hash(fmt.Sprintf("%x", rawHash)) + r.commits[hash] = commit{ + treeHash: treeHash, + parent: parent, + } + return hash, nil } func (r *mockRepoForTest) UpdateRef(ref string, hash util.Hash) error { + r.refs[ref] = hash return nil } + +func (r *mockRepoForTest) ListRefs(refspec string) ([]string, error) { + keys := make([]string, len(r.refs)) + + i := 0 + for k := range r.refs { + keys[i] = k + i++ + } + + return keys, nil +} + +func (r *mockRepoForTest) ListCommits(ref string) ([]util.Hash, error) { + var hashes []util.Hash + + hash := r.refs[ref] + + for { + commit, ok := r.commits[hash] + + if !ok { + break + } + + hashes = append([]util.Hash{hash}, hashes...) + hash = commit.parent + } + + return hashes, nil +} + +func (r *mockRepoForTest) ListEntries(hash util.Hash) ([]TreeEntry, error) { + var data string + + data, ok := r.trees[hash] + + if !ok { + // Git will understand a commit hash to reach a tree + commit, ok := r.commits[hash] + + if !ok { + return nil, errors.New("unknown hash") + } + + data, ok = r.trees[commit.treeHash] + + if !ok { + return nil, errors.New("unknown hash") + } + } + + return readTreeEntries(data) +} diff --git a/repository/repo.go b/repository/repo.go index a58f35d9..26fe0fa6 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -1,7 +1,11 @@ // Package repository contains helper methods for working with a Git repo. package repository -import "github.com/MichaelMure/git-bug/util" +import ( + "bytes" + "github.com/MichaelMure/git-bug/util" + "strings" +) // Repo represents a source code repository. type Repo interface { @@ -26,8 +30,11 @@ type Repo interface { // StoreData will store arbitrary data and return the corresponding hash StoreData(data []byte) (util.Hash, error) + // ReadData will attempt to read arbitrary data from the given hash + ReadData(hash util.Hash) ([]byte, error) + // StoreTree will store a mapping key-->Hash as a Git tree - StoreTree(mapping map[string]util.Hash) (util.Hash, error) + StoreTree(mapping []TreeEntry) (util.Hash, error) // StoreCommit will store a Git commit with the given Git tree StoreCommit(treeHash util.Hash) (util.Hash, error) @@ -37,4 +44,44 @@ type Repo interface { // UpdateRef will create or update a Git reference UpdateRef(ref string, hash util.Hash) error + + // ListRefs will return a list of Git ref matching the given refspec + ListRefs(refspec string) ([]string, error) + + // ListCommits will return the list of tree hashes of a ref, in chronological order + ListCommits(ref string) ([]util.Hash, error) + + // ListEntries will return the list of entries in a Git tree + ListEntries(hash util.Hash) ([]TreeEntry, error) +} + +func prepareTreeEntries(entries []TreeEntry) bytes.Buffer { + var buffer bytes.Buffer + + for _, entry := range entries { + buffer.WriteString(entry.Format()) + } + + return buffer +} + +func readTreeEntries(s string) ([]TreeEntry, error) { + splitted := strings.Split(s, "\n") + + casted := make([]TreeEntry, len(splitted)) + for i, line := range splitted { + if line == "" { + continue + } + + entry, err := ParseTreeEntry(line) + + if err != nil { + return nil, err + } + + casted[i] = entry + } + + return casted, nil } diff --git a/tests/bug_test.go b/tests/bug_test.go index ab7803f9..2fb79be5 100644 --- a/tests/bug_test.go +++ b/tests/bug_test.go @@ -2,6 +2,8 @@ package tests import ( "github.com/MichaelMure/git-bug/bug" + "github.com/MichaelMure/git-bug/repository" + "reflect" "testing" ) @@ -11,7 +13,7 @@ func TestBugId(t *testing.T) { t.Error(err) } - if len(bug1.HumanId()) == 0 { + if len(bug1.Id()) == 0 { t.Fatal("Bug doesn't have a human readable identifier") } } @@ -44,3 +46,28 @@ func TestBugValidity(t *testing.T) { t.Fatal("Bug with multiple CREATE should be invalid") } } + +func TestBugSerialisation(t *testing.T) { + bug1, err := bug.NewBug() + if err != nil { + t.Error(err) + } + + bug1.Append(createOp) + bug1.Append(setTitleOp) + bug1.Append(setTitleOp) + bug1.Append(addCommentOp) + + repo := repository.NewMockRepoForTest() + + bug1.Commit(repo) + + bug2, err := bug.ReadBug(repo, bug1.Id()) + if err != nil { + t.Error(err) + } + + if !reflect.DeepEqual(bug1, bug2) { + t.Fatalf("%s different than %s", bug1, bug2) + } +} diff --git a/tests/operation_pack_test.go b/tests/operation_pack_test.go index 2b19e364..35b77a8f 100644 --- a/tests/operation_pack_test.go +++ b/tests/operation_pack_test.go @@ -1,9 +1,6 @@ package tests import ( - "bytes" - "encoding/json" - "fmt" "github.com/MichaelMure/git-bug/bug" "testing" ) @@ -15,24 +12,19 @@ func TestOperationPackSerialize(t *testing.T) { opp.Append(setTitleOp) opp.Append(addCommentOp) - jsonBytes, err := opp.Serialize() + data, err := opp.Serialize() if err != nil { t.Fatal(err) } - if len(jsonBytes) == 0 { - t.Fatal("empty json") + if len(data) == 0 { + t.Fatal("empty serialized data") } - fmt.Println(prettyPrintJSON(jsonBytes)) -} + _, err = bug.ParseOperationPack(data) -func prettyPrintJSON(jsonBytes []byte) (string, error) { - var prettyBytes bytes.Buffer - err := json.Indent(&prettyBytes, jsonBytes, "", " ") if err != nil { - return "", err + t.Fatal(err) } - return prettyBytes.String(), nil } |