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

View File

@ -3,6 +3,9 @@ package dag
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/identity"
@ -94,23 +97,28 @@ func makeTestContext() (repository.ClockedRepo, identity.Interface, identity.Int
return repo, id1, id2, def 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) repoA := repository.CreateGoGitTestRepo(false)
repoB := repository.CreateGoGitTestRepo(false) repoB := repository.CreateGoGitTestRepo(false)
remote := repository.CreateGoGitTestRepo(true) remote := repository.CreateGoGitTestRepo(true)
err := repoA.AddRemote("origin", remote.GetLocalRemote()) err := repoA.AddRemote("remote", remote.GetLocalRemote())
if err != nil { require.NoError(t, err)
panic(err) err = repoA.AddRemote("repoB", repoB.GetLocalRemote())
} require.NoError(t, err)
err = repoB.AddRemote("remote", remote.GetLocalRemote())
err = repoB.AddRemote("origin", remote.GetLocalRemote()) require.NoError(t, err)
if err != nil { err = repoB.AddRemote("repoA", repoA.GetLocalRemote())
panic(err) require.NoError(t, err)
}
id1, id2, def := makeTestContextInternal(repoA) 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 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 // Fetch retrieve updates from a remote
// This does not change the local entity state // This does not change the local entity state
func Fetch(def Definition, repo repository.Repo, remote string) (string, error) { func Fetch(def Definition, repo repository.Repo, remote string) (string, error) {
// "refs/<entity>/*:refs/remotes/<remote>/<entity>/*" return repo.FetchRefs(remote, def.namespace)
fetchRefSpec := fmt.Sprintf("refs/%s/*:refs/remotes/%s/%s/*",
def.namespace, remote, def.namespace)
return repo.FetchRefs(remote, fetchRefSpec)
} }
// Push update a remote with the local changes // Push update a remote with the local changes
func Push(def Definition, repo repository.Repo, remote string) (string, error) { func Push(def Definition, repo repository.Repo, remote string) (string, error) {
// "refs/<entity>/*:refs/<entity>/*" return repo.PushRefs(remote, def.namespace)
refspec := fmt.Sprintf("refs/%s/*:refs/%s/*",
def.namespace, def.namespace)
return repo.PushRefs(remote, refspec)
} }
// Pull will do a Fetch + MergeAll // Pull will do a Fetch + MergeAll

View File

@ -1,62 +1,58 @@
package dag package dag
import ( import (
"sort"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/repository"
) )
func allEntities(t testing.TB, bugs <-chan StreamedEntity) []*Entity { func allEntities(t testing.TB, bugs <-chan StreamedEntity) []*Entity {
t.Helper()
var result []*Entity var result []*Entity
for streamed := range bugs { for streamed := range bugs {
if streamed.Err != nil { require.NoError(t, streamed.Err)
t.Fatal(streamed.Err)
}
result = append(result, streamed.Entity) result = append(result, streamed.Entity)
} }
return result return result
} }
func TestPushPull(t *testing.T) { 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) 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 // A --> remote --> B
entity := New(def) e := New(def)
entity.Append(newOp1(id1, "foo")) e.Append(newOp1(id1, "foo"))
err = entity.Commit(repoA) err := e.Commit(repoA)
require.NoError(t, err) require.NoError(t, err)
_, err = Push(def, repoA, "origin") _, err = Push(def, repoA, "remote")
require.NoError(t, err) require.NoError(t, err)
err = Pull(def, repoB, "origin") err = Pull(def, repoB, "remote")
require.NoError(t, err) require.NoError(t, err)
entities := allEntities(t, ReadAll(def, repoB)) entities := allEntities(t, ReadAll(def, repoB))
require.Len(t, entities, 1) require.Len(t, entities, 1)
// B --> remote --> A // B --> remote --> A
entity = New(def) e = New(def)
entity.Append(newOp2(id2, "bar")) e.Append(newOp2(id2, "bar"))
err = entity.Commit(repoB) err = e.Commit(repoB)
require.NoError(t, err) require.NoError(t, err)
_, err = Push(def, repoB, "origin") _, err = Push(def, repoB, "remote")
require.NoError(t, err) require.NoError(t, err)
err = Pull(def, repoA, "origin") err = Pull(def, repoA, "remote")
require.NoError(t, err) require.NoError(t, err)
entities = allEntities(t, ReadAll(def, repoB)) entities = allEntities(t, ReadAll(def, repoB))
@ -64,39 +60,33 @@ func TestPushPull(t *testing.T) {
} }
func TestListLocalIds(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) 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 // A --> remote --> B
entity := New(def) e := New(def)
entity.Append(newOp1(id1, "foo")) e.Append(newOp1(id1, "foo"))
err = entity.Commit(repoA) err := e.Commit(repoA)
require.NoError(t, err) require.NoError(t, err)
entity = New(def) e = New(def)
entity.Append(newOp2(id2, "bar")) e.Append(newOp2(id2, "bar"))
err = entity.Commit(repoA) err = e.Commit(repoA)
require.NoError(t, err) require.NoError(t, err)
listLocalIds(t, def, repoA, 2) listLocalIds(t, def, repoA, 2)
listLocalIds(t, def, repoB, 0) listLocalIds(t, def, repoB, 0)
_, err = Push(def, repoA, "origin") _, err = Push(def, repoA, "remote")
require.NoError(t, err) require.NoError(t, err)
_, err = Fetch(def, repoB, "origin") _, err = Fetch(def, repoB, "remote")
require.NoError(t, err) require.NoError(t, err)
listLocalIds(t, def, repoA, 2) listLocalIds(t, def, repoA, 2)
listLocalIds(t, def, repoB, 0) listLocalIds(t, def, repoB, 0)
err = Pull(def, repoB, "origin") err = Pull(def, repoB, "remote")
require.NoError(t, err) require.NoError(t, err)
listLocalIds(t, def, repoA, 2) 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.NoError(t, err)
require.Len(t, ids, expectedCount) 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 // Fetch retrieve updates from a remote
// This does not change the local identities state // This does not change the local identities state
func Fetch(repo repository.Repo, remote string) (string, error) { func Fetch(repo repository.Repo, remote string) (string, error) {
// "refs/identities/*:refs/remotes/<remote>/identities/*" return repo.FetchRefs(remote, "identities")
remoteRefSpec := fmt.Sprintf(identityRemoteRefPattern, remote)
fetchRefSpec := fmt.Sprintf("%s*:%s*", identityRefPattern, remoteRefSpec)
return repo.FetchRefs(remote, fetchRefSpec)
} }
// Push update a remote with the local changes // Push update a remote with the local changes
func Push(repo repository.Repo, remote string) (string, error) { func Push(repo repository.Repo, remote string) (string, error) {
// "refs/identities/*:refs/identities/*" return repo.PushRefs(remote, "identities")
refspec := fmt.Sprintf("%s*:%s*", identityRefPattern, identityRefPattern)
return repo.PushRefs(remote, refspec)
} }
// Pull will do a Fetch + MergeAll // Pull will do a Fetch + MergeAll

View File

@ -353,13 +353,17 @@ func (repo *GoGitRepo) ClearBleveIndex(name string) error {
return nil return nil
} }
// FetchRefs fetch git refs from a remote // FetchRefs fetch git refs matching a directory prefix to a remote
func (repo *GoGitRepo) FetchRefs(remote string, refSpec string) (string, error) { // 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) buf := bytes.NewBuffer(nil)
err := repo.r.Fetch(&gogit.FetchOptions{ err := repo.r.Fetch(&gogit.FetchOptions{
RemoteName: remote, RemoteName: remote,
RefSpecs: []config.RefSpec{config.RefSpec(refSpec)}, RefSpecs: []config.RefSpec{config.RefSpec(refspec)},
Progress: buf, Progress: buf,
}) })
if err == gogit.NoErrAlreadyUpToDate { if err == gogit.NoErrAlreadyUpToDate {
@ -372,13 +376,41 @@ func (repo *GoGitRepo) FetchRefs(remote string, refSpec string) (string, error)
return buf.String(), nil return buf.String(), nil
} }
// PushRefs push git refs to a remote // PushRefs push git refs matching a directory prefix to a remote
func (repo *GoGitRepo) PushRefs(remote string, refSpec string) (string, error) { // 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) buf := bytes.NewBuffer(nil)
err := repo.r.Push(&gogit.PushOptions{ err = remo.Push(&gogit.PushOptions{
RemoteName: remote, RemoteName: remote,
RefSpecs: []config.RefSpec{config.RefSpec(refSpec)}, RefSpecs: []config.RefSpec{config.RefSpec(refspec)},
Progress: buf, Progress: buf,
}) })
if err == gogit.NoErrAlreadyUpToDate { 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") panic("implement me")
} }
// PushRefs push git refs to a remote // 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") panic("implement me")
} }

View File

@ -100,11 +100,18 @@ type Commit struct {
// RepoData give access to the git data storage // RepoData give access to the git data storage
type RepoData interface { type RepoData interface {
// FetchRefs fetch git refs from a remote // FetchRefs fetch git refs matching a directory prefix to a remote
FetchRefs(remote string, refSpec string) (string, error) // 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 push git refs matching a directory prefix to a remote
PushRefs(remote string, refSpec string) (string, error) // 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 will store arbitrary data and return the corresponding hash
StoreData(data []byte) (Hash, error) StoreData(data []byte) (Hash, error)