entity: more testing and bug fixing

This commit is contained in:
Michael Muré 2021-02-05 11:18:38 +01:00
parent fe4237df3c
commit e35c7c4d17
No known key found for this signature in database
GPG Key ID: A4457C029293126F
8 changed files with 220 additions and 88 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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 {

View File

@ -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")
}

View File

@ -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)
// 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 to a remote
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.
PushRefs(remote string, prefix string) (string, error)
// StoreData will store arbitrary data and return the corresponding hash
StoreData(data []byte) (Hash, error)