aboutsummaryrefslogtreecommitdiffstats
path: root/bug
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2019-03-01 23:17:57 +0100
committerGitHub <noreply@github.com>2019-03-01 23:17:57 +0100
commit7260ca05bc3588c0572887a7d8f1b897c7fc13da (patch)
tree66854358df3cb9de651f7688556ec5a4b8ab1868 /bug
parent0aefae6fcca5786f2c898029c3d6282f760f2c63 (diff)
parentb6bed784e5664819250aac20b2b9690879ee6ab1 (diff)
downloadgit-bug-7260ca05bc3588c0572887a7d8f1b897c7fc13da.tar.gz
Merge pull request #89 from MichaelMure/identity
WIP identity in git
Diffstat (limited to 'bug')
-rw-r--r--bug/bug.go48
-rw-r--r--bug/bug_actions.go62
-rw-r--r--bug/bug_actions_test.go343
-rw-r--r--bug/bug_test.go57
-rw-r--r--bug/comment.go6
-rw-r--r--bug/identity.go27
-rw-r--r--bug/label.go2
-rw-r--r--bug/op_add_comment.go63
-rw-r--r--bug/op_add_comment_test.go25
-rw-r--r--bug/op_create.go68
-rw-r--r--bug/op_create_test.go37
-rw-r--r--bug/op_edit_comment.go69
-rw-r--r--bug/op_edit_comment_test.go34
-rw-r--r--bug/op_label_change.go66
-rw-r--r--bug/op_label_change_test.go25
-rw-r--r--bug/op_noop.go53
-rw-r--r--bug/op_noop_test.go25
-rw-r--r--bug/op_set_metadata.go68
-rw-r--r--bug/op_set_metadata_test.go35
-rw-r--r--bug/op_set_status.go63
-rw-r--r--bug/op_set_status_test.go25
-rw-r--r--bug/op_set_title.go66
-rw-r--r--bug/op_set_title_test.go25
-rw-r--r--bug/operation.go64
-rw-r--r--bug/operation_iterator_test.go40
-rw-r--r--bug/operation_pack.go47
-rw-r--r--bug/operation_pack_test.go44
-rw-r--r--bug/operation_test.go25
-rw-r--r--bug/person.go95
-rw-r--r--bug/snapshot.go3
-rw-r--r--bug/time.go9
-rw-r--r--bug/timeline.go12
32 files changed, 1135 insertions, 496 deletions
diff --git a/bug/bug.go b/bug/bug.go
index b6d09c50..f1bd1114 100644
--- a/bug/bug.go
+++ b/bug/bug.go
@@ -6,10 +6,12 @@ import (
"fmt"
"strings"
+ "github.com/pkg/errors"
+
+ "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/git"
"github.com/MichaelMure/git-bug/util/lamport"
- "github.com/pkg/errors"
)
const bugsRefPattern = "refs/bugs/"
@@ -113,13 +115,6 @@ func ReadRemoteBug(repo repository.ClockedRepo, remote string, id string) (*Bug,
// readBug will read and parse a Bug from git
func readBug(repo repository.ClockedRepo, ref string) (*Bug, error) {
- hashes, err := repo.ListCommits(ref)
-
- // TODO: this is not perfect, it might be a command invoke error
- if err != nil {
- return nil, ErrBugNotExist
- }
-
refSplit := strings.Split(ref, "/")
id := refSplit[len(refSplit)-1]
@@ -127,6 +122,13 @@ func readBug(repo repository.ClockedRepo, ref string) (*Bug, error) {
return nil, fmt.Errorf("invalid ref length")
}
+ hashes, err := repo.ListCommits(ref)
+
+ // TODO: this is not perfect, it might be a command invoke error
+ if err != nil {
+ return nil, ErrBugNotExist
+ }
+
bug := Bug{
id: id,
}
@@ -217,6 +219,13 @@ func readBug(repo repository.ClockedRepo, ref string) (*Bug, error) {
bug.packs = append(bug.packs, *opp)
}
+ // Make sure that the identities are properly loaded
+ resolver := identity.NewSimpleResolver(repo)
+ err = bug.EnsureIdentities(resolver)
+ if err != nil {
+ return nil, err
+ }
+
return &bug, nil
}
@@ -312,6 +321,11 @@ func (bug *Bug) Validate() error {
return fmt.Errorf("first operation should be a Create op")
}
+ // The bug ID should be the hash of the first commit
+ if len(bug.packs) > 0 && string(bug.packs[0].commitHash) != bug.id {
+ return fmt.Errorf("bug id should be the first commit hash")
+ }
+
// Check that there is no more CreateOp op
it := NewOperationIterator(bug)
createCount := 0
@@ -340,7 +354,8 @@ func (bug *Bug) HasPendingOp() bool {
// Commit write the staging area in Git and move the operations to the packs
func (bug *Bug) Commit(repo repository.ClockedRepo) error {
- if bug.staging.IsEmpty() {
+
+ if !bug.NeedCommit() {
return fmt.Errorf("can't commit a bug with no pending operation")
}
@@ -450,12 +465,24 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error {
return err
}
+ bug.staging.commitHash = hash
bug.packs = append(bug.packs, bug.staging)
bug.staging = OperationPack{}
return nil
}
+func (bug *Bug) CommitAsNeeded(repo repository.ClockedRepo) error {
+ if !bug.NeedCommit() {
+ return nil
+ }
+ return bug.Commit(repo)
+}
+
+func (bug *Bug) NeedCommit() bool {
+ return !bug.staging.IsEmpty()
+}
+
func makeMediaTree(pack OperationPack) []repository.TreeEntry {
var tree []repository.TreeEntry
counter := 0
@@ -504,9 +531,8 @@ func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) {
}
ancestor, err := repo.FindCommonAncestor(bug.lastCommit, otherBug.lastCommit)
-
if err != nil {
- return false, err
+ return false, errors.Wrap(err, "can't find common ancestor")
}
ancestorIndex := 0
diff --git a/bug/bug_actions.go b/bug/bug_actions.go
index 487ba25e..20360200 100644
--- a/bug/bug_actions.go
+++ b/bug/bug_actions.go
@@ -4,29 +4,68 @@ import (
"fmt"
"strings"
+ "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
"github.com/pkg/errors"
)
-// Fetch retrieve update from a remote
+// Note:
+//
+// For the actions (fetch/push/pull/merge/commit), this package act as a master for
+// the identity package and will also drive the needed identity actions. That is,
+// if bug.Push() is called, identity.Push will also be called to make sure that
+// the dependant identities are also present and up to date on the remote.
+//
+// I'm not entirely sure this is the correct way to do it, as it might introduce
+// too much complexity and hard coupling, but it does make this package easier
+// to use.
+
+// Fetch retrieve updates from a remote
// This does not change the local bugs state
func Fetch(repo repository.Repo, remote string) (string, error) {
+ stdout, err := identity.Fetch(repo, remote)
+ if err != nil {
+ return stdout, err
+ }
+
remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote)
fetchRefSpec := fmt.Sprintf("%s*:%s*", bugsRefPattern, remoteRefSpec)
- return repo.FetchRefs(remote, fetchRefSpec)
+ stdout2, err := repo.FetchRefs(remote, fetchRefSpec)
+
+ return stdout + "\n" + stdout2, err
}
// Push update a remote with the local changes
func Push(repo repository.Repo, remote string) (string, error) {
- return repo.PushRefs(remote, bugsRefPattern+"*")
+ stdout, err := identity.Push(repo, remote)
+ if err != nil {
+ return stdout, err
+ }
+
+ stdout2, err := repo.PushRefs(remote, bugsRefPattern+"*")
+
+ return stdout + "\n" + stdout2, err
}
// Pull will do a Fetch + MergeAll
-// This function won't give details on the underlying process. If you need more
-// use Fetch and MergeAll separately.
+// This function will return an error if a merge fail
func Pull(repo repository.ClockedRepo, remote string) error {
- _, err := Fetch(repo, remote)
+ _, err := identity.Fetch(repo, remote)
+ if err != nil {
+ return err
+ }
+
+ for merge := range identity.MergeAll(repo, remote) {
+ if merge.Err != nil {
+ return merge.Err
+ }
+ if merge.Status == identity.MergeStatusInvalid {
+ return errors.Errorf("merge failure: %s", merge.Reason)
+ }
+ }
+
+ _, err = Fetch(repo, remote)
if err != nil {
return err
}
@@ -35,12 +74,21 @@ func Pull(repo repository.ClockedRepo, remote string) error {
if merge.Err != nil {
return merge.Err
}
+ if merge.Status == MergeStatusInvalid {
+ return errors.Errorf("merge failure: %s", merge.Reason)
+ }
}
return nil
}
-// MergeAll will merge all the available remote bug
+// MergeAll will merge all the available remote bug:
+//
+// - If the remote has new commit, the local bug is updated to match the same history
+// (fast-forward update)
+// - if the local bug has new commits but the remote don't, nothing is changed
+// - if both local and remote bug have new commits (that is, we have a concurrent edition),
+// new local commits are rewritten at the head of the remote history (that is, a rebase)
func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult {
out := make(chan MergeResult)
diff --git a/bug/bug_actions_test.go b/bug/bug_actions_test.go
index a60e5c68..345b5e9a 100644
--- a/bug/bug_actions_test.go
+++ b/bug/bug_actions_test.go
@@ -1,93 +1,31 @@
package bug
import (
- "github.com/MichaelMure/git-bug/repository"
- "github.com/stretchr/testify/assert"
-
- "io/ioutil"
- "log"
- "os"
"testing"
-)
-
-func createRepo(bare bool) *repository.GitRepo {
- dir, err := ioutil.TempDir("", "")
- if err != nil {
- log.Fatal(err)
- }
-
- // fmt.Println("Creating repo:", dir)
-
- var creator func(string) (*repository.GitRepo, error)
-
- if bare {
- creator = repository.InitBareGitRepo
- } else {
- creator = repository.InitGitRepo
- }
-
- repo, err := creator(dir)
- if err != nil {
- log.Fatal(err)
- }
-
- if err := repo.StoreConfig("user.name", "testuser"); err != nil {
- log.Fatal("failed to set user.name for test repository: ", err)
- }
- if err := repo.StoreConfig("user.email", "testuser@example.com"); err != nil {
- log.Fatal("failed to set user.email for test repository: ", err)
- }
-
- return repo
-}
-
-func cleanupRepo(repo repository.Repo) error {
- path := repo.GetPath()
- // fmt.Println("Cleaning repo:", path)
- return os.RemoveAll(path)
-}
-
-func setupRepos(t testing.TB) (repoA, repoB, remote *repository.GitRepo) {
- repoA = createRepo(false)
- repoB = createRepo(false)
- remote = createRepo(true)
+ "time"
- remoteAddr := "file://" + remote.GetPath()
-
- err := repoA.AddRemote("origin", remoteAddr)
- if err != nil {
- t.Fatal(err)
- }
-
- err = repoB.AddRemote("origin", remoteAddr)
- if err != nil {
- t.Fatal(err)
- }
-
- return repoA, repoB, remote
-}
-
-func cleanupRepos(repoA, repoB, remote *repository.GitRepo) {
- cleanupRepo(repoA)
- cleanupRepo(repoB)
- cleanupRepo(remote)
-}
+ "github.com/MichaelMure/git-bug/identity"
+ "github.com/MichaelMure/git-bug/util/test"
+ "github.com/stretchr/testify/require"
+)
func TestPushPull(t *testing.T) {
- repoA, repoB, remote := setupRepos(t)
- defer cleanupRepos(repoA, repoB, remote)
+ repoA, repoB, remote := test.SetupReposAndRemote(t)
+ defer test.CleanupRepos(repoA, repoB, remote)
- bug1, _, err := Create(rene, unix, "bug1", "message")
- assert.Nil(t, err)
+ reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
+
+ bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message")
+ require.NoError(t, err)
err = bug1.Commit(repoA)
- assert.Nil(t, err)
+ require.NoError(t, err)
// A --> remote --> B
_, err = Push(repoA, "origin")
- assert.Nil(t, err)
+ require.NoError(t, err)
err = Pull(repoB, "origin")
- assert.Nil(t, err)
+ require.NoError(t, err)
bugs := allBugs(t, ReadAllLocalBugs(repoB))
@@ -96,16 +34,19 @@ func TestPushPull(t *testing.T) {
}
// B --> remote --> A
- bug2, _, err := Create(rene, unix, "bug2", "message")
- assert.Nil(t, err)
+ reneB, err := identity.ReadLocal(repoA, reneA.Id())
+ require.NoError(t, err)
+
+ bug2, _, err := Create(reneB, time.Now().Unix(), "bug2", "message")
+ require.NoError(t, err)
err = bug2.Commit(repoB)
- assert.Nil(t, err)
+ require.NoError(t, err)
_, err = Push(repoB, "origin")
- assert.Nil(t, err)
+ require.NoError(t, err)
err = Pull(repoA, "origin")
- assert.Nil(t, err)
+ require.NoError(t, err)
bugs = allBugs(t, ReadAllLocalBugs(repoA))
@@ -136,41 +77,46 @@ func BenchmarkRebaseTheirs(b *testing.B) {
}
func _RebaseTheirs(t testing.TB) {
- repoA, repoB, remote := setupRepos(t)
- defer cleanupRepos(repoA, repoB, remote)
+ repoA, repoB, remote := test.SetupReposAndRemote(t)
+ defer test.CleanupRepos(repoA, repoB, remote)
+
+ reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
- bug1, _, err := Create(rene, unix, "bug1", "message")
- assert.Nil(t, err)
+ bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message")
+ require.NoError(t, err)
err = bug1.Commit(repoA)
- assert.Nil(t, err)
+ require.NoError(t, err)
// A --> remote
_, err = Push(repoA, "origin")
- assert.Nil(t, err)
+ require.NoError(t, err)
// remote --> B
err = Pull(repoB, "origin")
- assert.Nil(t, err)
+ require.NoError(t, err)
bug2, err := ReadLocalBug(repoB, bug1.Id())
- assert.Nil(t, err)
-
- _, err = AddComment(bug2, rene, unix, "message2")
- assert.Nil(t, err)
- _, err = AddComment(bug2, rene, unix, "message3")
- assert.Nil(t, err)
- _, err = AddComment(bug2, rene, unix, "message4")
- assert.Nil(t, err)
+ require.NoError(t, err)
+
+ reneB, err := identity.ReadLocal(repoA, reneA.Id())
+ require.NoError(t, err)
+
+ _, err = AddComment(bug2, reneB, time.Now().Unix(), "message2")
+ require.NoError(t, err)
+ _, err = AddComment(bug2, reneB, time.Now().Unix(), "message3")
+ require.NoError(t, err)
+ _, err = AddComment(bug2, reneB, time.Now().Unix(), "message4")
+ require.NoError(t, err)
err = bug2.Commit(repoB)
- assert.Nil(t, err)
+ require.NoError(t, err)
// B --> remote
_, err = Push(repoB, "origin")
- assert.Nil(t, err)
+ require.NoError(t, err)
// remote --> A
err = Pull(repoA, "origin")
- assert.Nil(t, err)
+ require.NoError(t, err)
bugs := allBugs(t, ReadAllLocalBugs(repoB))
@@ -179,7 +125,7 @@ func _RebaseTheirs(t testing.TB) {
}
bug3, err := ReadLocalBug(repoA, bug1.Id())
- assert.Nil(t, err)
+ require.NoError(t, err)
if nbOps(bug3) != 4 {
t.Fatal("Unexpected number of operations")
@@ -197,52 +143,54 @@ func BenchmarkRebaseOurs(b *testing.B) {
}
func _RebaseOurs(t testing.TB) {
- repoA, repoB, remote := setupRepos(t)
- defer cleanupRepos(repoA, repoB, remote)
+ repoA, repoB, remote := test.SetupReposAndRemote(t)
+ defer test.CleanupRepos(repoA, repoB, remote)
- bug1, _, err := Create(rene, unix, "bug1", "message")
- assert.Nil(t, err)
+ reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
+
+ bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message")
+ require.NoError(t, err)
err = bug1.Commit(repoA)
- assert.Nil(t, err)
+ require.NoError(t, err)
// A --> remote
_, err = Push(repoA, "origin")
- assert.Nil(t, err)
+ require.NoError(t, err)
// remote --> B
err = Pull(repoB, "origin")
- assert.Nil(t, err)
-
- _, err = AddComment(bug1, rene, unix, "message2")
- assert.Nil(t, err)
- _, err = AddComment(bug1, rene, unix, "message3")
- assert.Nil(t, err)
- _, err = AddComment(bug1, rene, unix, "message4")
- assert.Nil(t, err)
+ require.NoError(t, err)
+
+ _, err = AddComment(bug1, reneA, time.Now().Unix(), "message2")
+ require.NoError(t, err)
+ _, err = AddComment(bug1, reneA, time.Now().Unix(), "message3")
+ require.NoError(t, err)
+ _, err = AddComment(bug1, reneA, time.Now().Unix(), "message4")
+ require.NoError(t, err)
err = bug1.Commit(repoA)
- assert.Nil(t, err)
-
- _, err = AddComment(bug1, rene, unix, "message5")
- assert.Nil(t, err)
- _, err = AddComment(bug1, rene, unix, "message6")
- assert.Nil(t, err)
- _, err = AddComment(bug1, rene, unix, "message7")
- assert.Nil(t, err)
+ require.NoError(t, err)
+
+ _, err = AddComment(bug1, reneA, time.Now().Unix(), "message5")
+ require.NoError(t, err)
+ _, err = AddComment(bug1, reneA, time.Now().Unix(), "message6")
+ require.NoError(t, err)
+ _, err = AddComment(bug1, reneA, time.Now().Unix(), "message7")
+ require.NoError(t, err)
err = bug1.Commit(repoA)
- assert.Nil(t, err)
-
- _, err = AddComment(bug1, rene, unix, "message8")
- assert.Nil(t, err)
- _, err = AddComment(bug1, rene, unix, "message9")
- assert.Nil(t, err)
- _, err = AddComment(bug1, rene, unix, "message10")
- assert.Nil(t, err)
+ require.NoError(t, err)
+
+ _, err = AddComment(bug1, reneA, time.Now().Unix(), "message8")
+ require.NoError(t, err)
+ _, err = AddComment(bug1, reneA, time.Now().Unix(), "message9")
+ require.NoError(t, err)
+ _, err = AddComment(bug1, reneA, time.Now().Unix(), "message10")
+ require.NoError(t, err)
err = bug1.Commit(repoA)
- assert.Nil(t, err)
+ require.NoError(t, err)
// remote --> A
err = Pull(repoA, "origin")
- assert.Nil(t, err)
+ require.NoError(t, err)
bugs := allBugs(t, ReadAllLocalBugs(repoA))
@@ -251,7 +199,7 @@ func _RebaseOurs(t testing.TB) {
}
bug2, err := ReadLocalBug(repoA, bug1.Id())
- assert.Nil(t, err)
+ require.NoError(t, err)
if nbOps(bug2) != 10 {
t.Fatal("Unexpected number of operations")
@@ -278,86 +226,91 @@ func BenchmarkRebaseConflict(b *testing.B) {
}
func _RebaseConflict(t testing.TB) {
- repoA, repoB, remote := setupRepos(t)
- defer cleanupRepos(repoA, repoB, remote)
+ repoA, repoB, remote := test.SetupReposAndRemote(t)
+ defer test.CleanupRepos(repoA, repoB, remote)
+
+ reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
- bug1, _, err := Create(rene, unix, "bug1", "message")
- assert.Nil(t, err)
+ bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message")
+ require.NoError(t, err)
err = bug1.Commit(repoA)
- assert.Nil(t, err)
+ require.NoError(t, err)
// A --> remote
_, err = Push(repoA, "origin")
- assert.Nil(t, err)
+ require.NoError(t, err)
// remote --> B
err = Pull(repoB, "origin")
- assert.Nil(t, err)
-
- _, err = AddComment(bug1, rene, unix, "message2")
- assert.Nil(t, err)
- _, err = AddComment(bug1, rene, unix, "message3")
- assert.Nil(t, err)
- _, err = AddComment(bug1, rene, unix, "message4")
- assert.Nil(t, err)
+ require.NoError(t, err)
+
+ _, err = AddComment(bug1, reneA, time.Now().Unix(), "message2")
+ require.NoError(t, err)
+ _, err = AddComment(bug1, reneA, time.Now().Unix(), "message3")
+ require.NoError(t, err)
+ _, err = AddComment(bug1, reneA, time.Now().Unix(), "message4")
+ require.NoError(t, err)
err = bug1.Commit(repoA)
- assert.Nil(t, err)
-
- _, err = AddComment(bug1, rene, unix, "message5")
- assert.Nil(t, err)
- _, err = AddComment(bug1, rene, unix, "message6")
- assert.Nil(t, err)
- _, err = AddComment(bug1, rene, unix, "message7")
- assert.Nil(t, err)
+ require.NoError(t, err)
+
+ _, err = AddComment(bug1, reneA, time.Now().Unix(), "message5")
+ require.NoError(t, err)
+ _, err = AddComment(bug1, reneA, time.Now().Unix(), "message6")
+ require.NoError(t, err)
+ _, err = AddComment(bug1, reneA, time.Now().Unix(), "message7")
+ require.NoError(t, err)
err = bug1.Commit(repoA)
- assert.Nil(t, err)
-
- _, err = AddComment(bug1, rene, unix, "message8")
- assert.Nil(t, err)
- _, err = AddComment(bug1, rene, unix, "message9")
- assert.Nil(t, err)
- _, err = AddComment(bug1, rene, unix, "message10")
- assert.Nil(t, err)
+ require.NoError(t, err)
+
+ _, err = AddComment(bug1, reneA, time.Now().Unix(), "message8")
+ require.NoError(t, err)
+ _, err = AddComment(bug1, reneA, time.Now().Unix(), "message9")
+ require.NoError(t, err)
+ _, err = AddComment(bug1, reneA, time.Now().Unix(), "message10")
+ require.NoError(t, err)
err = bug1.Commit(repoA)
- assert.Nil(t, err)
+ require.NoError(t, err)
bug2, err := ReadLocalBug(repoB, bug1.Id())
- assert.Nil(t, err)
-
- _, err = AddComment(bug2, rene, unix, "message11")
- assert.Nil(t, err)
- _, err = AddComment(bug2, rene, unix, "message12")
- assert.Nil(t, err)
- _, err = AddComment(bug2, rene, unix, "message13")
- assert.Nil(t, err)
+ require.NoError(t, err)
+
+ reneB, err := identity.ReadLocal(repoA, reneA.Id())
+ require.NoError(t, err)
+
+ _, err = AddComment(bug2, reneB, time.Now().Unix(), "message11")
+ require.NoError(t, err)
+ _, err = AddComment(bug2, reneB, time.Now().Unix(), "message12")
+ require.NoError(t, err)
+ _, err = AddComment(bug2, reneB, time.Now().Unix(), "message13")
+ require.NoError(t, err)
err = bug2.Commit(repoB)
- assert.Nil(t, err)
-
- _, err = AddComment(bug2, rene, unix, "message14")
- assert.Nil(t, err)
- _, err = AddComment(bug2, rene, unix, "message15")
- assert.Nil(t, err)
- _, err = AddComment(bug2, rene, unix, "message16")
- assert.Nil(t, err)
+ require.NoError(t, err)
+
+ _, err = AddComment(bug2, reneB, time.Now().Unix(), "message14")
+ require.NoError(t, err)
+ _, err = AddComment(bug2, reneB, time.Now().Unix(), "message15")
+ require.NoError(t, err)
+ _, err = AddComment(bug2, reneB, time.Now().Unix(), "message16")
+ require.NoError(t, err)
err = bug2.Commit(repoB)
- assert.Nil(t, err)
-
- _, err = AddComment(bug2, rene, unix, "message17")
- assert.Nil(t, err)
- _, err = AddComment(bug2, rene, unix, "message18")
- assert.Nil(t, err)
- _, err = AddComment(bug2, rene, unix, "message19")
- assert.Nil(t, err)
+ require.NoError(t, err)
+
+ _, err = AddComment(bug2, reneB, time.Now().Unix(), "message17")
+ require.NoError(t, err)
+ _, err = AddComment(bug2, reneB, time.Now().Unix(), "message18")
+ require.NoError(t, err)
+ _, err = AddComment(bug2, reneB, time.Now().Unix(), "message19")
+ require.NoError(t, err)
err = bug2.Commit(repoB)
- assert.Nil(t, err)
+ require.NoError(t, err)
// A --> remote
_, err = Push(repoA, "origin")
- assert.Nil(t, err)
+ require.NoError(t, err)
// remote --> B
err = Pull(repoB, "origin")
- assert.Nil(t, err)
+ require.NoError(t, err)
bugs := allBugs(t, ReadAllLocalBugs(repoB))
@@ -366,7 +319,7 @@ func _RebaseConflict(t testing.TB) {
}
bug3, err := ReadLocalBug(repoB, bug1.Id())
- assert.Nil(t, err)
+ require.NoError(t, err)
if nbOps(bug3) != 19 {
t.Fatal("Unexpected number of operations")
@@ -374,11 +327,11 @@ func _RebaseConflict(t testing.TB) {
// B --> remote
_, err = Push(repoB, "origin")
- assert.Nil(t, err)
+ require.NoError(t, err)
// remote --> A
err = Pull(repoA, "origin")
- assert.Nil(t, err)
+ require.NoError(t, err)
bugs = allBugs(t, ReadAllLocalBugs(repoA))
@@ -387,7 +340,7 @@ func _RebaseConflict(t testing.TB) {
}
bug4, err := ReadLocalBug(repoA, bug1.Id())
- assert.Nil(t, err)
+ require.NoError(t, err)
if nbOps(bug4) != 19 {
t.Fatal("Unexpected number of operations")
diff --git a/bug/bug_test.go b/bug/bug_test.go
index 0fd373d5..4c3952eb 100644
--- a/bug/bug_test.go
+++ b/bug/bug_test.go
@@ -1,11 +1,12 @@
package bug
import (
+ "testing"
+ "time"
+
+ "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
- "github.com/go-test/deep"
"github.com/stretchr/testify/assert"
-
- "testing"
)
func TestBugId(t *testing.T) {
@@ -13,6 +14,9 @@ func TestBugId(t *testing.T) {
bug1 := NewBug()
+ rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
+ createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
+
bug1.Append(createOp)
err := bug1.Commit(mockRepo)
@@ -29,6 +33,9 @@ func TestBugValidity(t *testing.T) {
bug1 := NewBug()
+ rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
+ createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
+
if bug1.Validate() == nil {
t.Fatal("Empty bug should be invalid")
}
@@ -56,9 +63,14 @@ func TestBugValidity(t *testing.T) {
}
}
-func TestBugSerialisation(t *testing.T) {
+func TestBugCommitLoad(t *testing.T) {
bug1 := NewBug()
+ rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
+ createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
+ setTitleOp := NewSetTitleOp(rene, time.Now().Unix(), "title2", "title1")
+ addCommentOp := NewAddCommentOp(rene, time.Now().Unix(), "message2", nil)
+
bug1.Append(createOp)
bug1.Append(setTitleOp)
bug1.Append(setTitleOp)
@@ -70,25 +82,30 @@ func TestBugSerialisation(t *testing.T) {
assert.Nil(t, err)
bug2, err := ReadLocalBug(repo, bug1.Id())
- if err != nil {
- t.Error(err)
- }
+ assert.NoError(t, err)
+ equivalentBug(t, bug1, bug2)
- // ignore some fields
- bug2.packs[0].commitHash = bug1.packs[0].commitHash
- for i := range bug1.packs[0].Operations {
- bug2.packs[0].Operations[i].base().hash = bug1.packs[0].Operations[i].base().hash
- }
+ // add more op
- // check hashes
- for i := range bug1.packs[0].Operations {
- if !bug2.packs[0].Operations[i].base().hash.IsValid() {
- t.Fatal("invalid hash")
+ bug1.Append(setTitleOp)
+ bug1.Append(addCommentOp)
+
+ err = bug1.Commit(repo)
+ assert.Nil(t, err)
+
+ bug3, err := ReadLocalBug(repo, bug1.Id())
+ assert.NoError(t, err)
+ equivalentBug(t, bug1, bug3)
+}
+
+func equivalentBug(t *testing.T, expected, actual *Bug) {
+ assert.Equal(t, len(expected.packs), len(actual.packs))
+
+ for i := range expected.packs {
+ for j := range expected.packs[i].Operations {
+ actual.packs[i].Operations[j].base().hash = expected.packs[i].Operations[j].base().hash
}
}
- deep.CompareUnexportedFields = true
- if diff := deep.Equal(bug1, bug2); diff != nil {
- t.Fatal(diff)
- }
+ assert.Equal(t, expected, actual)
}
diff --git a/bug/comment.go b/bug/comment.go
index 67936634..f7a6eada 100644
--- a/bug/comment.go
+++ b/bug/comment.go
@@ -1,19 +1,21 @@
package bug
import (
+ "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/git"
+ "github.com/MichaelMure/git-bug/util/timestamp"
"github.com/dustin/go-humanize"
)
// Comment represent a comment in a Bug
type Comment struct {
- Author Person
+ Author identity.Interface
Message string
Files []git.Hash
// Creation time of the comment.
// Should be used only for human display, never for ordering as we can't rely on it in a distributed system.
- UnixTime Timestamp
+ UnixTime timestamp.Timestamp
}
// FormatTimeRel format the UnixTime of the comment for human consumption
diff --git a/bug/identity.go b/bug/identity.go
new file mode 100644
index 00000000..2eb2bcaf
--- /dev/null
+++ b/bug/identity.go
@@ -0,0 +1,27 @@
+package bug
+
+import (
+ "github.com/MichaelMure/git-bug/identity"
+)
+
+// EnsureIdentities walk the graph of operations and make sure that all Identity
+// are properly loaded. That is, it replace all the IdentityStub with the full
+// Identity, loaded through a Resolver.
+func (bug *Bug) EnsureIdentities(resolver identity.Resolver) error {
+ it := NewOperationIterator(bug)
+
+ for it.Next() {
+ op := it.Value()
+ base := op.base()
+
+ if stub, ok := base.Author.(*identity.IdentityStub); ok {
+ i, err := resolver.ResolveIdentity(stub.Id())
+ if err != nil {
+ return err
+ }
+
+ base.Author = i
+ }
+ }
+ return nil
+}
diff --git a/bug/label.go b/bug/label.go
index b73222dd..0ad84f22 100644
--- a/bug/label.go
+++ b/bug/label.go
@@ -28,7 +28,7 @@ func (l *Label) UnmarshalGQL(v interface{}) error {
// MarshalGQL implements the graphql.Marshaler interface
func (l Label) MarshalGQL(w io.Writer) {
- w.Write([]byte(`"` + l.String() + `"`))
+ _, _ = w.Write([]byte(`"` + l.String() + `"`))
}
func (l Label) Validate() error {
diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go
index 2d6fb21a..9ecef941 100644
--- a/bug/op_add_comment.go
+++ b/bug/op_add_comment.go
@@ -1,10 +1,13 @@
package bug
import (
+ "encoding/json"
"fmt"
+ "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/git"
"github.com/MichaelMure/git-bug/util/text"
+ "github.com/MichaelMure/git-bug/util/timestamp"
)
var _ Operation = &AddCommentOperation{}
@@ -12,9 +15,9 @@ var _ Operation = &AddCommentOperation{}
// AddCommentOperation will add a new comment in the bug
type AddCommentOperation struct {
OpBase
- Message string `json:"message"`
+ Message string
// TODO: change for a map[string]util.hash to store the filename ?
- Files []git.Hash `json:"files"`
+ Files []git.Hash
}
func (op *AddCommentOperation) base() *OpBase {
@@ -30,7 +33,7 @@ func (op *AddCommentOperation) Apply(snapshot *Snapshot) {
Message: op.Message,
Author: op.Author,
Files: op.Files,
- UnixTime: Timestamp(op.UnixTime),
+ UnixTime: timestamp.Timestamp(op.UnixTime),
}
snapshot.Comments = append(snapshot.Comments, comment)
@@ -65,10 +68,58 @@ func (op *AddCommentOperation) Validate() error {
return nil
}
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *AddCommentOperation) MarshalJSON() ([]byte, error) {
+ base, err := json.Marshal(op.OpBase)
+ if err != nil {
+ return nil, err
+ }
+
+ // revert back to a flat map to be able to add our own fields
+ var data map[string]interface{}
+ if err := json.Unmarshal(base, &data); err != nil {
+ return nil, err
+ }
+
+ data["message"] = op.Message
+ data["files"] = op.Files
+
+ return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *AddCommentOperation) UnmarshalJSON(data []byte) error {
+ // Unmarshal OpBase and the op separately
+
+ base := OpBase{}
+ err := json.Unmarshal(data, &base)
+ if err != nil {
+ return err
+ }
+
+ aux := struct {
+ Message string `json:"message"`
+ Files []git.Hash `json:"files"`
+ }{}
+
+ err = json.Unmarshal(data, &aux)
+ if err != nil {
+ return err
+ }
+
+ op.OpBase = base
+ op.Message = aux.Message
+ op.Files = aux.Files
+
+ return nil
+}
+
// Sign post method for gqlgen
func (op *AddCommentOperation) IsAuthored() {}
-func NewAddCommentOp(author Person, unixTime int64, message string, files []git.Hash) *AddCommentOperation {
+func NewAddCommentOp(author identity.Interface, unixTime int64, message string, files []git.Hash) *AddCommentOperation {
return &AddCommentOperation{
OpBase: newOpBase(AddCommentOp, author, unixTime),
Message: message,
@@ -82,11 +133,11 @@ type AddCommentTimelineItem struct {
}
// Convenience function to apply the operation
-func AddComment(b Interface, author Person, unixTime int64, message string) (*AddCommentOperation, error) {
+func AddComment(b Interface, author identity.Interface, unixTime int64, message string) (*AddCommentOperation, error) {
return AddCommentWithFiles(b, author, unixTime, message, nil)
}
-func AddCommentWithFiles(b Interface, author Person, unixTime int64, message string, files []git.Hash) (*AddCommentOperation, error) {
+func AddCommentWithFiles(b Interface, author identity.Interface, unixTime int64, message string, files []git.Hash) (*AddCommentOperation, error) {
addCommentOp := NewAddCommentOp(author, unixTime, message, files)
if err := addCommentOp.Validate(); err != nil {
return nil, err
diff --git a/bug/op_add_comment_test.go b/bug/op_add_comment_test.go
new file mode 100644
index 00000000..a38d0228
--- /dev/null
+++ b/bug/op_add_comment_test.go
@@ -0,0 +1,25 @@
+package bug
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/MichaelMure/git-bug/identity"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAddCommentSerialize(t *testing.T) {
+ var rene = identity.NewBare("René Descartes", "rene@descartes.fr")
+ unix := time.Now().Unix()
+ before := NewAddCommentOp(rene, unix, "message", nil)
+
+ data, err := json.Marshal(before)
+ assert.NoError(t, err)
+
+ var after AddCommentOperation
+ err = json.Unmarshal(data, &after)
+ assert.NoError(t, err)
+
+ assert.Equal(t, before, &after)
+}
diff --git a/bug/op_create.go b/bug/op_create.go
index 3816d8b7..42d40f71 100644
--- a/bug/op_create.go
+++ b/bug/op_create.go
@@ -1,11 +1,14 @@
package bug
import (
+ "encoding/json"
"fmt"
"strings"
+ "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/git"
"github.com/MichaelMure/git-bug/util/text"
+ "github.com/MichaelMure/git-bug/util/timestamp"
)
var _ Operation = &CreateOperation{}
@@ -13,9 +16,9 @@ var _ Operation = &CreateOperation{}
// CreateOperation define the initial creation of a bug
type CreateOperation struct {
OpBase
- Title string `json:"title"`
- Message string `json:"message"`
- Files []git.Hash `json:"files"`
+ Title string
+ Message string
+ Files []git.Hash
}
func (op *CreateOperation) base() *OpBase {
@@ -32,7 +35,7 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) {
comment := Comment{
Message: op.Message,
Author: op.Author,
- UnixTime: Timestamp(op.UnixTime),
+ UnixTime: timestamp.Timestamp(op.UnixTime),
}
snapshot.Comments = []Comment{comment}
@@ -81,10 +84,61 @@ func (op *CreateOperation) Validate() error {
return nil
}
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *CreateOperation) MarshalJSON() ([]byte, error) {
+ base, err := json.Marshal(op.OpBase)
+ if err != nil {
+ return nil, err
+ }
+
+ // revert back to a flat map to be able to add our own fields
+ var data map[string]interface{}
+ if err := json.Unmarshal(base, &data); err != nil {
+ return nil, err
+ }
+
+ data["title"] = op.Title
+ data["message"] = op.Message
+ data["files"] = op.Files
+
+ return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *CreateOperation) UnmarshalJSON(data []byte) error {
+ // Unmarshal OpBase and the op separately
+
+ base := OpBase{}
+ err := json.Unmarshal(data, &base)
+ if err != nil {
+ return err
+ }
+
+ aux := struct {
+ Title string `json:"title"`
+ Message string `json:"message"`
+ Files []git.Hash `json:"files"`
+ }{}
+
+ err = json.Unmarshal(data, &aux)
+ if err != nil {
+ return err
+ }
+
+ op.OpBase = base
+ op.Title = aux.Title
+ op.Message = aux.Message
+ op.Files = aux.Files
+
+ return nil
+}
+
// Sign post method for gqlgen
func (op *CreateOperation) IsAuthored() {}
-func NewCreateOp(author Person, unixTime int64, title, message string, files []git.Hash) *CreateOperation {
+func NewCreateOp(author identity.Interface, unixTime int64, title, message string, files []git.Hash) *CreateOperation {
return &CreateOperation{
OpBase: newOpBase(CreateOp, author, unixTime),
Title: title,
@@ -99,11 +153,11 @@ type CreateTimelineItem struct {
}
// Convenience function to apply the operation
-func Create(author Person, unixTime int64, title, message string) (*Bug, *CreateOperation, error) {
+func Create(author identity.Interface, unixTime int64, title, message string) (*Bug, *CreateOperation, error) {
return CreateWithFiles(author, unixTime, title, message, nil)
}
-func CreateWithFiles(author Person, unixTime int64, title, message string, files []git.Hash) (*Bug, *CreateOperation, error) {
+func CreateWithFiles(author identity.Interface, unixTime int64, title, message string, files []git.Hash) (*Bug, *CreateOperation, error) {
newBug := NewBug()
createOp := NewCreateOp(author, unixTime, title, message, files)
diff --git a/bug/op_create_test.go b/bug/op_create_test.go
index d74051ec..92ac191d 100644
--- a/bug/op_create_test.go
+++ b/bug/op_create_test.go
@@ -1,20 +1,19 @@
package bug
import (
+ "encoding/json"
"testing"
"time"
- "github.com/go-test/deep"
+ "github.com/MichaelMure/git-bug/identity"
+ "github.com/MichaelMure/git-bug/util/timestamp"
+ "github.com/stretchr/testify/assert"
)
func TestCreate(t *testing.T) {
snapshot := Snapshot{}
- var rene = Person{
- Name: "René Descartes",
- Email: "rene@descartes.fr",
- }
-
+ rene := identity.NewBare("René Descartes", "rene@descartes.fr")
unix := time.Now().Unix()
create := NewCreateOp(rene, unix, "title", "message", nil)
@@ -22,11 +21,9 @@ func TestCreate(t *testing.T) {
create.Apply(&snapshot)
hash, err := create.Hash()
- if err != nil {
- t.Fatal(err)
- }
+ assert.NoError(t, err)
- comment := Comment{Author: rene, Message: "message", UnixTime: Timestamp(create.UnixTime)}
+ comment := Comment{Author: rene, Message: "message", UnixTime: timestamp.Timestamp(create.UnixTime)}
expected := Snapshot{
Title: "title",
@@ -42,8 +39,20 @@ func TestCreate(t *testing.T) {
},
}
- deep.CompareUnexportedFields = true
- if diff := deep.Equal(snapshot, expected); diff != nil {
- t.Fatal(diff)
- }
+ assert.Equal(t, expected, snapshot)
+}
+
+func TestCreateSerialize(t *testing.T) {
+ var rene = identity.NewBare("René Descartes", "rene@descartes.fr")
+ unix := time.Now().Unix()
+ before := NewCreateOp(rene, unix, "title", "message", nil)
+
+ data, err := json.Marshal(before)
+ assert.NoError(t, err)
+
+ var after CreateOperation
+ err = json.Unmarshal(data, &after)
+ assert.NoError(t, err)
+
+ assert.Equal(t, before, &after)
}
diff --git a/bug/op_edit_comment.go b/bug/op_edit_comment.go
index bc87310a..527b7440 100644
--- a/bug/op_edit_comment.go
+++ b/bug/op_edit_comment.go
@@ -1,8 +1,12 @@
package bug
import (
+ "encoding/json"
"fmt"
+ "github.com/MichaelMure/git-bug/identity"
+ "github.com/MichaelMure/git-bug/util/timestamp"
+
"github.com/MichaelMure/git-bug/util/git"
"github.com/MichaelMure/git-bug/util/text"
)
@@ -12,9 +16,9 @@ var _ Operation = &EditCommentOperation{}
// EditCommentOperation will change a comment in the bug
type EditCommentOperation struct {
OpBase
- Target git.Hash `json:"target"`
- Message string `json:"message"`
- Files []git.Hash `json:"files"`
+ Target git.Hash
+ Message string
+ Files []git.Hash
}
func (op *EditCommentOperation) base() *OpBase {
@@ -55,7 +59,7 @@ func (op *EditCommentOperation) Apply(snapshot *Snapshot) {
comment := Comment{
Message: op.Message,
Files: op.Files,
- UnixTime: Timestamp(op.UnixTime),
+ UnixTime: timestamp.Timestamp(op.UnixTime),
}
switch target.(type) {
@@ -92,10 +96,61 @@ func (op *EditCommentOperation) Validate() error {
return nil
}
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *EditCommentOperation) MarshalJSON() ([]byte, error) {
+ base, err := json.Marshal(op.OpBase)
+ if err != nil {
+ return nil, err
+ }
+
+ // revert back to a flat map to be able to add our own fields
+ var data map[string]interface{}
+ if err := json.Unmarshal(base, &data); err != nil {
+ return nil, err
+ }
+
+ data["target"] = op.Target
+ data["message"] = op.Message
+ data["files"] = op.Files
+
+ return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *EditCommentOperation) UnmarshalJSON(data []byte) error {
+ // Unmarshal OpBase and the op separately
+
+ base := OpBase{}
+ err := json.Unmarshal(data, &base)
+ if err != nil {
+ return err
+ }
+
+ aux := struct {
+ Target git.Hash `json:"target"`
+ Message string `json:"message"`
+ Files []git.Hash `json:"files"`
+ }{}
+
+ err = json.Unmarshal(data, &aux)
+ if err != nil {
+ return err
+ }
+
+ op.OpBase = base
+ op.Target = aux.Target
+ op.Message = aux.Message
+ op.Files = aux.Files
+
+ return nil
+}
+
// Sign post method for gqlgen
func (op *EditCommentOperation) IsAuthored() {}
-func NewEditCommentOp(author Person, unixTime int64, target git.Hash, message string, files []git.Hash) *EditCommentOperation {
+func NewEditCommentOp(author identity.Interface, unixTime int64, target git.Hash, message string, files []git.Hash) *EditCommentOperation {
return &EditCommentOperation{
OpBase: newOpBase(EditCommentOp, author, unixTime),
Target: target,
@@ -105,11 +160,11 @@ func NewEditCommentOp(author Person, unixTime int64, target git.Hash, message st
}
// Convenience function to apply the operation
-func EditComment(b Interface, author Person, unixTime int64, target git.Hash, message string) (*EditCommentOperation, error) {
+func EditComment(b Interface, author identity.Interface, unixTime int64, target git.Hash, message string) (*EditCommentOperation, error) {
return EditCommentWithFiles(b, author, unixTime, target, message, nil)
}
-func EditCommentWithFiles(b Interface, author Person, unixTime int64, target git.Hash, message string, files []git.Hash) (*EditCommentOperation, error) {
+func EditCommentWithFiles(b Interface, author identity.Interface, unixTime int64, target git.Hash, message string, files []git.Hash) (*EditCommentOperation, error) {
editCommentOp := NewEditCommentOp(author, unixTime, target, message, files)
if err := editCommentOp.Validate(); err != nil {
return nil, err
diff --git a/bug/op_edit_comment_test.go b/bug/op_edit_comment_test.go
index 71a7dda2..72f8a168 100644
--- a/bug/op_edit_comment_test.go
+++ b/bug/op_edit_comment_test.go
@@ -1,37 +1,32 @@
package bug
import (
+ "encoding/json"
"testing"
"time"
- "gotest.tools/assert"
+ "github.com/MichaelMure/git-bug/identity"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestEdit(t *testing.T) {
snapshot := Snapshot{}
- var rene = Person{
- Name: "René Descartes",
- Email: "rene@descartes.fr",
- }
-
+ rene := identity.NewBare("René Descartes", "rene@descartes.fr")
unix := time.Now().Unix()
create := NewCreateOp(rene, unix, "title", "create", nil)
create.Apply(&snapshot)
hash1, err := create.Hash()
- if err != nil {
- t.Fatal(err)
- }
+ require.NoError(t, err)
comment := NewAddCommentOp(rene, unix, "comment", nil)
comment.Apply(&snapshot)
hash2, err := comment.Hash()
- if err != nil {
- t.Fatal(err)
- }
+ require.NoError(t, err)
edit := NewEditCommentOp(rene, unix, hash1, "create edited", nil)
edit.Apply(&snapshot)
@@ -51,3 +46,18 @@ func TestEdit(t *testing.T) {
assert.Equal(t, snapshot.Comments[0].Message, "create edited")
assert.Equal(t, snapshot.Comments[1].Message, "comment edited")
}
+
+func TestEditCommentSerialize(t *testing.T) {
+ var rene = identity.NewBare("René Descartes", "rene@descartes.fr")
+ unix := time.Now().Unix()
+ before := NewEditCommentOp(rene, unix, "target", "message", nil)
+
+ data, err := json.Marshal(before)
+ assert.NoError(t, err)
+
+ var after EditCommentOperation
+ err = json.Unmarshal(data, &after)
+ assert.NoError(t, err)
+
+ assert.Equal(t, before, &after)
+}
diff --git a/bug/op_label_change.go b/bug/op_label_change.go
index d7aab06b..84542b6e 100644
--- a/bug/op_label_change.go
+++ b/bug/op_label_change.go
@@ -1,9 +1,13 @@
package bug
import (
+ "encoding/json"
"fmt"
"sort"
+ "github.com/MichaelMure/git-bug/identity"
+ "github.com/MichaelMure/git-bug/util/timestamp"
+
"github.com/MichaelMure/git-bug/util/git"
"github.com/pkg/errors"
)
@@ -13,8 +17,8 @@ var _ Operation = &LabelChangeOperation{}
// LabelChangeOperation define a Bug operation to add or remove labels
type LabelChangeOperation struct {
OpBase
- Added []Label `json:"added"`
- Removed []Label `json:"removed"`
+ Added []Label
+ Removed []Label
}
func (op *LabelChangeOperation) base() *OpBase {
@@ -65,7 +69,7 @@ AddLoop:
item := &LabelChangeTimelineItem{
hash: hash,
Author: op.Author,
- UnixTime: Timestamp(op.UnixTime),
+ UnixTime: timestamp.Timestamp(op.UnixTime),
Added: op.Added,
Removed: op.Removed,
}
@@ -97,10 +101,58 @@ func (op *LabelChangeOperation) Validate() error {
return nil
}
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *LabelChangeOperation) MarshalJSON() ([]byte, error) {
+ base, err := json.Marshal(op.OpBase)
+ if err != nil {
+ return nil, err
+ }
+
+ // revert back to a flat map to be able to add our own fields
+ var data map[string]interface{}
+ if err := json.Unmarshal(base, &data); err != nil {
+ return nil, err
+ }
+
+ data["added"] = op.Added
+ data["removed"] = op.Removed
+
+ return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *LabelChangeOperation) UnmarshalJSON(data []byte) error {
+ // Unmarshal OpBase and the op separately
+
+ base := OpBase{}
+ err := json.Unmarshal(data, &base)
+ if err != nil {
+ return err
+ }
+
+ aux := struct {
+ Added []Label `json:"added"`
+ Removed []Label `json:"removed"`
+ }{}
+
+ err = json.Unmarshal(data, &aux)
+ if err != nil {
+ return err
+ }
+
+ op.OpBase = base
+ op.Added = aux.Added
+ op.Removed = aux.Removed
+
+ return nil
+}
+
// Sign post method for gqlgen
func (op *LabelChangeOperation) IsAuthored() {}
-func NewLabelChangeOperation(author Person, unixTime int64, added, removed []Label) *LabelChangeOperation {
+func NewLabelChangeOperation(author identity.Interface, unixTime int64, added, removed []Label) *LabelChangeOperation {
return &LabelChangeOperation{
OpBase: newOpBase(LabelChangeOp, author, unixTime),
Added: added,
@@ -110,8 +162,8 @@ func NewLabelChangeOperation(author Person, unixTime int64, added, removed []Lab
type LabelChangeTimelineItem struct {
hash git.Hash
- Author Person
- UnixTime Timestamp
+ Author identity.Interface
+ UnixTime timestamp.Timestamp
Added []Label
Removed []Label
}
@@ -121,7 +173,7 @@ func (l LabelChangeTimelineItem) Hash() git.Hash {
}
// ChangeLabels is a convenience function to apply the operation
-func ChangeLabels(b Interface, author Person, unixTime int64, add, remove []string) ([]LabelChangeResult, *LabelChangeOperation, error) {
+func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) ([]LabelChangeResult, *LabelChangeOperation, error) {
var added, removed []Label
var results []LabelChangeResult
diff --git a/bug/op_label_change_test.go b/bug/op_label_change_test.go
new file mode 100644
index 00000000..f5550b72
--- /dev/null
+++ b/bug/op_label_change_test.go
@@ -0,0 +1,25 @@
+package bug
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/MichaelMure/git-bug/identity"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestLabelChangeSerialize(t *testing.T) {
+ var rene = identity.NewBare("René Descartes", "rene@descartes.fr")
+ unix := time.Now().Unix()
+ before := NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"})
+
+ data, err := json.Marshal(before)
+ assert.NoError(t, err)
+
+ var after LabelChangeOperation
+ err = json.Unmarshal(data, &after)
+ assert.NoError(t, err)
+
+ assert.Equal(t, before, &after)
+}
diff --git a/bug/op_noop.go b/bug/op_noop.go
index ac898dde..3cd9f39a 100644
--- a/bug/op_noop.go
+++ b/bug/op_noop.go
@@ -1,12 +1,17 @@
package bug
-import "github.com/MichaelMure/git-bug/util/git"
+import (
+ "encoding/json"
+
+ "github.com/MichaelMure/git-bug/identity"
+ "github.com/MichaelMure/git-bug/util/git"
+)
var _ Operation = &NoOpOperation{}
// NoOpOperation is an operation that does not change the bug state. It can
// however be used to store arbitrary metadata in the bug history, for example
-// to support a bridge feature
+// to support a bridge feature.
type NoOpOperation struct {
OpBase
}
@@ -27,17 +32,57 @@ func (op *NoOpOperation) Validate() error {
return opBaseValidate(op, NoOpOp)
}
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *NoOpOperation) MarshalJSON() ([]byte, error) {
+ base, err := json.Marshal(op.OpBase)
+ if err != nil {
+ return nil, err
+ }
+
+ // revert back to a flat map to be able to add our own fields
+ var data map[string]interface{}
+ if err := json.Unmarshal(base, &data); err != nil {
+ return nil, err
+ }
+
+ return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *NoOpOperation) UnmarshalJSON(data []byte) error {
+ // Unmarshal OpBase and the op separately
+
+ base := OpBase{}
+ err := json.Unmarshal(data, &base)
+ if err != nil {
+ return err
+ }
+
+ aux := struct{}{}
+
+ err = json.Unmarshal(data, &aux)
+ if err != nil {
+ return err
+ }
+
+ op.OpBase = base
+
+ return nil
+}
+
// Sign post method for gqlgen
func (op *NoOpOperation) IsAuthored() {}
-func NewNoOpOp(author Person, unixTime int64) *NoOpOperation {
+func NewNoOpOp(author identity.Interface, unixTime int64) *NoOpOperation {
return &NoOpOperation{
OpBase: newOpBase(NoOpOp, author, unixTime),
}
}
// Convenience function to apply the operation
-func NoOp(b Interface, author Person, unixTime int64, metadata map[string]string) (*NoOpOperation, error) {
+func NoOp(b Interface, author identity.Interface, unixTime int64, metadata map[string]string) (*NoOpOperation, error) {
op := NewNoOpOp(author, unixTime)
for key, value := range metadata {
diff --git a/bug/op_noop_test.go b/bug/op_noop_test.go
new file mode 100644
index 00000000..385bc914
--- /dev/null
+++ b/bug/op_noop_test.go
@@ -0,0 +1,25 @@
+package bug
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/MichaelMure/git-bug/identity"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNoopSerialize(t *testing.T) {
+ var rene = identity.NewBare("René Descartes", "rene@descartes.fr")
+ unix := time.Now().Unix()
+ before := NewNoOpOp(rene, unix)
+
+ data, err := json.Marshal(before)
+ assert.NoError(t, err)
+
+ var after NoOpOperation
+ err = json.Unmarshal(data, &after)
+ assert.NoError(t, err)
+
+ assert.Equal(t, before, &after)
+}
diff --git a/bug/op_set_metadata.go b/bug/op_set_metadata.go
index aac81f3b..7616a591 100644
--- a/bug/op_set_metadata.go
+++ b/bug/op_set_metadata.go
@@ -1,13 +1,19 @@
package bug
-import "github.com/MichaelMure/git-bug/util/git"
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/MichaelMure/git-bug/identity"
+ "github.com/MichaelMure/git-bug/util/git"
+)
var _ Operation = &SetMetadataOperation{}
type SetMetadataOperation struct {
OpBase
- Target git.Hash `json:"target"`
- NewMetadata map[string]string `json:"new_metadata"`
+ Target git.Hash
+ NewMetadata map[string]string
}
func (op *SetMetadataOperation) base() *OpBase {
@@ -50,13 +56,65 @@ func (op *SetMetadataOperation) Validate() error {
return err
}
+ if !op.Target.IsValid() {
+ return fmt.Errorf("target hash is invalid")
+ }
+
+ return nil
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *SetMetadataOperation) MarshalJSON() ([]byte, error) {
+ base, err := json.Marshal(op.OpBase)
+ if err != nil {
+ return nil, err
+ }
+
+ // revert back to a flat map to be able to add our own fields
+ var data map[string]interface{}
+ if err := json.Unmarshal(base, &data); err != nil {
+ return nil, err
+ }
+
+ data["target"] = op.Target
+ data["new_metadata"] = op.NewMetadata
+
+ return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *SetMetadataOperation) UnmarshalJSON(data []byte) error {
+ // Unmarshal OpBase and the op separately
+
+ base := OpBase{}
+ err := json.Unmarshal(data, &base)
+ if err != nil {
+ return err
+ }
+
+ aux := struct {
+ Target git.Hash `json:"target"`
+ NewMetadata map[string]string `json:"new_metadata"`
+ }{}
+
+ err = json.Unmarshal(data, &aux)
+ if err != nil {
+ return err
+ }
+
+ op.OpBase = base
+ op.Target = aux.Target
+ op.NewMetadata = aux.NewMetadata
+
return nil
}
// Sign post method for gqlgen
func (op *SetMetadataOperation) IsAuthored() {}
-func NewSetMetadataOp(author Person, unixTime int64, target git.Hash, newMetadata map[string]string) *SetMetadataOperation {
+func NewSetMetadataOp(author identity.Interface, unixTime int64, target git.Hash, newMetadata map[string]string) *SetMetadataOperation {
return &SetMetadataOperation{
OpBase: newOpBase(SetMetadataOp, author, unixTime),
Target: target,
@@ -65,7 +123,7 @@ func NewSetMetadataOp(author Person, unixTime int64, target git.Hash, newMetadat
}
// Convenience function to apply the operation
-func SetMetadata(b Interface, author Person, unixTime int64, target git.Hash, newMetadata map[string]string) (*SetMetadataOperation, error) {
+func SetMetadata(b Interface, author identity.Interface, unixTime int64, target git.Hash, newMetadata map[string]string) (*SetMetadataOperation, error) {
SetMetadataOp := NewSetMetadataOp(author, unixTime, target, newMetadata)
if err := SetMetadataOp.Validate(); err != nil {
return nil, err
diff --git a/bug/op_set_metadata_test.go b/bug/op_set_metadata_test.go
index 068e2bb0..a7f9f313 100644
--- a/bug/op_set_metadata_test.go
+++ b/bug/op_set_metadata_test.go
@@ -1,20 +1,19 @@
package bug
import (
+ "encoding/json"
"testing"
"time"
+ "github.com/MichaelMure/git-bug/identity"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestSetMetadata(t *testing.T) {
snapshot := Snapshot{}
- var rene = Person{
- Name: "René Descartes",
- Email: "rene@descartes.fr",
- }
-
+ rene := identity.NewBare("René Descartes", "rene@descartes.fr")
unix := time.Now().Unix()
create := NewCreateOp(rene, unix, "title", "create", nil)
@@ -23,9 +22,7 @@ func TestSetMetadata(t *testing.T) {
snapshot.Operations = append(snapshot.Operations, create)
hash1, err := create.Hash()
- if err != nil {
- t.Fatal(err)
- }
+ require.NoError(t, err)
comment := NewAddCommentOp(rene, unix, "comment", nil)
comment.SetMetadata("key2", "value2")
@@ -33,9 +30,7 @@ func TestSetMetadata(t *testing.T) {
snapshot.Operations = append(snapshot.Operations, comment)
hash2, err := comment.Hash()
- if err != nil {
- t.Fatal(err)
- }
+ require.NoError(t, err)
op1 := NewSetMetadataOp(rene, unix, hash1, map[string]string{
"key": "override",
@@ -96,3 +91,21 @@ func TestSetMetadata(t *testing.T) {
assert.Equal(t, commentMetadata["key2"], "value2")
assert.Equal(t, commentMetadata["key3"], "value3")
}
+
+func TestSetMetadataSerialize(t *testing.T) {
+ var rene = identity.NewBare("René Descartes", "rene@descartes.fr")
+ unix := time.Now().Unix()
+ before := NewSetMetadataOp(rene, unix, "message", map[string]string{
+ "key1": "value1",
+ "key2": "value2",
+ })
+
+ data, err := json.Marshal(before)
+ assert.NoError(t, err)
+
+ var after SetMetadataOperation
+ err = json.Unmarshal(data, &after)
+ assert.NoError(t, err)
+
+ assert.Equal(t, before, &after)
+}
diff --git a/bug/op_set_status.go b/bug/op_set_status.go
index 54f476cb..0105d78d 100644
--- a/bug/op_set_status.go
+++ b/bug/op_set_status.go
@@ -1,7 +1,11 @@
package bug
import (
+ "encoding/json"
+
+ "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/git"
+ "github.com/MichaelMure/git-bug/util/timestamp"
"github.com/pkg/errors"
)
@@ -10,7 +14,7 @@ var _ Operation = &SetStatusOperation{}
// SetStatusOperation will change the status of a bug
type SetStatusOperation struct {
OpBase
- Status Status `json:"status"`
+ Status Status
}
func (op *SetStatusOperation) base() *OpBase {
@@ -34,7 +38,7 @@ func (op *SetStatusOperation) Apply(snapshot *Snapshot) {
item := &SetStatusTimelineItem{
hash: hash,
Author: op.Author,
- UnixTime: Timestamp(op.UnixTime),
+ UnixTime: timestamp.Timestamp(op.UnixTime),
Status: op.Status,
}
@@ -53,10 +57,55 @@ func (op *SetStatusOperation) Validate() error {
return nil
}
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *SetStatusOperation) MarshalJSON() ([]byte, error) {
+ base, err := json.Marshal(op.OpBase)
+ if err != nil {
+ return nil, err
+ }
+
+ // revert back to a flat map to be able to add our own fields
+ var data map[string]interface{}
+ if err := json.Unmarshal(base, &data); err != nil {
+ return nil, err
+ }
+
+ data["status"] = op.Status
+
+ return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *SetStatusOperation) UnmarshalJSON(data []byte) error {
+ // Unmarshal OpBase and the op separately
+
+ base := OpBase{}
+ err := json.Unmarshal(data, &base)
+ if err != nil {
+ return err
+ }
+
+ aux := struct {
+ Status Status `json:"status"`
+ }{}
+
+ err = json.Unmarshal(data, &aux)
+ if err != nil {
+ return err
+ }
+
+ op.OpBase = base
+ op.Status = aux.Status
+
+ return nil
+}
+
// Sign post method for gqlgen
func (op *SetStatusOperation) IsAuthored() {}
-func NewSetStatusOp(author Person, unixTime int64, status Status) *SetStatusOperation {
+func NewSetStatusOp(author identity.Interface, unixTime int64, status Status) *SetStatusOperation {
return &SetStatusOperation{
OpBase: newOpBase(SetStatusOp, author, unixTime),
Status: status,
@@ -65,8 +114,8 @@ func NewSetStatusOp(author Person, unixTime int64, status Status) *SetStatusOper
type SetStatusTimelineItem struct {
hash git.Hash
- Author Person
- UnixTime Timestamp
+ Author identity.Interface
+ UnixTime timestamp.Timestamp
Status Status
}
@@ -75,7 +124,7 @@ func (s SetStatusTimelineItem) Hash() git.Hash {
}
// Convenience function to apply the operation
-func Open(b Interface, author Person, unixTime int64) (*SetStatusOperation, error) {
+func Open(b Interface, author identity.Interface, unixTime int64) (*SetStatusOperation, error) {
op := NewSetStatusOp(author, unixTime, OpenStatus)
if err := op.Validate(); err != nil {
return nil, err
@@ -85,7 +134,7 @@ func Open(b Interface, author Person, unixTime int64) (*SetStatusOperation, erro
}
// Convenience function to apply the operation
-func Close(b Interface, author Person, unixTime int64) (*SetStatusOperation, error) {
+func Close(b Interface, author identity.Interface, unixTime int64) (*SetStatusOperation, error) {
op := NewSetStatusOp(author, unixTime, ClosedStatus)
if err := op.Validate(); err != nil {
return nil, err
diff --git a/bug/op_set_status_test.go b/bug/op_set_status_test.go
new file mode 100644
index 00000000..2506b947
--- /dev/null
+++ b/bug/op_set_status_test.go
@@ -0,0 +1,25 @@
+package bug
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/MichaelMure/git-bug/identity"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSetStatusSerialize(t *testing.T) {
+ var rene = identity.NewBare("René Descartes", "rene@descartes.fr")
+ unix := time.Now().Unix()
+ before := NewSetStatusOp(rene, unix, ClosedStatus)
+
+ data, err := json.Marshal(before)
+ assert.NoError(t, err)
+
+ var after SetStatusOperation
+ err = json.Unmarshal(data, &after)
+ assert.NoError(t, err)
+
+ assert.Equal(t, before, &after)
+}
diff --git a/bug/op_set_title.go b/bug/op_set_title.go
index b631ca18..084838cb 100644
--- a/bug/op_set_title.go
+++ b/bug/op_set_title.go
@@ -1,9 +1,13 @@
package bug
import (
+ "encoding/json"
"fmt"
"strings"
+ "github.com/MichaelMure/git-bug/identity"
+ "github.com/MichaelMure/git-bug/util/timestamp"
+
"github.com/MichaelMure/git-bug/util/git"
"github.com/MichaelMure/git-bug/util/text"
)
@@ -13,8 +17,8 @@ var _ Operation = &SetTitleOperation{}
// SetTitleOperation will change the title of a bug
type SetTitleOperation struct {
OpBase
- Title string `json:"title"`
- Was string `json:"was"`
+ Title string
+ Was string
}
func (op *SetTitleOperation) base() *OpBase {
@@ -38,7 +42,7 @@ func (op *SetTitleOperation) Apply(snapshot *Snapshot) {
item := &SetTitleTimelineItem{
hash: hash,
Author: op.Author,
- UnixTime: Timestamp(op.UnixTime),
+ UnixTime: timestamp.Timestamp(op.UnixTime),
Title: op.Title,
Was: op.Was,
}
@@ -74,10 +78,58 @@ func (op *SetTitleOperation) Validate() error {
return nil
}
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *SetTitleOperation) MarshalJSON() ([]byte, error) {
+ base, err := json.Marshal(op.OpBase)
+ if err != nil {
+ return nil, err
+ }
+
+ // revert back to a flat map to be able to add our own fields
+ var data map[string]interface{}
+ if err := json.Unmarshal(base, &data); err != nil {
+ return nil, err
+ }
+
+ data["title"] = op.Title
+ data["was"] = op.Was
+
+ return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *SetTitleOperation) UnmarshalJSON(data []byte) error {
+ // Unmarshal OpBase and the op separately
+
+ base := OpBase{}
+ err := json.Unmarshal(data, &base)
+ if err != nil {
+ return err
+ }
+
+ aux := struct {
+ Title string `json:"title"`
+ Was string `json:"was"`
+ }{}
+
+ err = json.Unmarshal(data, &aux)
+ if err != nil {
+ return err
+ }
+
+ op.OpBase = base
+ op.Title = aux.Title
+ op.Was = aux.Was
+
+ return nil
+}
+
// Sign post method for gqlgen
func (op *SetTitleOperation) IsAuthored() {}
-func NewSetTitleOp(author Person, unixTime int64, title string, was string) *SetTitleOperation {
+func NewSetTitleOp(author identity.Interface, unixTime int64, title string, was string) *SetTitleOperation {
return &SetTitleOperation{
OpBase: newOpBase(SetTitleOp, author, unixTime),
Title: title,
@@ -87,8 +139,8 @@ func NewSetTitleOp(author Person, unixTime int64, title string, was string) *Set
type SetTitleTimelineItem struct {
hash git.Hash
- Author Person
- UnixTime Timestamp
+ Author identity.Interface
+ UnixTime timestamp.Timestamp
Title string
Was string
}
@@ -98,7 +150,7 @@ func (s SetTitleTimelineItem) Hash() git.Hash {
}
// Convenience function to apply the operation
-func SetTitle(b Interface, author Person, unixTime int64, title string) (*SetTitleOperation, error) {
+func SetTitle(b Interface, author identity.Interface, unixTime int64, title string) (*SetTitleOperation, error) {
it := NewOperationIterator(b)
var lastTitleOp Operation
diff --git a/bug/op_set_title_test.go b/bug/op_set_title_test.go
new file mode 100644
index 00000000..1f730596
--- /dev/null
+++ b/bug/op_set_title_test.go
@@ -0,0 +1,25 @@
+package bug
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/MichaelMure/git-bug/identity"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSetTitleSerialize(t *testing.T) {
+ var rene = identity.NewBare("René Descartes", "rene@descartes.fr")
+ unix := time.Now().Unix()
+ before := NewSetTitleOp(rene, unix, "title", "was")
+
+ data, err := json.Marshal(before)
+ assert.NoError(t, err)
+
+ var after SetTitleOperation
+ err = json.Unmarshal(data, &after)
+ assert.NoError(t, err)
+
+ assert.Equal(t, before, &after)
+}
diff --git a/bug/operation.go b/bug/operation.go
index 592b5616..cc5b0007 100644
--- a/bug/operation.go
+++ b/bug/operation.go
@@ -6,6 +6,8 @@ import (
"fmt"
"time"
+ "github.com/MichaelMure/git-bug/identity"
+
"github.com/MichaelMure/git-bug/util/git"
"github.com/pkg/errors"
)
@@ -76,19 +78,19 @@ func hashOperation(op Operation) (git.Hash, error) {
// OpBase implement the common code for all operations
type OpBase struct {
- OperationType OperationType `json:"type"`
- Author Person `json:"author"`
- UnixTime int64 `json:"timestamp"`
- Metadata map[string]string `json:"metadata,omitempty"`
+ OperationType OperationType
+ Author identity.Interface
+ UnixTime int64
+ Metadata map[string]string
// Not serialized. Store the op's hash in memory.
hash git.Hash
- // Not serialized. Store the extra metadata compiled from SetMetadataOperation
- // in memory.
+ // Not serialized. Store the extra metadata in memory,
+ // compiled from SetMetadataOperation.
extraMetadata map[string]string
}
// newOpBase is the constructor for an OpBase
-func newOpBase(opType OperationType, author Person, unixTime int64) OpBase {
+func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase {
return OpBase{
OperationType: opType,
Author: author,
@@ -96,6 +98,46 @@ func newOpBase(opType OperationType, author Person, unixTime int64) OpBase {
}
}
+func (op OpBase) MarshalJSON() ([]byte, error) {
+ return json.Marshal(struct {
+ OperationType OperationType `json:"type"`
+ Author identity.Interface `json:"author"`
+ UnixTime int64 `json:"timestamp"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+ }{
+ OperationType: op.OperationType,
+ Author: op.Author,
+ UnixTime: op.UnixTime,
+ Metadata: op.Metadata,
+ })
+}
+
+func (op *OpBase) UnmarshalJSON(data []byte) error {
+ aux := struct {
+ OperationType OperationType `json:"type"`
+ Author json.RawMessage `json:"author"`
+ UnixTime int64 `json:"timestamp"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+ }{}
+
+ if err := json.Unmarshal(data, &aux); err != nil {
+ return err
+ }
+
+ // delegate the decoding of the identity
+ author, err := identity.UnmarshalJSON(aux.Author)
+ if err != nil {
+ return err
+ }
+
+ op.OperationType = aux.OperationType
+ op.Author = author
+ op.UnixTime = aux.UnixTime
+ op.Metadata = aux.Metadata
+
+ return nil
+}
+
// Time return the time when the operation was added
func (op *OpBase) Time() time.Time {
return time.Unix(op.UnixTime, 0)
@@ -117,14 +159,14 @@ func opBaseValidate(op Operation, opType OperationType) error {
return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, op.base().OperationType)
}
- if _, err := op.Hash(); err != nil {
- return errors.Wrap(err, "op is not serializable")
- }
-
if op.GetUnixTime() == 0 {
return fmt.Errorf("time not set")
}
+ if op.base().Author == nil {
+ return fmt.Errorf("author not set")
+ }
+
if err := op.base().Author.Validate(); err != nil {
return errors.Wrap(err, "author")
}
diff --git a/bug/operation_iterator_test.go b/bug/operation_iterator_test.go
index 506cc94f..b922bec1 100644
--- a/bug/operation_iterator_test.go
+++ b/bug/operation_iterator_test.go
@@ -1,29 +1,39 @@
package bug
import (
+ "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
+ "github.com/stretchr/testify/assert"
+
"testing"
"time"
)
-var (
- rene = Person{
- Name: "René Descartes",
- Email: "rene@descartes.fr",
- }
+func ExampleOperationIterator() {
+ b := NewBug()
- unix = time.Now().Unix()
+ // add operations
- createOp = NewCreateOp(rene, unix, "title", "message", nil)
- setTitleOp = NewSetTitleOp(rene, unix, "title2", "title1")
- addCommentOp = NewAddCommentOp(rene, unix, "message2", nil)
- setStatusOp = NewSetStatusOp(rene, unix, ClosedStatus)
- labelChangeOp = NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"})
-)
+ it := NewOperationIterator(b)
+
+ for it.Next() {
+ // do something with each operations
+ _ = it.Value()
+ }
+}
func TestOpIterator(t *testing.T) {
mockRepo := repository.NewMockRepoForTest()
+ rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
+ unix := time.Now().Unix()
+
+ createOp := NewCreateOp(rene, unix, "title", "message", nil)
+ setTitleOp := NewSetTitleOp(rene, unix, "title2", "title1")
+ addCommentOp := NewAddCommentOp(rene, unix, "message2", nil)
+ setStatusOp := NewSetStatusOp(rene, unix, ClosedStatus)
+ labelChangeOp := NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"})
+
bug1 := NewBug()
// first pack
@@ -32,13 +42,15 @@ func TestOpIterator(t *testing.T) {
bug1.Append(addCommentOp)
bug1.Append(setStatusOp)
bug1.Append(labelChangeOp)
- bug1.Commit(mockRepo)
+ err := bug1.Commit(mockRepo)
+ assert.NoError(t, err)
// second pack
bug1.Append(setTitleOp)
bug1.Append(setTitleOp)
bug1.Append(setTitleOp)
- bug1.Commit(mockRepo)
+ err = bug1.Commit(mockRepo)
+ assert.NoError(t, err)
// staging
bug1.Append(setTitleOp)
diff --git a/bug/operation_pack.go b/bug/operation_pack.go
index f33d94bf..5f3e9da8 100644
--- a/bug/operation_pack.go
+++ b/bug/operation_pack.go
@@ -20,7 +20,7 @@ const formatVersion = 1
type OperationPack struct {
Operations []Operation
- // Private field so not serialized by gob
+ // Private field so not serialized
commitHash git.Hash
}
@@ -57,6 +57,7 @@ func (opp *OperationPack) UnmarshalJSON(data []byte) error {
return err
}
+ // delegate to specialized unmarshal function
op, err := opp.unmarshalOp(raw, t.OperationType)
if err != nil {
return err
@@ -73,28 +74,36 @@ func (opp *OperationPack) UnmarshalJSON(data []byte) error {
func (opp *OperationPack) unmarshalOp(raw []byte, _type OperationType) (Operation, error) {
switch _type {
+ case AddCommentOp:
+ op := &AddCommentOperation{}
+ err := json.Unmarshal(raw, &op)
+ return op, err
case CreateOp:
op := &CreateOperation{}
err := json.Unmarshal(raw, &op)
return op, err
- case SetTitleOp:
- op := &SetTitleOperation{}
+ case EditCommentOp:
+ op := &EditCommentOperation{}
err := json.Unmarshal(raw, &op)
return op, err
- case AddCommentOp:
- op := &AddCommentOperation{}
+ case LabelChangeOp:
+ op := &LabelChangeOperation{}
err := json.Unmarshal(raw, &op)
return op, err
- case SetStatusOp:
- op := &SetStatusOperation{}
+ case NoOpOp:
+ op := &NoOpOperation{}
err := json.Unmarshal(raw, &op)
return op, err
- case LabelChangeOp:
- op := &LabelChangeOperation{}
+ case SetMetadataOp:
+ op := &SetMetadataOperation{}
err := json.Unmarshal(raw, &op)
return op, err
- case EditCommentOp:
- op := &EditCommentOperation{}
+ case SetStatusOp:
+ op := &SetStatusOperation{}
+ err := json.Unmarshal(raw, &op)
+ return op, err
+ case SetTitleOp:
+ op := &SetTitleOperation{}
err := json.Unmarshal(raw, &op)
return op, err
default:
@@ -129,7 +138,21 @@ func (opp *OperationPack) Validate() error {
// Write will serialize and store the OperationPack as a git blob and return
// its hash
-func (opp *OperationPack) Write(repo repository.Repo) (git.Hash, error) {
+func (opp *OperationPack) Write(repo repository.ClockedRepo) (git.Hash, error) {
+ // make sure we don't write invalid data
+ err := opp.Validate()
+ if err != nil {
+ return "", errors.Wrap(err, "validation error")
+ }
+
+ // First, make sure that all the identities are properly Commit as well
+ for _, op := range opp.Operations {
+ err := op.base().Author.CommitAsNeeded(repo)
+ if err != nil {
+ return "", err
+ }
+ }
+
data, err := json.Marshal(opp)
if err != nil {
diff --git a/bug/operation_pack_test.go b/bug/operation_pack_test.go
index 48f9f80c..09d159af 100644
--- a/bug/operation_pack_test.go
+++ b/bug/operation_pack_test.go
@@ -3,51 +3,59 @@ package bug
import (
"encoding/json"
"testing"
+ "time"
+ "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/git"
- "github.com/go-test/deep"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestOperationPackSerialize(t *testing.T) {
opp := &OperationPack{}
+ rene := identity.NewBare("René Descartes", "rene@descartes.fr")
+ createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
+ setTitleOp := NewSetTitleOp(rene, time.Now().Unix(), "title2", "title1")
+ addCommentOp := NewAddCommentOp(rene, time.Now().Unix(), "message2", nil)
+ setStatusOp := NewSetStatusOp(rene, time.Now().Unix(), ClosedStatus)
+ labelChangeOp := NewLabelChangeOperation(rene, time.Now().Unix(), []Label{"added"}, []Label{"removed"})
+
opp.Append(createOp)
opp.Append(setTitleOp)
opp.Append(addCommentOp)
opp.Append(setStatusOp)
opp.Append(labelChangeOp)
- opMeta := NewCreateOp(rene, unix, "title", "message", nil)
+ opMeta := NewSetTitleOp(rene, time.Now().Unix(), "title3", "title2")
opMeta.SetMetadata("key", "value")
opp.Append(opMeta)
- if len(opMeta.Metadata) != 1 {
- t.Fatal()
- }
+ assert.Equal(t, 1, len(opMeta.Metadata))
- opFile := NewCreateOp(rene, unix, "title", "message", []git.Hash{
+ opFile := NewAddCommentOp(rene, time.Now().Unix(), "message", []git.Hash{
"abcdef",
"ghijkl",
})
opp.Append(opFile)
- if len(opFile.Files) != 2 {
- t.Fatal()
- }
+ assert.Equal(t, 2, len(opFile.Files))
data, err := json.Marshal(opp)
- if err != nil {
- t.Fatal(err)
- }
+ assert.NoError(t, err)
var opp2 *OperationPack
err = json.Unmarshal(data, &opp2)
- if err != nil {
- t.Fatal(err)
- }
+ assert.NoError(t, err)
+
+ ensureHash(t, opp)
+
+ assert.Equal(t, opp, opp2)
+}
- deep.CompareUnexportedFields = false
- if diff := deep.Equal(opp, opp2); diff != nil {
- t.Fatal(diff)
+func ensureHash(t *testing.T, opp *OperationPack) {
+ for _, op := range opp.Operations {
+ _, err := op.Hash()
+ require.NoError(t, err)
}
}
diff --git a/bug/operation_test.go b/bug/operation_test.go
index 255d6d98..f9a7d191 100644
--- a/bug/operation_test.go
+++ b/bug/operation_test.go
@@ -2,13 +2,19 @@ package bug
import (
"testing"
+ "time"
+ "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/git"
+ "github.com/MichaelMure/git-bug/util/test"
"github.com/stretchr/testify/require"
)
func TestValidate(t *testing.T) {
+ rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
+ unix := time.Now().Unix()
+
good := []Operation{
NewCreateOp(rene, unix, "title", "message", nil),
NewSetTitleOp(rene, unix, "title2", "title1"),
@@ -25,11 +31,11 @@ func TestValidate(t *testing.T) {
bad := []Operation{
// opbase
- NewSetStatusOp(Person{Name: "", Email: "rene@descartes.fr"}, unix, ClosedStatus),
- NewSetStatusOp(Person{Name: "René Descartes\u001b", Email: "rene@descartes.fr"}, unix, ClosedStatus),
- NewSetStatusOp(Person{Name: "René Descartes", Email: "rene@descartes.fr\u001b"}, unix, ClosedStatus),
- NewSetStatusOp(Person{Name: "René \nDescartes", Email: "rene@descartes.fr"}, unix, ClosedStatus),
- NewSetStatusOp(Person{Name: "René Descartes", Email: "rene@\ndescartes.fr"}, unix, ClosedStatus),
+ NewSetStatusOp(identity.NewIdentity("", "rene@descartes.fr"), unix, ClosedStatus),
+ NewSetStatusOp(identity.NewIdentity("René Descartes\u001b", "rene@descartes.fr"), unix, ClosedStatus),
+ NewSetStatusOp(identity.NewIdentity("René Descartes", "rene@descartes.fr\u001b"), unix, ClosedStatus),
+ NewSetStatusOp(identity.NewIdentity("René \nDescartes", "rene@descartes.fr"), unix, ClosedStatus),
+ NewSetStatusOp(identity.NewIdentity("René Descartes", "rene@\ndescartes.fr"), unix, ClosedStatus),
&CreateOperation{OpBase: OpBase{
Author: rene,
UnixTime: 0,
@@ -63,7 +69,8 @@ func TestValidate(t *testing.T) {
}
func TestMetadata(t *testing.T) {
- op := NewCreateOp(rene, unix, "title", "message", nil)
+ rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
+ op := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
op.SetMetadata("key", "value")
@@ -75,11 +82,13 @@ func TestMetadata(t *testing.T) {
func TestHash(t *testing.T) {
repos := []repository.ClockedRepo{
repository.NewMockRepoForTest(),
- createRepo(false),
+ test.CreateRepo(false),
}
for _, repo := range repos {
- b, op, err := Create(rene, unix, "title", "message")
+ rene := identity.NewBare("René Descartes", "rene@descartes.fr")
+
+ b, op, err := Create(rene, time.Now().Unix(), "title", "message")
require.Nil(t, err)
h1, err := op.Hash()
diff --git a/bug/person.go b/bug/person.go
deleted file mode 100644
index 449e2262..00000000
--- a/bug/person.go
+++ /dev/null
@@ -1,95 +0,0 @@
-package bug
-
-import (
- "errors"
- "fmt"
- "strings"
-
- "github.com/MichaelMure/git-bug/repository"
- "github.com/MichaelMure/git-bug/util/text"
-)
-
-type Person struct {
- Name string `json:"name"`
- Email string `json:"email"`
- Login string `json:"login"`
- AvatarUrl string `json:"avatar_url"`
-}
-
-// GetUser will query the repository for user detail and build the corresponding Person
-func GetUser(repo repository.Repo) (Person, error) {
- name, err := repo.GetUserName()
- if err != nil {
- return Person{}, err
- }
- if name == "" {
- return Person{}, errors.New("User name is not configured in git yet. Please use `git config --global user.name \"John Doe\"`")
- }
-
- email, err := repo.GetUserEmail()
- if err != nil {
- return Person{}, err
- }
- if email == "" {
- return Person{}, errors.New("User name is not configured in git yet. Please use `git config --global user.email johndoe@example.com`")
- }
-
- return Person{Name: name, Email: email}, nil
-}
-
-// Match tell is the Person match the given query string
-func (p Person) Match(query string) bool {
- query = strings.ToLower(query)
-
- return strings.Contains(strings.ToLower(p.Name), query) ||
- strings.Contains(strings.ToLower(p.Login), query)
-}
-
-func (p Person) Validate() error {
- if text.Empty(p.Name) && text.Empty(p.Login) {
- return fmt.Errorf("either name or login should be set")
- }
-
- if strings.Contains(p.Name, "\n") {
- return fmt.Errorf("name should be a single line")
- }
-
- if !text.Safe(p.Name) {
- return fmt.Errorf("name is not fully printable")
- }
-
- if strings.Contains(p.Login, "\n") {
- return fmt.Errorf("login should be a single line")
- }
-
- if !text.Safe(p.Login) {
- return fmt.Errorf("login is not fully printable")
- }
-
- if strings.Contains(p.Email, "\n") {
- return fmt.Errorf("email should be a single line")
- }
-
- if !text.Safe(p.Email) {
- return fmt.Errorf("email is not fully printable")
- }
-
- if p.AvatarUrl != "" && !text.ValidUrl(p.AvatarUrl) {
- return fmt.Errorf("avatarUrl is not a valid URL")
- }
-
- return nil
-}
-
-func (p Person) DisplayName() string {
- switch {
- case p.Name == "" && p.Login != "":
- return p.Login
- case p.Name != "" && p.Login == "":
- return p.Name
- case p.Name != "" && p.Login != "":
- return fmt.Sprintf("%s (%s)", p.Name, p.Login)
- }
-
- panic("invalid person data")
-}
diff --git a/bug/snapshot.go b/bug/snapshot.go
index 72e673d4..46618ebd 100644
--- a/bug/snapshot.go
+++ b/bug/snapshot.go
@@ -4,6 +4,7 @@ import (
"fmt"
"time"
+ "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/git"
)
@@ -15,7 +16,7 @@ type Snapshot struct {
Title string
Comments []Comment
Labels []Label
- Author Person
+ Author identity.Interface
CreatedAt time.Time
Timeline []TimelineItem
diff --git a/bug/time.go b/bug/time.go
deleted file mode 100644
index a085e8e9..00000000
--- a/bug/time.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package bug
-
-import "time"
-
-type Timestamp int64
-
-func (t Timestamp) Time() time.Time {
- return time.Unix(int64(t), 0)
-}
diff --git a/bug/timeline.go b/bug/timeline.go
index a84c45e4..d8ee2c6b 100644
--- a/bug/timeline.go
+++ b/bug/timeline.go
@@ -3,7 +3,9 @@ package bug
import (
"strings"
+ "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/git"
+ "github.com/MichaelMure/git-bug/util/timestamp"
)
type TimelineItem interface {
@@ -15,20 +17,20 @@ type TimelineItem interface {
type CommentHistoryStep struct {
// The author of the edition, not necessarily the same as the author of the
// original comment
- Author Person
+ Author identity.Interface
// The new message
Message string
- UnixTime Timestamp
+ UnixTime timestamp.Timestamp
}
// CommentTimelineItem is a TimelineItem that holds a Comment and its edition history
type CommentTimelineItem struct {
hash git.Hash
- Author Person
+ Author identity.Interface
Message string
Files []git.Hash
- CreatedAt Timestamp
- LastEdit Timestamp
+ CreatedAt timestamp.Timestamp
+ LastEdit timestamp.Timestamp
History []CommentHistoryStep
}