entity: more testing and bug fixing

Michael Muré created

Change summary

bug/bug_actions.go                |  11 -
entity/dag/common_test.go         |  28 +++-
entity/dag/entity_actions.go      |  12 -
entity/dag/entity_actions_test.go | 181 ++++++++++++++++++++++++++------
identity/identity_actions.go      |  11 -
repository/gogit.go               |  46 +++++++-
repository/mock_repo.go           |   4 
repository/repo.go                |  17 ++
8 files changed, 221 insertions(+), 89 deletions(-)

Detailed changes

bug/bug_actions.go 🔗

@@ -14,19 +14,12 @@ import (
 // Fetch retrieve updates from a remote
 // This does not change the local bugs state
 func Fetch(repo repository.Repo, remote string) (string, error) {
-	// "refs/bugs/*:refs/remotes/<remote>>/bugs/*"
-	remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote)
-	fetchRefSpec := fmt.Sprintf("%s*:%s*", bugsRefPattern, remoteRefSpec)
-
-	return repo.FetchRefs(remote, fetchRefSpec)
+	return repo.FetchRefs(remote, "bugs")
 }
 
 // Push update a remote with the local changes
 func Push(repo repository.Repo, remote string) (string, error) {
-	// "refs/bugs/*:refs/bugs/*"
-	refspec := fmt.Sprintf("%s*:%s*", bugsRefPattern, bugsRefPattern)
-
-	return repo.PushRefs(remote, refspec)
+	return repo.PushRefs(remote, "bugs")
 }
 
 // Pull will do a Fetch + MergeAll

entity/dag/common_test.go 🔗

@@ -3,6 +3,9 @@ package dag
 import (
 	"encoding/json"
 	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/require"
 
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/identity"
@@ -94,23 +97,28 @@ func makeTestContext() (repository.ClockedRepo, identity.Interface, identity.Int
 	return repo, id1, id2, def
 }
 
-func makeTestContextRemote() (repository.ClockedRepo, repository.ClockedRepo, repository.ClockedRepo, identity.Interface, identity.Interface, Definition) {
+func makeTestContextRemote(t *testing.T) (repository.ClockedRepo, repository.ClockedRepo, repository.ClockedRepo, identity.Interface, identity.Interface, Definition) {
 	repoA := repository.CreateGoGitTestRepo(false)
 	repoB := repository.CreateGoGitTestRepo(false)
 	remote := repository.CreateGoGitTestRepo(true)
 
-	err := repoA.AddRemote("origin", remote.GetLocalRemote())
-	if err != nil {
-		panic(err)
-	}
-
-	err = repoB.AddRemote("origin", remote.GetLocalRemote())
-	if err != nil {
-		panic(err)
-	}
+	err := repoA.AddRemote("remote", remote.GetLocalRemote())
+	require.NoError(t, err)
+	err = repoA.AddRemote("repoB", repoB.GetLocalRemote())
+	require.NoError(t, err)
+	err = repoB.AddRemote("remote", remote.GetLocalRemote())
+	require.NoError(t, err)
+	err = repoB.AddRemote("repoA", repoA.GetLocalRemote())
+	require.NoError(t, err)
 
 	id1, id2, def := makeTestContextInternal(repoA)
 
+	// distribute the identities
+	_, err = identity.Push(repoA, "remote")
+	require.NoError(t, err)
+	err = identity.Pull(repoB, "remote")
+	require.NoError(t, err)
+
 	return repoA, repoB, remote, id1, id2, def
 }
 

entity/dag/entity_actions.go 🔗

@@ -21,20 +21,12 @@ func ListLocalIds(def Definition, repo repository.RepoData) ([]entity.Id, error)
 // Fetch retrieve updates from a remote
 // This does not change the local entity state
 func Fetch(def Definition, repo repository.Repo, remote string) (string, error) {
-	// "refs/<entity>/*:refs/remotes/<remote>/<entity>/*"
-	fetchRefSpec := fmt.Sprintf("refs/%s/*:refs/remotes/%s/%s/*",
-		def.namespace, remote, def.namespace)
-
-	return repo.FetchRefs(remote, fetchRefSpec)
+	return repo.FetchRefs(remote, def.namespace)
 }
 
 // Push update a remote with the local changes
 func Push(def Definition, repo repository.Repo, remote string) (string, error) {
-	// "refs/<entity>/*:refs/<entity>/*"
-	refspec := fmt.Sprintf("refs/%s/*:refs/%s/*",
-		def.namespace, def.namespace)
-
-	return repo.PushRefs(remote, refspec)
+	return repo.PushRefs(remote, def.namespace)
 }
 
 // Pull will do a Fetch + MergeAll

entity/dag/entity_actions_test.go 🔗

@@ -1,62 +1,58 @@
 package dag
 
 import (
+	"sort"
 	"testing"
 
 	"github.com/stretchr/testify/require"
 
-	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/repository"
 )
 
 func allEntities(t testing.TB, bugs <-chan StreamedEntity) []*Entity {
+	t.Helper()
+
 	var result []*Entity
 	for streamed := range bugs {
-		if streamed.Err != nil {
-			t.Fatal(streamed.Err)
-		}
+		require.NoError(t, streamed.Err)
+
 		result = append(result, streamed.Entity)
 	}
 	return result
 }
 
 func TestPushPull(t *testing.T) {
-	repoA, repoB, remote, id1, id2, def := makeTestContextRemote()
+	repoA, repoB, remote, id1, id2, def := makeTestContextRemote(t)
 	defer repository.CleanupTestRepos(repoA, repoB, remote)
 
-	// distribute the identities
-	_, err := identity.Push(repoA, "origin")
-	require.NoError(t, err)
-	err = identity.Pull(repoB, "origin")
-	require.NoError(t, err)
-
 	// A --> remote --> B
-	entity := New(def)
-	entity.Append(newOp1(id1, "foo"))
+	e := New(def)
+	e.Append(newOp1(id1, "foo"))
 
-	err = entity.Commit(repoA)
+	err := e.Commit(repoA)
 	require.NoError(t, err)
 
-	_, err = Push(def, repoA, "origin")
+	_, err = Push(def, repoA, "remote")
 	require.NoError(t, err)
 
-	err = Pull(def, repoB, "origin")
+	err = Pull(def, repoB, "remote")
 	require.NoError(t, err)
 
 	entities := allEntities(t, ReadAll(def, repoB))
 	require.Len(t, entities, 1)
 
 	// B --> remote --> A
-	entity = New(def)
-	entity.Append(newOp2(id2, "bar"))
+	e = New(def)
+	e.Append(newOp2(id2, "bar"))
 
-	err = entity.Commit(repoB)
+	err = e.Commit(repoB)
 	require.NoError(t, err)
 
-	_, err = Push(def, repoB, "origin")
+	_, err = Push(def, repoB, "remote")
 	require.NoError(t, err)
 
-	err = Pull(def, repoA, "origin")
+	err = Pull(def, repoA, "remote")
 	require.NoError(t, err)
 
 	entities = allEntities(t, ReadAll(def, repoB))
@@ -64,39 +60,33 @@ func TestPushPull(t *testing.T) {
 }
 
 func TestListLocalIds(t *testing.T) {
-	repoA, repoB, remote, id1, id2, def := makeTestContextRemote()
+	repoA, repoB, remote, id1, id2, def := makeTestContextRemote(t)
 	defer repository.CleanupTestRepos(repoA, repoB, remote)
 
-	// distribute the identities
-	_, err := identity.Push(repoA, "origin")
-	require.NoError(t, err)
-	err = identity.Pull(repoB, "origin")
-	require.NoError(t, err)
-
 	// A --> remote --> B
-	entity := New(def)
-	entity.Append(newOp1(id1, "foo"))
-	err = entity.Commit(repoA)
+	e := New(def)
+	e.Append(newOp1(id1, "foo"))
+	err := e.Commit(repoA)
 	require.NoError(t, err)
 
-	entity = New(def)
-	entity.Append(newOp2(id2, "bar"))
-	err = entity.Commit(repoA)
+	e = New(def)
+	e.Append(newOp2(id2, "bar"))
+	err = e.Commit(repoA)
 	require.NoError(t, err)
 
 	listLocalIds(t, def, repoA, 2)
 	listLocalIds(t, def, repoB, 0)
 
-	_, err = Push(def, repoA, "origin")
+	_, err = Push(def, repoA, "remote")
 	require.NoError(t, err)
 
-	_, err = Fetch(def, repoB, "origin")
+	_, err = Fetch(def, repoB, "remote")
 	require.NoError(t, err)
 
 	listLocalIds(t, def, repoA, 2)
 	listLocalIds(t, def, repoB, 0)
 
-	err = Pull(def, repoB, "origin")
+	err = Pull(def, repoB, "remote")
 	require.NoError(t, err)
 
 	listLocalIds(t, def, repoA, 2)
@@ -108,3 +98,120 @@ func listLocalIds(t *testing.T, def Definition, repo repository.RepoData, expect
 	require.NoError(t, err)
 	require.Len(t, ids, expectedCount)
 }
+
+func assertMergeResults(t *testing.T, expected []entity.MergeResult, results <-chan entity.MergeResult) {
+	t.Helper()
+
+	var allResults []entity.MergeResult
+	for result := range results {
+		allResults = append(allResults, result)
+	}
+
+	require.Equal(t, len(expected), len(allResults))
+
+	sort.Slice(allResults, func(i, j int) bool {
+		return allResults[i].Id < allResults[j].Id
+	})
+	sort.Slice(expected, func(i, j int) bool {
+		return expected[i].Id < expected[j].Id
+	})
+
+	for i, result := range allResults {
+		require.NoError(t, result.Err)
+
+		require.Equal(t, expected[i].Id, result.Id)
+		require.Equal(t, expected[i].Status, result.Status)
+
+		switch result.Status {
+		case entity.MergeStatusNew, entity.MergeStatusUpdated:
+			require.NotNil(t, result.Entity)
+			require.Equal(t, expected[i].Id, result.Entity.Id())
+		}
+
+		i++
+	}
+}
+
+func TestMerge(t *testing.T) {
+	repoA, repoB, remote, id1, id2, def := makeTestContextRemote(t)
+	defer repository.CleanupTestRepos(repoA, repoB, remote)
+
+	// SCENARIO 1
+	// if the remote Entity doesn't exist locally, it's created
+
+	// 2 entities in repoA + push to remote
+	e1 := New(def)
+	e1.Append(newOp1(id1, "foo"))
+	err := e1.Commit(repoA)
+	require.NoError(t, err)
+
+	e2 := New(def)
+	e2.Append(newOp2(id2, "bar"))
+	err = e2.Commit(repoA)
+	require.NoError(t, err)
+
+	_, err = Push(def, repoA, "remote")
+	require.NoError(t, err)
+
+	// repoB: fetch + merge from remote
+
+	_, err = Fetch(def, repoB, "remote")
+	require.NoError(t, err)
+
+	results := MergeAll(def, repoB, "remote")
+
+	assertMergeResults(t, []entity.MergeResult{
+		{
+			Id:     e1.Id(),
+			Status: entity.MergeStatusNew,
+		},
+		{
+			Id:     e2.Id(),
+			Status: entity.MergeStatusNew,
+		},
+	}, results)
+
+	// SCENARIO 2
+	// if the remote and local Entity have the same state, nothing is changed
+
+	results = MergeAll(def, repoB, "remote")
+
+	assertMergeResults(t, []entity.MergeResult{
+		{
+			Id:     e1.Id(),
+			Status: entity.MergeStatusNothing,
+		},
+		{
+			Id:     e2.Id(),
+			Status: entity.MergeStatusNothing,
+		},
+	}, results)
+
+	// SCENARIO 3
+	// if the local Entity has new commits but the remote don't, nothing is changed
+
+	e1.Append(newOp1(id1, "barbar"))
+	err = e1.Commit(repoA)
+	require.NoError(t, err)
+
+	e2.Append(newOp2(id2, "barbarbar"))
+	err = e2.Commit(repoA)
+	require.NoError(t, err)
+
+	results = MergeAll(def, repoA, "remote")
+
+	assertMergeResults(t, []entity.MergeResult{
+		{
+			Id:     e1.Id(),
+			Status: entity.MergeStatusNothing,
+		},
+		{
+			Id:     e2.Id(),
+			Status: entity.MergeStatusNothing,
+		},
+	}, results)
+
+	// SCENARIO 4
+	// if the remote has new commit, the local bug is updated to match the same history
+	// (fast-forward update)
+}

identity/identity_actions.go 🔗

@@ -13,19 +13,12 @@ import (
 // Fetch retrieve updates from a remote
 // This does not change the local identities state
 func Fetch(repo repository.Repo, remote string) (string, error) {
-	// "refs/identities/*:refs/remotes/<remote>/identities/*"
-	remoteRefSpec := fmt.Sprintf(identityRemoteRefPattern, remote)
-	fetchRefSpec := fmt.Sprintf("%s*:%s*", identityRefPattern, remoteRefSpec)
-
-	return repo.FetchRefs(remote, fetchRefSpec)
+	return repo.FetchRefs(remote, "identities")
 }
 
 // Push update a remote with the local changes
 func Push(repo repository.Repo, remote string) (string, error) {
-	// "refs/identities/*:refs/identities/*"
-	refspec := fmt.Sprintf("%s*:%s*", identityRefPattern, identityRefPattern)
-
-	return repo.PushRefs(remote, refspec)
+	return repo.PushRefs(remote, "identities")
 }
 
 // Pull will do a Fetch + MergeAll

repository/gogit.go 🔗

@@ -353,13 +353,17 @@ func (repo *GoGitRepo) ClearBleveIndex(name string) error {
 	return nil
 }
 
-// FetchRefs fetch git refs from a remote
-func (repo *GoGitRepo) FetchRefs(remote string, refSpec string) (string, error) {
+// FetchRefs fetch git refs matching a directory prefix to a remote
+// Ex: prefix="foo" will fetch any remote refs matching "refs/foo/*" locally.
+// The equivalent git refspec would be "refs/foo/*:refs/remotes/<remote>/foo/*"
+func (repo *GoGitRepo) FetchRefs(remote string, prefix string) (string, error) {
+	refspec := fmt.Sprintf("refs/%s/*:refs/remotes/%s/%s/*", prefix, remote, prefix)
+
 	buf := bytes.NewBuffer(nil)
 
 	err := repo.r.Fetch(&gogit.FetchOptions{
 		RemoteName: remote,
-		RefSpecs:   []config.RefSpec{config.RefSpec(refSpec)},
+		RefSpecs:   []config.RefSpec{config.RefSpec(refspec)},
 		Progress:   buf,
 	})
 	if err == gogit.NoErrAlreadyUpToDate {
@@ -372,13 +376,41 @@ func (repo *GoGitRepo) FetchRefs(remote string, refSpec string) (string, error)
 	return buf.String(), nil
 }
 
-// PushRefs push git refs to a remote
-func (repo *GoGitRepo) PushRefs(remote string, refSpec string) (string, error) {
+// PushRefs push git refs matching a directory prefix to a remote
+// Ex: prefix="foo" will push any local refs matching "refs/foo/*" to the remote.
+// The equivalent git refspec would be "refs/foo/*:refs/foo/*"
+//
+// Additionally, PushRefs will update the local references in refs/remotes/<remote>/foo to match
+// the remote state.
+func (repo *GoGitRepo) PushRefs(remote string, prefix string) (string, error) {
+	refspec := fmt.Sprintf("refs/%s/*:refs/%s/*", prefix, prefix)
+
+	remo, err := repo.r.Remote(remote)
+	if err != nil {
+		return "", err
+	}
+
+	// to make sure that the push also create the corresponding refs/remotes/<remote>/... references,
+	// we need to have a default fetch refspec configured on the remote, to make our refs "track" the remote ones.
+	// This does not change the config on disk, only on memory.
+	hasCustomFetch := false
+	fetchRefspec := fmt.Sprintf("refs/%s/*:refs/remotes/%s/%s/*", prefix, remote, prefix)
+	for _, r := range remo.Config().Fetch {
+		if string(r) == fetchRefspec {
+			hasCustomFetch = true
+			break
+		}
+	}
+
+	if !hasCustomFetch {
+		remo.Config().Fetch = append(remo.Config().Fetch, config.RefSpec(fetchRefspec))
+	}
+
 	buf := bytes.NewBuffer(nil)
 
-	err := repo.r.Push(&gogit.PushOptions{
+	err = remo.Push(&gogit.PushOptions{
 		RemoteName: remote,
-		RefSpecs:   []config.RefSpec{config.RefSpec(refSpec)},
+		RefSpecs:   []config.RefSpec{config.RefSpec(refspec)},
 		Progress:   buf,
 	})
 	if err == gogit.NoErrAlreadyUpToDate {

repository/mock_repo.go 🔗

@@ -201,12 +201,12 @@ func NewMockRepoData() *mockRepoData {
 	}
 }
 
-func (r *mockRepoData) FetchRefs(remote string, refSpec string) (string, error) {
+func (r *mockRepoData) FetchRefs(remote string, prefix string) (string, error) {
 	panic("implement me")
 }
 
 // PushRefs push git refs to a remote
-func (r *mockRepoData) PushRefs(remote string, refSpec string) (string, error) {
+func (r *mockRepoData) PushRefs(remote string, prefix string) (string, error) {
 	panic("implement me")
 }
 

repository/repo.go 🔗

@@ -100,11 +100,18 @@ type Commit struct {
 
 // RepoData give access to the git data storage
 type RepoData interface {
-	// FetchRefs fetch git refs from a remote
-	FetchRefs(remote string, refSpec string) (string, error)
-
-	// PushRefs push git refs to a remote
-	PushRefs(remote string, refSpec string) (string, error)
+	// FetchRefs fetch git refs matching a directory prefix to a remote
+	// Ex: prefix="foo" will fetch any remote refs matching "refs/foo/*" locally.
+	// The equivalent git refspec would be "refs/foo/*:refs/remotes/<remote>/foo/*"
+	FetchRefs(remote string, prefix string) (string, error)
+
+	// PushRefs push git refs matching a directory prefix to a remote
+	// Ex: prefix="foo" will push any local refs matching "refs/foo/*" to the remote.
+	// The equivalent git refspec would be "refs/foo/*:refs/foo/*"
+	//
+	// Additionally, PushRefs will update the local references in refs/remotes/<remote>/foo to match
+	// the remote state.
+	PushRefs(remote string, prefix string) (string, error)
 
 	// StoreData will store arbitrary data and return the corresponding hash
 	StoreData(data []byte) (Hash, error)