aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2018-07-14 22:17:37 +0200
committerMichael Muré <batolettre@gmail.com>2018-07-14 22:17:37 +0200
commitda470993d13ce63087034db9b7e8ffbdf18e87a5 (patch)
tree7846ad86de6d93c51c54bf3e764a2108baa63612
parentf8e07748743f7e66ff1adf101a797cb1bedfc140 (diff)
downloadgit-bug-da470993d13ce63087034db9b7e8ffbdf18e87a5.tar.gz
complete the storage/read process + tests (!)
-rw-r--r--bug/bug.go105
-rw-r--r--bug/operation.go4
-rw-r--r--bug/operation_pack.go30
-rw-r--r--bug/operations/create.go6
-rw-r--r--bug/operations/operation_test.go57
-rw-r--r--bug/operations/operations.go10
-rw-r--r--commands/new.go5
-rw-r--r--notes3
-rw-r--r--repository/git.go69
-rw-r--r--repository/mock_repo.go118
-rw-r--r--repository/repo.go51
-rw-r--r--tests/bug_test.go29
-rw-r--r--tests/operation_pack_test.go18
13 files changed, 400 insertions, 105 deletions
diff --git a/bug/bug.go b/bug/bug.go
index fc7c0c2a..9803a970 100644
--- a/bug/bug.go
+++ b/bug/bug.go
@@ -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
}
diff --git a/notes b/notes
index daefbcd8..0bdccee3 100644
--- a/notes
+++ b/notes
@@ -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
}