From 44d7587940f842a343a64d9107601591bdfb1027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 8 Nov 2020 17:53:11 +0100 Subject: [PATCH 01/42] lamport: match wikipedia algorithm --- util/lamport/clock_testing.go | 2 +- util/lamport/mem_clock.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/util/lamport/clock_testing.go b/util/lamport/clock_testing.go index fc59afb2153fa0d079f1a6f69027a1bf656fab60..4bf6d2bf8640700c08246a96a9f343fb4e41b6f2 100644 --- a/util/lamport/clock_testing.go +++ b/util/lamport/clock_testing.go @@ -11,7 +11,7 @@ func testClock(t *testing.T, c Clock) { val, err := c.Increment() assert.NoError(t, err) - assert.Equal(t, Time(1), val) + assert.Equal(t, Time(2), val) assert.Equal(t, Time(2), c.Time()) err = c.Witness(41) diff --git a/util/lamport/mem_clock.go b/util/lamport/mem_clock.go index ce6f2d4d7cbe9fa836cfc372a18c675b02e4e093..f113b5013a6f0a9149a76606347265d38f0a9595 100644 --- a/util/lamport/mem_clock.go +++ b/util/lamport/mem_clock.go @@ -62,7 +62,7 @@ func (mc *MemClock) Time() Time { // Increment is used to return the value of the lamport clock and increment it afterwards func (mc *MemClock) Increment() (Time, error) { - return Time(atomic.AddUint64(&mc.counter, 1) - 1), nil + return Time(atomic.AddUint64(&mc.counter, 1)), nil } // Witness is called to update our local clock if necessary after From fb0c5fd06184f33a03d8d4fb29a3aef8b1dafe78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 8 Nov 2020 17:54:28 +0100 Subject: [PATCH 02/42] repo: expose all lamport clocks, move clocks in their own folder --- bridge/core/auth/credential_test.go | 4 +- bug/bug_test.go | 6 +- bug/op_add_comment_test.go | 2 +- bug/op_create_test.go | 2 +- bug/op_edit_comment_test.go | 4 +- bug/op_label_change_test.go | 2 +- bug/op_noop_test.go | 2 +- bug/op_set_metadata_test.go | 4 +- bug/op_set_status_test.go | 2 +- bug/op_set_title_test.go | 2 +- bug/operation_iterator_test.go | 2 +- bug/operation_pack_test.go | 2 +- bug/operation_test.go | 2 +- go.mod | 8 --- go.sum | 27 ++++++-- identity/identity_test.go | 6 +- repository/git.go | 98 +++++++++++++++++++++++------ repository/gogit.go | 55 +++++++++++++++- repository/mock_repo.go | 70 +++++++++++++++------ repository/mock_repo_test.go | 2 +- repository/repo.go | 11 +++- repository/repo_testing.go | 13 +++- 22 files changed, 251 insertions(+), 75 deletions(-) diff --git a/bridge/core/auth/credential_test.go b/bridge/core/auth/credential_test.go index 60c631d71f56c5ab8fbd5d40564b43c08c7666ac..8bb258356f02d5ae7f97f6e1b6eec2eb6ce35866 100644 --- a/bridge/core/auth/credential_test.go +++ b/bridge/core/auth/credential_test.go @@ -11,7 +11,7 @@ import ( ) func TestCredential(t *testing.T) { - repo := repository.NewMockRepoForTest() + repo := repository.NewMockRepo() storeToken := func(val string, target string) *Token { token := NewToken(target, val) @@ -102,7 +102,7 @@ func sameIds(t *testing.T, a []Credential, b []Credential) { } func testCredentialSerial(t *testing.T, original Credential) Credential { - repo := repository.NewMockRepoForTest() + repo := repository.NewMockRepo() original.SetMetadata("test", "value") diff --git a/bug/bug_test.go b/bug/bug_test.go index 6363f4e92db5bf2da5cc6620fea38f8cace203c5..047fe386d812b58a2ebf9ccabbc69da9253ab009 100644 --- a/bug/bug_test.go +++ b/bug/bug_test.go @@ -12,7 +12,7 @@ import ( ) func TestBugId(t *testing.T) { - mockRepo := repository.NewMockRepoForTest() + mockRepo := repository.NewMockRepo() bug1 := NewBug() @@ -34,7 +34,7 @@ func TestBugId(t *testing.T) { } func TestBugValidity(t *testing.T) { - mockRepo := repository.NewMockRepoForTest() + mockRepo := repository.NewMockRepo() bug1 := NewBug() @@ -72,7 +72,7 @@ func TestBugValidity(t *testing.T) { } func TestBugCommitLoad(t *testing.T) { - repo := repository.NewMockRepoForTest() + repo := repository.NewMockRepo() bug1 := NewBug() diff --git a/bug/op_add_comment_test.go b/bug/op_add_comment_test.go index 8bcc64e19d64d3aef9747acfb58b01a1a2fe5096..3f9d02f1d49271ba431daceafd0988d7884b838f 100644 --- a/bug/op_add_comment_test.go +++ b/bug/op_add_comment_test.go @@ -13,7 +13,7 @@ import ( ) func TestAddCommentSerialize(t *testing.T) { - repo := repository.NewMockRepoForTest() + repo := repository.NewMockRepo() rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") err := rene.Commit(repo) require.NoError(t, err) diff --git a/bug/op_create_test.go b/bug/op_create_test.go index f68b7637b43838fef1b38a192abd0ec4cc5db09f..2d28a208bd61e55f403589b7602357076604bc6b 100644 --- a/bug/op_create_test.go +++ b/bug/op_create_test.go @@ -52,7 +52,7 @@ func TestCreate(t *testing.T) { } func TestCreateSerialize(t *testing.T) { - repo := repository.NewMockRepoForTest() + repo := repository.NewMockRepo() rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") err := rene.Commit(repo) require.NoError(t, err) diff --git a/bug/op_edit_comment_test.go b/bug/op_edit_comment_test.go index 583ba656e2ce25a43d2e37b35f9ad69e68492cf3..263111f96d5e61e9e503f14c1b60d72d27102eeb 100644 --- a/bug/op_edit_comment_test.go +++ b/bug/op_edit_comment_test.go @@ -15,7 +15,7 @@ import ( func TestEdit(t *testing.T) { snapshot := Snapshot{} - repo := repository.NewMockRepoForTest() + repo := repository.NewMockRepo() rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") err := rene.Commit(repo) require.NoError(t, err) @@ -79,7 +79,7 @@ func TestEdit(t *testing.T) { } func TestEditCommentSerialize(t *testing.T) { - repo := repository.NewMockRepoForTest() + repo := repository.NewMockRepo() rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") err := rene.Commit(repo) require.NoError(t, err) diff --git a/bug/op_label_change_test.go b/bug/op_label_change_test.go index c98b2207bdc984f66def1ce900b82f5efb20b463..ea73368cedcbba62e72084ca8acbbd939f3c7511 100644 --- a/bug/op_label_change_test.go +++ b/bug/op_label_change_test.go @@ -14,7 +14,7 @@ import ( ) func TestLabelChangeSerialize(t *testing.T) { - repo := repository.NewMockRepoForTest() + repo := repository.NewMockRepo() rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") err := rene.Commit(repo) require.NoError(t, err) diff --git a/bug/op_noop_test.go b/bug/op_noop_test.go index 0e34c9610bf97e023743a12368af77bead1ba860..812851ea2a10bb9dbdcc668eac2f0463f3a69f16 100644 --- a/bug/op_noop_test.go +++ b/bug/op_noop_test.go @@ -14,7 +14,7 @@ import ( ) func TestNoopSerialize(t *testing.T) { - repo := repository.NewMockRepoForTest() + repo := repository.NewMockRepo() rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") err := rene.Commit(repo) require.NoError(t, err) diff --git a/bug/op_set_metadata_test.go b/bug/op_set_metadata_test.go index d771124933c2f2f9c7f6570af5b91f4802d4121e..ba068f61e27db0f44b6d4402c93608fffe5874f1 100644 --- a/bug/op_set_metadata_test.go +++ b/bug/op_set_metadata_test.go @@ -15,7 +15,7 @@ import ( func TestSetMetadata(t *testing.T) { snapshot := Snapshot{} - repo := repository.NewMockRepoForTest() + repo := repository.NewMockRepo() rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") err := rene.Commit(repo) require.NoError(t, err) @@ -99,7 +99,7 @@ func TestSetMetadata(t *testing.T) { } func TestSetMetadataSerialize(t *testing.T) { - repo := repository.NewMockRepoForTest() + repo := repository.NewMockRepo() rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") err := rene.Commit(repo) require.NoError(t, err) diff --git a/bug/op_set_status_test.go b/bug/op_set_status_test.go index cdea2dd28ff5dc64e474f47348d10b802f5c892f..0619c9135c208d37161e4734ec4c280f54e13021 100644 --- a/bug/op_set_status_test.go +++ b/bug/op_set_status_test.go @@ -14,7 +14,7 @@ import ( ) func TestSetStatusSerialize(t *testing.T) { - repo := repository.NewMockRepoForTest() + repo := repository.NewMockRepo() rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") err := rene.Commit(repo) require.NoError(t, err) diff --git a/bug/op_set_title_test.go b/bug/op_set_title_test.go index 368ada61b8601e9567f871d23030503690e3d122..df27ee35fd055cc270713f0f76c84c8a5261edfe 100644 --- a/bug/op_set_title_test.go +++ b/bug/op_set_title_test.go @@ -14,7 +14,7 @@ import ( ) func TestSetTitleSerialize(t *testing.T) { - repo := repository.NewMockRepoForTest() + repo := repository.NewMockRepo() rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") err := rene.Commit(repo) require.NoError(t, err) diff --git a/bug/operation_iterator_test.go b/bug/operation_iterator_test.go index 5d245185bd549809ba06ed93615854437caadaf8..e066ddd8e13a50e4fed66311f3d880205e33d171 100644 --- a/bug/operation_iterator_test.go +++ b/bug/operation_iterator_test.go @@ -25,7 +25,7 @@ func ExampleOperationIterator() { } func TestOpIterator(t *testing.T) { - mockRepo := repository.NewMockRepoForTest() + mockRepo := repository.NewMockRepo() rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") err := rene.Commit(mockRepo) diff --git a/bug/operation_pack_test.go b/bug/operation_pack_test.go index 6aab00978aed1b58854fbbc72da33e76348fdc39..e1388240decd23482606cc1a262fb8747aebedc7 100644 --- a/bug/operation_pack_test.go +++ b/bug/operation_pack_test.go @@ -15,7 +15,7 @@ import ( func TestOperationPackSerialize(t *testing.T) { opp := &OperationPack{} - repo := repository.NewMockRepoForTest() + repo := repository.NewMockRepo() rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") err := rene.Commit(repo) require.NoError(t, err) diff --git a/bug/operation_test.go b/bug/operation_test.go index 20799bb1bcba55ad4a62dc96c62bffd51e809e1b..91e1d936cfbe0c0c5cfdfab509551ab6e59c7487 100644 --- a/bug/operation_test.go +++ b/bug/operation_test.go @@ -83,7 +83,7 @@ func TestID(t *testing.T) { defer repository.CleanupTestRepos(repo) repos := []repository.ClockedRepo{ - repository.NewMockRepoForTest(), + repository.NewMockRepo(), repo, } diff --git a/go.mod b/go.mod index d67bea3f0039e136e587e84b15e0cda25c51546c..69e62bcc3cfd7a60fe888b813bb4a4d5b72eb6f1 100644 --- a/go.mod +++ b/go.mod @@ -12,13 +12,7 @@ require ( github.com/blevesearch/bleve v1.0.14 github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9 github.com/corpix/uarand v0.1.1 // indirect - github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d // indirect - github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect - github.com/cznic/strutil v0.0.0-20181122101858-275e90344537 // indirect github.com/dustin/go-humanize v1.0.0 - github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c // indirect - github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect - github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect github.com/fatih/color v1.10.0 github.com/go-git/go-billy/v5 v5.0.0 github.com/go-git/go-git/v5 v5.2.0 @@ -28,11 +22,9 @@ require ( github.com/hashicorp/golang-lru v0.5.4 github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428 github.com/imdario/mergo v0.3.11 // indirect - github.com/jmhodges/levigo v1.0.0 // indirect github.com/mattn/go-isatty v0.0.12 github.com/phayes/freeport v0.0.0-20171002181615-b8543db493a5 github.com/pkg/errors v0.9.1 - github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7 github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e diff --git a/go.sum b/go.sum index 082484c1c443877eb6cc8eed7a48e4b91179a419..9927e4179f633762e3f3a79634c225f5f5ab5920 100644 --- a/go.sum +++ b/go.sum @@ -51,10 +51,13 @@ github.com/RoaringBitmap/roaring v0.4.23 h1:gpyfd12QohbqhFO4NVDUdoPOCXsyahYRQhIN github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195 h1:c4mLfegoDw6OhSJXTd2jUEQgZUQuJWtocudb97Qn9EM= github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI= @@ -62,6 +65,7 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986 h1:QvIfX96O11qjX1Zr3hKkG0dI12JBRBGABWffyZ1GI60= github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA= @@ -80,8 +84,7 @@ github.com/blevesearch/bleve v1.0.13 h1:NtqdA+2UL715y2/9Epg9Ie9uspNcilGMYNM+tT+H github.com/blevesearch/bleve v1.0.13/go.mod h1:3y+16vR4Cwtis/bOGCt7r+CHKB2/ewizEqKBUycXomA= github.com/blevesearch/bleve v1.0.14 h1:Q8r+fHTt35jtGXJUM0ULwM3Tzg+MRfyai4ZkWDy2xO4= github.com/blevesearch/bleve v1.0.14/go.mod h1:e/LJTr+E7EaoVdkQZTfoz7dt4KoDNvDbLb8MSKuNTLQ= -github.com/blevesearch/blevex v0.0.0-20190916190636-152f0fe5c040 h1:SjYVcfJVZoCfBlg+fkaq2eoZHTf5HaJfaTeTkOtyfHQ= -github.com/blevesearch/blevex v0.0.0-20190916190636-152f0fe5c040/go.mod h1:WH+MU2F4T0VmSdaPX+Wu5GYoZBrYWdOZWSjzvYcDmqQ= +github.com/blevesearch/blevex v1.0.0 h1:pnilj2Qi3YSEGdWgLj1Pn9Io7ukfXPoQcpAI1Bv8n/o= github.com/blevesearch/blevex v1.0.0/go.mod h1:2rNVqoG2BZI8t1/P1awgTKnGlx5MP9ZbtEciQaNhswc= github.com/blevesearch/cld2 v0.0.0-20200327141045-8b5f551d37f5/go.mod h1:PN0QNTLs9+j1bKy3d/GB/59wsNBFC4sWLWG3k69lWbc= github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= @@ -123,6 +126,14 @@ github.com/blevesearch/zap/v14 v14.0.5/go.mod h1:bWe8S7tRrSBTIaZ6cLRbgNH4TUDaC9L github.com/blevesearch/zap/v15 v15.0.1/go.mod h1:ho0frqAex2ktT9cYFAxQpoQXsxb/KEfdjpx4s49rf/M= github.com/blevesearch/zap/v15 v15.0.2 h1:7wV4ksnKzBibLaWBolzbxngxdVAUmF7HJ+gMOqkzsdQ= github.com/blevesearch/zap/v15 v15.0.2/go.mod h1:nfycXPgfbio8l+ZSkXUkhSNjTpp57jZ0/MKa6TigWvM= +github.com/blevesearch/zap/v11 v11.0.14 h1:IrDAvtlzDylh6H2QCmS0OGcN9Hpf6mISJlfKjcwJs7k= +github.com/blevesearch/zap/v11 v11.0.14/go.mod h1:MUEZh6VHGXv1PKx3WnCbdP404LGG2IZVa/L66pyFwnY= +github.com/blevesearch/zap/v12 v12.0.14 h1:2o9iRtl1xaRjsJ1xcqTyLX414qPAwykHNV7wNVmbp3w= +github.com/blevesearch/zap/v12 v12.0.14/go.mod h1:rOnuZOiMKPQj18AEKEHJxuI14236tTQ1ZJz4PAnWlUg= +github.com/blevesearch/zap/v13 v13.0.6 h1:r+VNSVImi9cBhTNNR+Kfl5uiGy8kIbb0JMz/h8r6+O4= +github.com/blevesearch/zap/v13 v13.0.6/go.mod h1:L89gsjdRKGyGrRN6nCpIScCvvkyxvmeDCwZRcjjPCrw= +github.com/blevesearch/zap/v14 v14.0.5 h1:NdcT+81Nvmp2zL+NhwSvGSLh7xNgGL8QRVZ67njR0NU= +github.com/blevesearch/zap/v14 v14.0.5/go.mod h1:bWe8S7tRrSBTIaZ6cLRbgNH4TUDaC9LZSpRGs85AsGY= github.com/blevesearch/zap/v15 v15.0.3 h1:Ylj8Oe+mo0P25tr9iLPp33lN6d4qcztGjaIsP51UxaY= github.com/blevesearch/zap/v15 v15.0.3/go.mod h1:iuwQrImsh1WjWJ0Ue2kBqY83a0rFtJTqfa9fp1rbVVU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -186,10 +197,12 @@ github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+ne github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2 h1:Ujru1hufTHVb++eG6OuNDKMxZnGIvF6o/u8q/8h2+I4= github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= @@ -200,10 +213,9 @@ github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= -github.com/go-git/go-billy v4.2.0+incompatible h1:Z6QtVXd5tjxUtcODLugkJg4WaZnGg13CD8qB9pr+7q0= -github.com/go-git/go-billy v4.2.0+incompatible/go.mod h1:hedUGslB3n31bx5SW9KMjV/t0CUKnrapjVG9fT7xKX4= github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM= github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M= github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= github.com/go-git/go-git/v5 v5.2.0 h1:YPBLG/3UK1we1ohRkncLjaXWLW+HKp5QNM/jTli2JgI= github.com/go-git/go-git/v5 v5.2.0/go.mod h1:kh02eMX+wdqqxgNMEyq8YgwlIOsDOa9homkUq1PoTMs= @@ -293,6 +305,7 @@ github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyN github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= @@ -324,6 +337,7 @@ github.com/ikawaha/kagome.ipadic v1.1.2/go.mod h1:DPSBbU0czaJhAb/5uKQZHMc9MTVRpD github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= @@ -339,6 +353,7 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d h1:Z+RDyXzjKE0i2sTjZ/b1uxiGtPhFy34Ou/Tk0qwN0kM= github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d/go.mod h1:JJNrCn9otv/2QP4D7SMJBgaleKpOf66PnW6F5WGNRIc= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -348,6 +363,7 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -387,6 +403,7 @@ github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -737,6 +754,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -794,6 +812,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= diff --git a/identity/identity_test.go b/identity/identity_test.go index 82e58b01a91eca8f81956a0010ca0a86959ab81b..dc5925d99bbbad5a4fb445c514bae2e0fbce64dd 100644 --- a/identity/identity_test.go +++ b/identity/identity_test.go @@ -12,7 +12,7 @@ import ( // Test the commit and load of an Identity with multiple versions func TestIdentityCommitLoad(t *testing.T) { - mockRepo := repository.NewMockRepoForTest() + mockRepo := repository.NewMockRepo() // single version @@ -193,7 +193,7 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) { // Test the immutable or mutable metadata search func TestMetadata(t *testing.T) { - mockRepo := repository.NewMockRepoForTest() + mockRepo := repository.NewMockRepo() identity := NewIdentity("René Descartes", "rene.descartes@example.com") @@ -235,7 +235,7 @@ func assertHasKeyValue(t *testing.T, metadata map[string]string, key, value stri } func TestJSON(t *testing.T) { - mockRepo := repository.NewMockRepoForTest() + mockRepo := repository.NewMockRepo() identity := &Identity{ id: entity.UnsetId, diff --git a/repository/git.go b/repository/git.go index bc9d87723691ce4e04f151d227ad59218ea29574..57c07c892e113908336aaafc861d2cf31d9e566b 100644 --- a/repository/git.go +++ b/repository/git.go @@ -4,6 +4,7 @@ package repository import ( "bytes" "fmt" + "io/ioutil" "os" "path/filepath" "strings" @@ -16,10 +17,6 @@ import ( "github.com/MichaelMure/git-bug/util/lamport" ) -const ( - clockPath = "git-bug" -) - var _ ClockedRepo = &GitRepo{} var _ TestedRepo = &GitRepo{} @@ -34,7 +31,8 @@ type GitRepo struct { indexesMutex sync.Mutex indexes map[string]bleve.Index - keyring Keyring + keyring Keyring + localStorage billy.Filesystem } // OpenGitRepo determines if the given working directory is inside of a git repository, @@ -66,6 +64,7 @@ func OpenGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) { // Fix the path to be sure we are at the root repo.path = stdout repo.gitCli.path = stdout + repo.localStorage = osfs.New(filepath.Join(path, "git-bug")) for _, loader := range clockLoaders { allExist := true @@ -88,14 +87,21 @@ func OpenGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) { // InitGitRepo create a new empty git repo at the given path func InitGitRepo(path string) (*GitRepo, error) { + k, err := defaultKeyring() + if err != nil { + return nil, err + } + repo := &GitRepo{ - gitCli: gitCli{path: path}, - path: path + "/.git", - clocks: make(map[string]lamport.Clock), - indexes: make(map[string]bleve.Index), + gitCli: gitCli{path: path}, + path: filepath.Join(path, ".git"), + clocks: make(map[string]lamport.Clock), + indexes: make(map[string]bleve.Index), + keyring: k, + localStorage: osfs.New(filepath.Join(path, ".git", "git-bug")), } - _, err := repo.runGitCommand("init", path) + _, err = repo.runGitCommand("init", path) if err != nil { return nil, err } @@ -105,14 +111,21 @@ func InitGitRepo(path string) (*GitRepo, error) { // InitBareGitRepo create a new --bare empty git repo at the given path func InitBareGitRepo(path string) (*GitRepo, error) { + k, err := defaultKeyring() + if err != nil { + return nil, err + } + repo := &GitRepo{ - gitCli: gitCli{path: path}, - path: path, - clocks: make(map[string]lamport.Clock), - indexes: make(map[string]bleve.Index), + gitCli: gitCli{path: path}, + path: path, + clocks: make(map[string]lamport.Clock), + indexes: make(map[string]bleve.Index), + keyring: k, + localStorage: osfs.New(filepath.Join(path, "git-bug")), } - _, err := repo.runGitCommand("init", "--bare", path) + _, err = repo.runGitCommand("init", "--bare", path) if err != nil { return nil, err } @@ -198,7 +211,7 @@ func (repo *GitRepo) GetRemotes() (map[string]string, error) { // LocalStorage return a billy.Filesystem giving access to $RepoPath/.git/git-bug func (repo *GitRepo) LocalStorage() billy.Filesystem { - return osfs.New(repo.path) + return repo.localStorage } // GetBleveIndex return a bleve.Index that can be used to index documents @@ -434,6 +447,37 @@ func (repo *GitRepo) GetTreeHash(commit Hash) (Hash, error) { return Hash(stdout), nil } +func (repo *GitRepo) AllClocks() (map[string]lamport.Clock, error) { + repo.clocksMutex.Lock() + defer repo.clocksMutex.Unlock() + + result := make(map[string]lamport.Clock) + + files, err := ioutil.ReadDir(filepath.Join(repo.path, "git-bug", clockPath)) + if os.IsNotExist(err) { + return nil, nil + } + if err != nil { + return nil, err + } + + for _, file := range files { + name := file.Name() + if c, ok := repo.clocks[name]; ok { + result[name] = c + } else { + c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name)) + if err != nil { + return nil, err + } + repo.clocks[name] = c + result[name] = c + } + } + + return result, nil +} + // GetOrCreateClock return a Lamport clock stored in the Repo. // If the clock doesn't exist, it's created. func (repo *GitRepo) GetOrCreateClock(name string) (lamport.Clock, error) { @@ -448,7 +492,7 @@ func (repo *GitRepo) GetOrCreateClock(name string) (lamport.Clock, error) { return nil, err } - c, err = lamport.NewPersistedClock(repo.LocalStorage(), name+"-clock") + c, err = lamport.NewPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name)) if err != nil { return nil, err } @@ -462,7 +506,7 @@ func (repo *GitRepo) getClock(name string) (lamport.Clock, error) { return c, nil } - c, err := lamport.LoadPersistedClock(repo.LocalStorage(), name+"-clock") + c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name)) if err == nil { repo.clocks[name] = c return c, nil @@ -473,6 +517,24 @@ func (repo *GitRepo) getClock(name string) (lamport.Clock, error) { return nil, err } +// Increment is equivalent to c = GetOrCreateClock(name) + c.Increment() +func (repo *GitRepo) Increment(name string) (lamport.Time, error) { + c, err := repo.GetOrCreateClock(name) + if err != nil { + return lamport.Time(0), err + } + return c.Increment() +} + +// Witness is equivalent to c = GetOrCreateClock(name) + c.Witness(time) +func (repo *GitRepo) Witness(name string, time lamport.Time) error { + c, err := repo.GetOrCreateClock(name) + if err != nil { + return err + } + return c.Witness(time) +} + // AddRemote add a new remote to the repository // Not in the interface because it's only used for testing func (repo *GitRepo) AddRemote(name string, url string) error { diff --git a/repository/gogit.go b/repository/gogit.go index bdac259de85ebda97275ee7fc402c9e9c0023782..5abdef39bbca5c58dfb96a770f00d92c7404057e 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -24,6 +24,8 @@ import ( "github.com/MichaelMure/git-bug/util/lamport" ) +const clockPath = "clocks" + var _ ClockedRepo = &GoGitRepo{} var _ TestedRepo = &GoGitRepo{} @@ -677,6 +679,37 @@ func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) { return hashes, nil } +func (repo *GoGitRepo) AllClocks() (map[string]lamport.Clock, error) { + repo.clocksMutex.Lock() + defer repo.clocksMutex.Unlock() + + result := make(map[string]lamport.Clock) + + files, err := ioutil.ReadDir(filepath.Join(repo.path, "git-bug", clockPath)) + if os.IsNotExist(err) { + return nil, nil + } + if err != nil { + return nil, err + } + + for _, file := range files { + name := file.Name() + if c, ok := repo.clocks[name]; ok { + result[name] = c + } else { + c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name)) + if err != nil { + return nil, err + } + repo.clocks[name] = c + result[name] = c + } + } + + return result, nil +} + // GetOrCreateClock return a Lamport clock stored in the Repo. // If the clock doesn't exist, it's created. func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) { @@ -691,7 +724,7 @@ func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) { return nil, err } - c, err = lamport.NewPersistedClock(repo.localStorage, name+"-clock") + c, err = lamport.NewPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name)) if err != nil { return nil, err } @@ -705,7 +738,7 @@ func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) { return c, nil } - c, err := lamport.LoadPersistedClock(repo.localStorage, name+"-clock") + c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name)) if err == nil { repo.clocks[name] = c return c, nil @@ -716,6 +749,24 @@ func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) { return nil, err } +// Increment is equivalent to c = GetOrCreateClock(name) + c.Increment() +func (repo *GoGitRepo) Increment(name string) (lamport.Time, error) { + c, err := repo.GetOrCreateClock(name) + if err != nil { + return lamport.Time(0), err + } + return c.Increment() +} + +// Witness is equivalent to c = GetOrCreateClock(name) + c.Witness(time) +func (repo *GoGitRepo) Witness(name string, time lamport.Time) error { + c, err := repo.GetOrCreateClock(name) + if err != nil { + return err + } + return c.Witness(time) +} + // AddRemote add a new remote to the repository // Not in the interface because it's only used for testing func (repo *GoGitRepo) AddRemote(name string, url string) error { diff --git a/repository/mock_repo.go b/repository/mock_repo.go index 8a1724ef1c9c546a380d3a90fcc78fac39bff876..974c3fb2aa47cffb6ba133ca8749f6498e263659 100644 --- a/repository/mock_repo.go +++ b/repository/mock_repo.go @@ -14,11 +14,11 @@ import ( "github.com/MichaelMure/git-bug/util/lamport" ) -var _ ClockedRepo = &mockRepoForTest{} -var _ TestedRepo = &mockRepoForTest{} +var _ ClockedRepo = &mockRepo{} +var _ TestedRepo = &mockRepo{} -// mockRepoForTest defines an instance of Repo that can be used for testing. -type mockRepoForTest struct { +// mockRepo defines an instance of Repo that can be used for testing. +type mockRepo struct { *mockRepoConfig *mockRepoKeyring *mockRepoCommon @@ -26,12 +26,13 @@ type mockRepoForTest struct { *mockRepoBleve *mockRepoData *mockRepoClock + *mockRepoTest } -func (m *mockRepoForTest) Close() error { return nil } +func (m *mockRepo) Close() error { return nil } -func NewMockRepoForTest() *mockRepoForTest { - return &mockRepoForTest{ +func NewMockRepo() *mockRepo { + return &mockRepo{ mockRepoConfig: NewMockRepoConfig(), mockRepoKeyring: NewMockRepoKeyring(), mockRepoCommon: NewMockRepoCommon(), @@ -39,6 +40,7 @@ func NewMockRepoForTest() *mockRepoForTest { mockRepoBleve: newMockRepoBleve(), mockRepoData: NewMockRepoData(), mockRepoClock: NewMockRepoClock(), + mockRepoTest: NewMockRepoTest(), } } @@ -371,18 +373,7 @@ func (r *mockRepoData) GetTreeHash(commit Hash) (Hash, error) { return c.treeHash, nil } -func (r *mockRepoData) AddRemote(name string, url string) error { - panic("implement me") -} - -func (m mockRepoForTest) GetLocalRemote() string { - panic("implement me") -} - -func (m mockRepoForTest) EraseFromDisk() error { - // nothing to do - return nil -} +var _ RepoClock = &mockRepoClock{} type mockRepoClock struct { mu sync.Mutex @@ -395,6 +386,10 @@ func NewMockRepoClock() *mockRepoClock { } } +func (r *mockRepoClock) AllClocks() (map[string]lamport.Clock, error) { + return r.clocks, nil +} + func (r *mockRepoClock) GetOrCreateClock(name string) (lamport.Clock, error) { r.mu.Lock() defer r.mu.Unlock() @@ -407,3 +402,40 @@ func (r *mockRepoClock) GetOrCreateClock(name string) (lamport.Clock, error) { r.clocks[name] = c return c, nil } + +func (r *mockRepoClock) Increment(name string) (lamport.Time, error) { + c, err := r.GetOrCreateClock(name) + if err != nil { + return lamport.Time(0), err + } + return c.Increment() +} + +func (r *mockRepoClock) Witness(name string, time lamport.Time) error { + c, err := r.GetOrCreateClock(name) + if err != nil { + return err + } + return c.Witness(time) +} + +var _ repoTest = &mockRepoTest{} + +type mockRepoTest struct{} + +func NewMockRepoTest() *mockRepoTest { + return &mockRepoTest{} +} + +func (r *mockRepoTest) AddRemote(name string, url string) error { + panic("implement me") +} + +func (r mockRepoTest) GetLocalRemote() string { + panic("implement me") +} + +func (r mockRepoTest) EraseFromDisk() error { + // nothing to do + return nil +} diff --git a/repository/mock_repo_test.go b/repository/mock_repo_test.go index b56b94f26be5938e4e0740be1aeb5f8a18249e8c..dec09380d5ff5b26e9ba0e7c0c17deeeb860da1b 100644 --- a/repository/mock_repo_test.go +++ b/repository/mock_repo_test.go @@ -3,7 +3,7 @@ package repository import "testing" func TestMockRepo(t *testing.T) { - creator := func(bare bool) TestedRepo { return NewMockRepoForTest() } + creator := func(bare bool) TestedRepo { return NewMockRepo() } cleaner := func(repos ...Repo) {} RepoTest(t, creator, cleaner) diff --git a/repository/repo.go b/repository/repo.go index eb9296d4829704332960b586a0f54612fd992a3f..625e01439659738fbdf6d04227da8529afdebefa 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -22,9 +22,9 @@ type Repo interface { RepoConfig RepoKeyring RepoCommon - RepoData RepoStorage RepoBleve + RepoData Close() error } @@ -142,9 +142,18 @@ type RepoData interface { // RepoClock give access to Lamport clocks type RepoClock interface { + // AllClocks return all the known clocks + AllClocks() (map[string]lamport.Clock, error) + // GetOrCreateClock return a Lamport clock stored in the Repo. // If the clock doesn't exist, it's created. GetOrCreateClock(name string) (lamport.Clock, error) + + // Increment is equivalent to c = GetOrCreateClock(name) + c.Increment() + Increment(name string) (lamport.Time, error) + + // Witness is equivalent to c = GetOrCreateClock(name) + c.Witness(time) + Witness(name string, time lamport.Time) error } // ClockLoader hold which logical clock need to exist for an entity and diff --git a/repository/repo_testing.go b/repository/repo_testing.go index c0e1fa7979c239153536df28a67a5d216378fa83..2c8705d62066a0ed9b1b114a4fb47374291b54d4 100644 --- a/repository/repo_testing.go +++ b/repository/repo_testing.go @@ -191,13 +191,17 @@ func RepoDataTest(t *testing.T, repo RepoData) { // helper to test a RepoClock func RepoClockTest(t *testing.T, repo RepoClock) { + allClocks, err := repo.AllClocks() + require.NoError(t, err) + require.Len(t, allClocks, 0) + clock, err := repo.GetOrCreateClock("foo") require.NoError(t, err) require.Equal(t, lamport.Time(1), clock.Time()) time, err := clock.Increment() require.NoError(t, err) - require.Equal(t, lamport.Time(1), time) + require.Equal(t, lamport.Time(2), time) require.Equal(t, lamport.Time(2), clock.Time()) clock2, err := repo.GetOrCreateClock("foo") @@ -207,6 +211,13 @@ func RepoClockTest(t *testing.T, repo RepoClock) { clock3, err := repo.GetOrCreateClock("bar") require.NoError(t, err) require.Equal(t, lamport.Time(1), clock3.Time()) + + allClocks, err = repo.AllClocks() + require.NoError(t, err) + require.Equal(t, map[string]lamport.Clock{ + "foo": clock, + "bar": clock3, + }, allClocks) } func randomData() []byte { From 5ae8a132772385c903a62de2ceec02a97f108a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 8 Nov 2020 19:13:55 +0100 Subject: [PATCH 03/42] identity: Id from data, not git + hold multiple lamport clocks --- go.sum | 18 +- identity/identity.go | 270 ++++++++++++++---------------- identity/identity_actions_test.go | 36 ++-- identity/identity_stub.go | 25 +-- identity/identity_test.go | 205 ++++++++++------------- identity/interface.go | 24 +-- identity/version.go | 169 ++++++++++++------- identity/version_test.go | 71 ++++++-- 8 files changed, 432 insertions(+), 386 deletions(-) diff --git a/go.sum b/go.sum index 9927e4179f633762e3f3a79634c225f5f5ab5920..c6875b9a514d286b2865d81cba4c5a6d49b88a54 100644 --- a/go.sum +++ b/go.sum @@ -101,6 +101,8 @@ github.com/blevesearch/zap/v11 v11.0.12/go.mod h1:JLfFhc8DWP01zMG/6VwEY2eAnlJsTN github.com/blevesearch/zap/v11 v11.0.13 h1:NDvmjAyeEQsBbPElubVPqrBtSDOftXYwxkHeZfflU4A= github.com/blevesearch/zap/v11 v11.0.13/go.mod h1:qKkNigeXbxZwym02wsxoQpbme1DgAwTvRlT/beIGfTM= github.com/blevesearch/zap/v11 v11.0.14 h1:IrDAvtlzDylh6H2QCmS0OGcN9Hpf6mISJlfKjcwJs7k= +github.com/blevesearch/zap/v11 v11.0.14 h1:IrDAvtlzDylh6H2QCmS0OGcN9Hpf6mISJlfKjcwJs7k= +github.com/blevesearch/zap/v11 v11.0.14/go.mod h1:MUEZh6VHGXv1PKx3WnCbdP404LGG2IZVa/L66pyFwnY= github.com/blevesearch/zap/v11 v11.0.14/go.mod h1:MUEZh6VHGXv1PKx3WnCbdP404LGG2IZVa/L66pyFwnY= github.com/blevesearch/zap/v12 v12.0.10 h1:T1/GXNBxC9eetfuMwCM5RLWXeharSMyAdNEdXVtBuHA= github.com/blevesearch/zap/v12 v12.0.10/go.mod h1:QtKkjpmV/sVFEnKSaIWPXZJAaekL97TrTV3ImhNx+nw= @@ -108,6 +110,8 @@ github.com/blevesearch/zap/v12 v12.0.12/go.mod h1:1HrB4hhPfI8u8x4SPYbluhb8xhflpP github.com/blevesearch/zap/v12 v12.0.13 h1:05Ebdmv2tRTUytypG4DlOIHLLw995DtVV0Zl3YwwDew= github.com/blevesearch/zap/v12 v12.0.13/go.mod h1:0RTeU1uiLqsPoybUn6G/Zgy6ntyFySL3uWg89NgX3WU= github.com/blevesearch/zap/v12 v12.0.14 h1:2o9iRtl1xaRjsJ1xcqTyLX414qPAwykHNV7wNVmbp3w= +github.com/blevesearch/zap/v12 v12.0.14 h1:2o9iRtl1xaRjsJ1xcqTyLX414qPAwykHNV7wNVmbp3w= +github.com/blevesearch/zap/v12 v12.0.14/go.mod h1:rOnuZOiMKPQj18AEKEHJxuI14236tTQ1ZJz4PAnWlUg= github.com/blevesearch/zap/v12 v12.0.14/go.mod h1:rOnuZOiMKPQj18AEKEHJxuI14236tTQ1ZJz4PAnWlUg= github.com/blevesearch/zap/v13 v13.0.2 h1:quhI5OVFX33dhPpUW+nLyXGpu7QT8qTgzu6qA/fRRXM= github.com/blevesearch/zap/v13 v13.0.2/go.mod h1:/9QLKla8/8mloJvQQutPhB+tw6y35urvKeAFeun2JGA= @@ -115,6 +119,8 @@ github.com/blevesearch/zap/v13 v13.0.4/go.mod h1:YdB7UuG7TBWu/1dz9e2SaLp1RKfFfdJ github.com/blevesearch/zap/v13 v13.0.5 h1:+Gcwl95uei3MgBlJAddBFRv9gl+FMNcXpMa7BX3byJw= github.com/blevesearch/zap/v13 v13.0.5/go.mod h1:HTfWECmzBN7BbdBxdEigpUsD6MOPFOO84tZ0z/g3CnE= github.com/blevesearch/zap/v13 v13.0.6 h1:r+VNSVImi9cBhTNNR+Kfl5uiGy8kIbb0JMz/h8r6+O4= +github.com/blevesearch/zap/v13 v13.0.6 h1:r+VNSVImi9cBhTNNR+Kfl5uiGy8kIbb0JMz/h8r6+O4= +github.com/blevesearch/zap/v13 v13.0.6/go.mod h1:L89gsjdRKGyGrRN6nCpIScCvvkyxvmeDCwZRcjjPCrw= github.com/blevesearch/zap/v13 v13.0.6/go.mod h1:L89gsjdRKGyGrRN6nCpIScCvvkyxvmeDCwZRcjjPCrw= github.com/blevesearch/zap/v14 v14.0.1 h1:s8KeqX53Vc4eRaziHsnY2bYUE+8IktWqRL9W5H5VDMY= github.com/blevesearch/zap/v14 v14.0.1/go.mod h1:Y+tUL9TypMca5+96m7iJb2lpcntETXSeDoI5BBX2tvY= @@ -122,18 +128,12 @@ github.com/blevesearch/zap/v14 v14.0.3/go.mod h1:oObAhcDHw7p1ahiTCqhRkdxdl7UA8qp github.com/blevesearch/zap/v14 v14.0.4 h1:BnWWkdgmPhK50J9dkBlQrWB4UDa22OMPIUzn1oXcXfY= github.com/blevesearch/zap/v14 v14.0.4/go.mod h1:sTwuFoe1n/+VtaHNAjY3W5GzHZ5UxFkw1MZ82P/WKpA= github.com/blevesearch/zap/v14 v14.0.5 h1:NdcT+81Nvmp2zL+NhwSvGSLh7xNgGL8QRVZ67njR0NU= +github.com/blevesearch/zap/v14 v14.0.5 h1:NdcT+81Nvmp2zL+NhwSvGSLh7xNgGL8QRVZ67njR0NU= +github.com/blevesearch/zap/v14 v14.0.5/go.mod h1:bWe8S7tRrSBTIaZ6cLRbgNH4TUDaC9LZSpRGs85AsGY= github.com/blevesearch/zap/v14 v14.0.5/go.mod h1:bWe8S7tRrSBTIaZ6cLRbgNH4TUDaC9LZSpRGs85AsGY= github.com/blevesearch/zap/v15 v15.0.1/go.mod h1:ho0frqAex2ktT9cYFAxQpoQXsxb/KEfdjpx4s49rf/M= github.com/blevesearch/zap/v15 v15.0.2 h1:7wV4ksnKzBibLaWBolzbxngxdVAUmF7HJ+gMOqkzsdQ= github.com/blevesearch/zap/v15 v15.0.2/go.mod h1:nfycXPgfbio8l+ZSkXUkhSNjTpp57jZ0/MKa6TigWvM= -github.com/blevesearch/zap/v11 v11.0.14 h1:IrDAvtlzDylh6H2QCmS0OGcN9Hpf6mISJlfKjcwJs7k= -github.com/blevesearch/zap/v11 v11.0.14/go.mod h1:MUEZh6VHGXv1PKx3WnCbdP404LGG2IZVa/L66pyFwnY= -github.com/blevesearch/zap/v12 v12.0.14 h1:2o9iRtl1xaRjsJ1xcqTyLX414qPAwykHNV7wNVmbp3w= -github.com/blevesearch/zap/v12 v12.0.14/go.mod h1:rOnuZOiMKPQj18AEKEHJxuI14236tTQ1ZJz4PAnWlUg= -github.com/blevesearch/zap/v13 v13.0.6 h1:r+VNSVImi9cBhTNNR+Kfl5uiGy8kIbb0JMz/h8r6+O4= -github.com/blevesearch/zap/v13 v13.0.6/go.mod h1:L89gsjdRKGyGrRN6nCpIScCvvkyxvmeDCwZRcjjPCrw= -github.com/blevesearch/zap/v14 v14.0.5 h1:NdcT+81Nvmp2zL+NhwSvGSLh7xNgGL8QRVZ67njR0NU= -github.com/blevesearch/zap/v14 v14.0.5/go.mod h1:bWe8S7tRrSBTIaZ6cLRbgNH4TUDaC9LZSpRGs85AsGY= github.com/blevesearch/zap/v15 v15.0.3 h1:Ylj8Oe+mo0P25tr9iLPp33lN6d4qcztGjaIsP51UxaY= github.com/blevesearch/zap/v15 v15.0.3/go.mod h1:iuwQrImsh1WjWJ0Ue2kBqY83a0rFtJTqfa9fp1rbVVU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -492,6 +492,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= @@ -674,6 +675,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/identity/identity.go b/identity/identity.go index 8182e26350b3d5e087d66b6e94ed3545ad681cb5..6352212dbc6539c3b45e626a54d12652384795a3 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -6,7 +6,6 @@ import ( "fmt" "reflect" "strings" - "time" "github.com/pkg/errors" @@ -35,47 +34,27 @@ var _ Interface = &Identity{} var _ entity.Interface = &Identity{} type Identity struct { - // Id used as unique identifier - id entity.Id - // all the successive version of the identity - versions []*Version - - // not serialized - lastCommit repository.Hash + versions []*version } -func NewIdentity(name string, email string) *Identity { - return &Identity{ - id: entity.UnsetId, - versions: []*Version{ - { - name: name, - email: email, - nonce: makeNonce(20), - }, - }, - } +func NewIdentity(repo repository.RepoClock, name string, email string) (*Identity, error) { + return NewIdentityFull(repo, name, email, "", "", nil) } -func NewIdentityFull(name string, email string, login string, avatarUrl string) *Identity { - return &Identity{ - id: entity.UnsetId, - versions: []*Version{ - { - name: name, - email: email, - login: login, - avatarURL: avatarUrl, - nonce: makeNonce(20), - }, - }, +func NewIdentityFull(repo repository.RepoClock, name string, email string, login string, avatarUrl string, keys []*Key) (*Identity, error) { + v, err := newVersion(repo, name, email, login, avatarUrl, keys) + if err != nil { + return nil, err } + return &Identity{ + versions: []*version{v}, + }, nil } // NewFromGitUser will query the repository for user detail and // build the corresponding Identity -func NewFromGitUser(repo repository.Repo) (*Identity, error) { +func NewFromGitUser(repo repository.ClockedRepo) (*Identity, error) { name, err := repo.GetUserName() if err != nil { return nil, err @@ -92,13 +71,13 @@ func NewFromGitUser(repo repository.Repo) (*Identity, error) { return nil, errors.New("user name is not configured in git yet. Please use `git config --global user.email johndoe@example.com`") } - return NewIdentity(name, email), nil + return NewIdentity(repo, name, email) } // MarshalJSON will only serialize the id func (i *Identity) MarshalJSON() ([]byte, error) { return json.Marshal(&IdentityStub{ - id: i.id, + id: i.Id(), }) } @@ -131,28 +110,25 @@ func read(repo repository.Repo, ref string) (*Identity, error) { } hashes, err := repo.ListCommits(ref) - - // TODO: this is not perfect, it might be a command invoke error if err != nil { return nil, ErrIdentityNotExist } - - i := &Identity{ - id: id, + if len(hashes) == 0 { + return nil, fmt.Errorf("empty identity") } + i := &Identity{} + for _, hash := range hashes { entries, err := repo.ReadTree(hash) if err != nil { return nil, errors.Wrap(err, "can't list git tree entries") } - if len(entries) != 1 { return nil, fmt.Errorf("invalid identity data at hash %s", hash) } entry := entries[0] - if entry.Name != versionEntryName { return nil, fmt.Errorf("invalid identity data at hash %s", hash) } @@ -162,20 +138,22 @@ func read(repo repository.Repo, ref string) (*Identity, error) { return nil, errors.Wrap(err, "failed to read git blob data") } - var version Version + var version version err = json.Unmarshal(data, &version) - if err != nil { return nil, errors.Wrapf(err, "failed to decode Identity version json %s", hash) } // tag the version with the commit hash version.commitHash = hash - i.lastCommit = hash i.versions = append(i.versions, &version) } + if id != i.versions[0].Id() { + return nil, fmt.Errorf("identity ID doesn't math the first version ID") + } + return i, nil } @@ -292,32 +270,49 @@ type Mutator struct { } // Mutate allow to create a new version of the Identity in one go -func (i *Identity) Mutate(f func(orig Mutator) Mutator) { +func (i *Identity) Mutate(repo repository.RepoClock, f func(orig *Mutator)) error { + copyKeys := func(keys []*Key) []*Key { + result := make([]*Key, len(keys)) + for i, key := range keys { + result[i] = key.Clone() + } + return result + } + orig := Mutator{ Name: i.Name(), Email: i.Email(), Login: i.Login(), AvatarUrl: i.AvatarUrl(), - Keys: i.Keys(), + Keys: copyKeys(i.Keys()), } - mutated := f(orig) + mutated := orig + mutated.Keys = copyKeys(orig.Keys) + + f(&mutated) + if reflect.DeepEqual(orig, mutated) { - return - } - i.versions = append(i.versions, &Version{ - name: mutated.Name, - email: mutated.Email, - login: mutated.Login, - avatarURL: mutated.AvatarUrl, - keys: mutated.Keys, - }) + return nil + } + + v, err := newVersion(repo, + mutated.Name, + mutated.Email, + mutated.Login, + mutated.AvatarUrl, + mutated.Keys, + ) + if err != nil { + return err + } + + i.versions = append(i.versions, v) + return nil } // Write the identity into the Repository. In particular, this ensure that // the Id is properly set. func (i *Identity) Commit(repo repository.ClockedRepo) error { - // Todo: check for mismatch between memory and commit data - if !i.NeedCommit() { return fmt.Errorf("can't commit an identity with no pending version") } @@ -326,24 +321,14 @@ func (i *Identity) Commit(repo repository.ClockedRepo) error { return errors.Wrap(err, "can't commit an identity with invalid data") } + var lastCommit repository.Hash for _, v := range i.versions { if v.commitHash != "" { - i.lastCommit = v.commitHash + lastCommit = v.commitHash // ignore already commit versions continue } - // get the times where new versions starts to be valid - // TODO: instead of this hardcoded clock for bugs only, this need to be - // a vector of edit clock, one for each entity (bug, PR, config ..) - bugEditClock, err := repo.GetOrCreateClock("bug-edit") - if err != nil { - return err - } - - v.time = bugEditClock.Time() - v.unixTime = time.Now().Unix() - blobHash, err := v.Write(repo) if err != nil { return err @@ -360,37 +345,21 @@ func (i *Identity) Commit(repo repository.ClockedRepo) error { } var commitHash repository.Hash - if i.lastCommit != "" { - commitHash, err = repo.StoreCommitWithParent(treeHash, i.lastCommit) + if lastCommit != "" { + commitHash, err = repo.StoreCommitWithParent(treeHash, lastCommit) } else { commitHash, err = repo.StoreCommit(treeHash) } - if err != nil { return err } - i.lastCommit = commitHash + lastCommit = commitHash v.commitHash = commitHash - - // if it was the first commit, use the commit hash as the Identity id - if i.id == "" || i.id == entity.UnsetId { - i.id = entity.Id(commitHash) - } - } - - if i.id == "" { - panic("identity with no id") - } - - ref := fmt.Sprintf("%s%s", identityRefPattern, i.id) - err := repo.UpdateRef(ref, i.lastCommit) - - if err != nil { - return err } - return nil + ref := fmt.Sprintf("%s%s", identityRefPattern, i.Id().String()) + return repo.UpdateRef(ref, lastCommit) } func (i *Identity) CommitAsNeeded(repo repository.ClockedRepo) error { @@ -433,20 +402,17 @@ func (i *Identity) NeedCommit() bool { // confident enough to implement that. I choose the strict fast-forward only approach, // despite it's potential problem with two different version as mentioned above. func (i *Identity) Merge(repo repository.Repo, other *Identity) (bool, error) { - if i.id != other.id { + if i.Id() != other.Id() { return false, errors.New("merging unrelated identities is not supported") } - if i.lastCommit == "" || other.lastCommit == "" { - return false, errors.New("can't merge identities that has never been stored") - } - modified := false + var lastCommit repository.Hash for j, otherVersion := range other.versions { // if there is more version in other, take them if len(i.versions) == j { i.versions = append(i.versions, otherVersion) - i.lastCommit = otherVersion.commitHash + lastCommit = otherVersion.commitHash modified = true } @@ -458,7 +424,7 @@ func (i *Identity) Merge(repo repository.Repo, other *Identity) (bool, error) { } if modified { - err := repo.UpdateRef(identityRefPattern+i.id.String(), i.lastCommit) + err := repo.UpdateRef(identityRefPattern+i.Id().String(), lastCommit) if err != nil { return false, err } @@ -469,7 +435,7 @@ func (i *Identity) Merge(repo repository.Repo, other *Identity) (bool, error) { // Validate check if the Identity data is valid func (i *Identity) Validate() error { - lastTime := lamport.Time(0) + lastTimes := make(map[string]lamport.Time) if len(i.versions) == 0 { return fmt.Errorf("no version") @@ -480,22 +446,27 @@ func (i *Identity) Validate() error { return err } - if v.commitHash != "" && v.time < lastTime { - return fmt.Errorf("non-chronological version (%d --> %d)", lastTime, v.time) + // check for always increasing lamport time + // check that a new version didn't drop a clock + for name, previous := range lastTimes { + if now, ok := v.times[name]; ok { + if now < previous { + return fmt.Errorf("non-chronological lamport clock %s (%d --> %d)", name, previous, now) + } + } else { + return fmt.Errorf("version has less lamport clocks than before (missing %s)", name) + } } - lastTime = v.time - } - - // The identity Id should be the hash of the first commit - if i.versions[0].commitHash != "" && string(i.versions[0].commitHash) != i.id.String() { - return fmt.Errorf("identity id should be the first commit hash") + for name, now := range v.times { + lastTimes[name] = now + } } return nil } -func (i *Identity) lastVersion() *Version { +func (i *Identity) lastVersion() *version { if len(i.versions) <= 0 { panic("no version at all") } @@ -505,12 +476,8 @@ func (i *Identity) lastVersion() *Version { // Id return the Identity identifier func (i *Identity) Id() entity.Id { - if i.id == "" || i.id == entity.UnsetId { - // simply panic as it would be a coding error - // (using an id of an identity not stored yet) - panic("no id yet") - } - return i.id + // id is the id of the first version + return i.versions[0].Id() } // Name return the last version of the name @@ -518,6 +485,21 @@ func (i *Identity) Name() string { return i.lastVersion().name } +// DisplayName return a non-empty string to display, representing the +// identity, based on the non-empty values. +func (i *Identity) DisplayName() string { + switch { + case i.Name() == "" && i.Login() != "": + return i.Login() + case i.Name() != "" && i.Login() == "": + return i.Name() + case i.Name() != "" && i.Login() != "": + return fmt.Sprintf("%s (%s)", i.Name(), i.Login()) + } + + panic("invalid person data") +} + // Email return the last version of the email func (i *Identity) Email() string { return i.lastVersion().email @@ -539,11 +521,18 @@ func (i *Identity) Keys() []*Key { } // ValidKeysAtTime return the set of keys valid at a given lamport time -func (i *Identity) ValidKeysAtTime(time lamport.Time) []*Key { +func (i *Identity) ValidKeysAtTime(clockName string, time lamport.Time) []*Key { var result []*Key + var lastTime lamport.Time for _, v := range i.versions { - if v.time > time { + refTime, ok := v.times[clockName] + if !ok { + refTime = lastTime + } + lastTime = refTime + + if refTime > time { return result } @@ -553,19 +542,14 @@ func (i *Identity) ValidKeysAtTime(time lamport.Time) []*Key { return result } -// DisplayName return a non-empty string to display, representing the -// identity, based on the non-empty values. -func (i *Identity) DisplayName() string { - switch { - case i.Name() == "" && i.Login() != "": - return i.Login() - case i.Name() != "" && i.Login() == "": - return i.Name() - case i.Name() != "" && i.Login() != "": - return fmt.Sprintf("%s (%s)", i.Name(), i.Login()) - } +// LastModification return the timestamp at which the last version of the identity became valid. +func (i *Identity) LastModification() timestamp.Timestamp { + return timestamp.Timestamp(i.lastVersion().unixTime) +} - panic("invalid person data") +// LastModificationLamports return the lamport times at which the last version of the identity became valid. +func (i *Identity) LastModificationLamports() map[string]lamport.Time { + return i.lastVersion().times } // IsProtected return true if the chain of git commits started to be signed. @@ -575,27 +559,23 @@ func (i *Identity) IsProtected() bool { return false } -// LastModificationLamportTime return the Lamport time at which the last version of the identity became valid. -func (i *Identity) LastModificationLamport() lamport.Time { - return i.lastVersion().time -} - -// LastModification return the timestamp at which the last version of the identity became valid. -func (i *Identity) LastModification() timestamp.Timestamp { - return timestamp.Timestamp(i.lastVersion().unixTime) -} - -// SetMetadata store arbitrary metadata along the last not-commit Version. -// If the Version has been commit to git already, a new identical version is added and will need to be +// SetMetadata store arbitrary metadata along the last not-commit version. +// If the version has been commit to git already, a new identical version is added and will need to be // commit. func (i *Identity) SetMetadata(key string, value string) { + // once commit, data is immutable so we create a new version if i.lastVersion().commitHash != "" { i.versions = append(i.versions, i.lastVersion().Clone()) } + // if Id() has been called, we can't change the first version anymore, so we create a new version + if len(i.versions) == 1 && i.versions[0].id != entity.UnsetId && i.versions[0].id != "" { + i.versions = append(i.versions, i.lastVersion().Clone()) + } + i.lastVersion().SetMetadata(key, value) } -// ImmutableMetadata return all metadata for this Identity, accumulated from each Version. +// ImmutableMetadata return all metadata for this Identity, accumulated from each version. // If multiple value are found, the first defined takes precedence. func (i *Identity) ImmutableMetadata() map[string]string { metadata := make(map[string]string) @@ -611,7 +591,7 @@ func (i *Identity) ImmutableMetadata() map[string]string { return metadata } -// MutableMetadata return all metadata for this Identity, accumulated from each Version. +// MutableMetadata return all metadata for this Identity, accumulated from each version. // If multiple value are found, the last defined takes precedence. func (i *Identity) MutableMetadata() map[string]string { metadata := make(map[string]string) @@ -624,9 +604,3 @@ func (i *Identity) MutableMetadata() map[string]string { return metadata } - -// addVersionForTest add a new version to the identity -// Only for testing ! -func (i *Identity) addVersionForTest(version *Version) { - i.versions = append(i.versions, version) -} diff --git a/identity/identity_actions_test.go b/identity/identity_actions_test.go index 773574c6b324f8cde21d04dd4c1dadbec06dc6a4..63f6aacd7347f68fa8ea4547359af94d20e8b66b 100644 --- a/identity/identity_actions_test.go +++ b/identity/identity_actions_test.go @@ -12,8 +12,9 @@ func TestPushPull(t *testing.T) { repoA, repoB, remote := repository.SetupReposAndRemote() defer repository.CleanupTestRepos(repoA, repoB, remote) - identity1 := NewIdentity("name1", "email1") - err := identity1.Commit(repoA) + identity1, err := NewIdentity(repoA, "name1", "email1") + require.NoError(t, err) + err = identity1.Commit(repoA) require.NoError(t, err) // A --> remote --> B @@ -30,7 +31,8 @@ func TestPushPull(t *testing.T) { } // B --> remote --> A - identity2 := NewIdentity("name2", "email2") + identity2, err := NewIdentity(repoB, "name2", "email2") + require.NoError(t, err) err = identity2.Commit(repoB) require.NoError(t, err) @@ -48,17 +50,19 @@ func TestPushPull(t *testing.T) { // Update both - identity1.addVersionForTest(&Version{ - name: "name1b", - email: "email1b", + err = identity1.Mutate(repoA, func(orig *Mutator) { + orig.Name = "name1b" + orig.Email = "email1b" }) + require.NoError(t, err) err = identity1.Commit(repoA) require.NoError(t, err) - identity2.addVersionForTest(&Version{ - name: "name2b", - email: "email2b", + err = identity2.Mutate(repoB, func(orig *Mutator) { + orig.Name = "name2b" + orig.Email = "email2b" }) + require.NoError(t, err) err = identity2.Commit(repoB) require.NoError(t, err) @@ -92,20 +96,22 @@ func TestPushPull(t *testing.T) { // Concurrent update - identity1.addVersionForTest(&Version{ - name: "name1c", - email: "email1c", + err = identity1.Mutate(repoA, func(orig *Mutator) { + orig.Name = "name1c" + orig.Email = "email1c" }) + require.NoError(t, err) err = identity1.Commit(repoA) require.NoError(t, err) identity1B, err := ReadLocal(repoB, identity1.Id()) require.NoError(t, err) - identity1B.addVersionForTest(&Version{ - name: "name1concurrent", - email: "email1concurrent", + err = identity1B.Mutate(repoB, func(orig *Mutator) { + orig.Name = "name1concurrent" + orig.Email = "name1concurrent" }) + require.NoError(t, err) err = identity1B.Commit(repoB) require.NoError(t, err) diff --git a/identity/identity_stub.go b/identity/identity_stub.go index f4bf1d37c63ee4f2cf171e12b69c1079abf0779d..fec9201086d76e83069373d7fc66531a70fcc64c 100644 --- a/identity/identity_stub.go +++ b/identity/identity_stub.go @@ -4,7 +4,6 @@ import ( "encoding/json" "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/lamport" "github.com/MichaelMure/git-bug/util/timestamp" ) @@ -52,6 +51,10 @@ func (IdentityStub) Name() string { panic("identities needs to be properly loaded with identity.ReadLocal()") } +func (IdentityStub) DisplayName() string { + panic("identities needs to be properly loaded with identity.ReadLocal()") +} + func (IdentityStub) Email() string { panic("identities needs to be properly loaded with identity.ReadLocal()") } @@ -68,23 +71,15 @@ func (IdentityStub) Keys() []*Key { panic("identities needs to be properly loaded with identity.ReadLocal()") } -func (IdentityStub) ValidKeysAtTime(_ lamport.Time) []*Key { - panic("identities needs to be properly loaded with identity.ReadLocal()") -} - -func (IdentityStub) DisplayName() string { - panic("identities needs to be properly loaded with identity.ReadLocal()") -} - -func (IdentityStub) Validate() error { +func (IdentityStub) ValidKeysAtTime(_ string, _ lamport.Time) []*Key { panic("identities needs to be properly loaded with identity.ReadLocal()") } -func (IdentityStub) CommitWithRepo(repo repository.ClockedRepo) error { +func (i *IdentityStub) LastModification() timestamp.Timestamp { panic("identities needs to be properly loaded with identity.ReadLocal()") } -func (i *IdentityStub) CommitAsNeededWithRepo(repo repository.ClockedRepo) error { +func (i *IdentityStub) LastModificationLamports() map[string]lamport.Time { panic("identities needs to be properly loaded with identity.ReadLocal()") } @@ -92,11 +87,7 @@ func (IdentityStub) IsProtected() bool { panic("identities needs to be properly loaded with identity.ReadLocal()") } -func (i *IdentityStub) LastModificationLamport() lamport.Time { - panic("identities needs to be properly loaded with identity.ReadLocal()") -} - -func (i *IdentityStub) LastModification() timestamp.Timestamp { +func (IdentityStub) Validate() error { panic("identities needs to be properly loaded with identity.ReadLocal()") } diff --git a/identity/identity_test.go b/identity/identity_test.go index dc5925d99bbbad5a4fb445c514bae2e0fbce64dd..36d07be6a5fa8d3fdd888696c2b9797d7d0d834a 100644 --- a/identity/identity_test.go +++ b/identity/identity_test.go @@ -6,120 +6,108 @@ import ( "github.com/stretchr/testify/require" - "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/lamport" ) // Test the commit and load of an Identity with multiple versions func TestIdentityCommitLoad(t *testing.T) { - mockRepo := repository.NewMockRepo() + repo := makeIdentityTestRepo(t) // single version - identity := &Identity{ - id: entity.UnsetId, - versions: []*Version{ - { - name: "René Descartes", - email: "rene.descartes@example.com", - }, - }, - } + identity, err := NewIdentity(repo, "René Descartes", "rene.descartes@example.com") + require.NoError(t, err) - err := identity.Commit(mockRepo) + idBeforeCommit := identity.Id() + err = identity.Commit(repo) require.NoError(t, err) - require.NotEmpty(t, identity.id) - loaded, err := ReadLocal(mockRepo, identity.id) + commitsAreSet(t, identity) + require.NotEmpty(t, identity.Id()) + require.Equal(t, idBeforeCommit, identity.Id()) + require.Equal(t, idBeforeCommit, identity.versions[0].Id()) + + loaded, err := ReadLocal(repo, identity.Id()) require.NoError(t, err) commitsAreSet(t, loaded) require.Equal(t, identity, loaded) - // multiple version + // multiple versions - identity = &Identity{ - id: entity.UnsetId, - versions: []*Version{ - { - time: 100, - name: "René Descartes", - email: "rene.descartes@example.com", - keys: []*Key{ - {PubKey: "pubkeyA"}, - }, - }, - { - time: 200, - name: "René Descartes", - email: "rene.descartes@example.com", - keys: []*Key{ - {PubKey: "pubkeyB"}, - }, - }, - { - time: 201, - name: "René Descartes", - email: "rene.descartes@example.com", - keys: []*Key{ - {PubKey: "pubkeyC"}, - }, - }, - }, - } + identity, err = NewIdentityFull(repo, "René Descartes", "rene.descartes@example.com", "", "", []*Key{{PubKey: "pubkeyA"}}) + require.NoError(t, err) - err = identity.Commit(mockRepo) + idBeforeCommit = identity.Id() + err = identity.Mutate(repo, func(orig *Mutator) { + orig.Keys = []*Key{{PubKey: "pubkeyB"}} + }) require.NoError(t, err) - require.NotEmpty(t, identity.id) - loaded, err = ReadLocal(mockRepo, identity.id) + err = identity.Mutate(repo, func(orig *Mutator) { + orig.Keys = []*Key{{PubKey: "pubkeyC"}} + }) + require.NoError(t, err) + + require.Equal(t, idBeforeCommit, identity.Id()) + + err = identity.Commit(repo) + require.NoError(t, err) + + commitsAreSet(t, identity) + require.NotEmpty(t, identity.Id()) + require.Equal(t, idBeforeCommit, identity.Id()) + require.Equal(t, idBeforeCommit, identity.versions[0].Id()) + + loaded, err = ReadLocal(repo, identity.Id()) require.NoError(t, err) commitsAreSet(t, loaded) require.Equal(t, identity, loaded) // add more version - identity.addVersionForTest(&Version{ - time: 201, - name: "René Descartes", - email: "rene.descartes@example.com", - keys: []*Key{ - {PubKey: "pubkeyD"}, - }, + err = identity.Mutate(repo, func(orig *Mutator) { + orig.Email = "rene@descartes.com" + orig.Keys = []*Key{{PubKey: "pubkeyD"}} }) + require.NoError(t, err) - identity.addVersionForTest(&Version{ - time: 300, - name: "René Descartes", - email: "rene.descartes@example.com", - keys: []*Key{ - {PubKey: "pubkeyE"}, - }, + err = identity.Mutate(repo, func(orig *Mutator) { + orig.Email = "rene@descartes.com" + orig.Keys = []*Key{{PubKey: "pubkeyD"}, {PubKey: "pubkeyE"}} }) + require.NoError(t, err) - err = identity.Commit(mockRepo) - + err = identity.Commit(repo) require.NoError(t, err) - require.NotEmpty(t, identity.id) - loaded, err = ReadLocal(mockRepo, identity.id) + commitsAreSet(t, identity) + require.NotEmpty(t, identity.Id()) + require.Equal(t, idBeforeCommit, identity.Id()) + require.Equal(t, idBeforeCommit, identity.versions[0].Id()) + + loaded, err = ReadLocal(repo, identity.Id()) require.NoError(t, err) commitsAreSet(t, loaded) require.Equal(t, identity, loaded) } func TestIdentityMutate(t *testing.T) { - identity := NewIdentity("René Descartes", "rene.descartes@example.com") + repo := makeIdentityTestRepo(t) + + identity, err := NewIdentity(repo, "René Descartes", "rene.descartes@example.com") + require.NoError(t, err) require.Len(t, identity.versions, 1) - identity.Mutate(func(orig Mutator) Mutator { + err = identity.Mutate(repo, func(orig *Mutator) { orig.Email = "rene@descartes.fr" orig.Name = "René" orig.Login = "rene" - return orig }) + require.NoError(t, err) require.Len(t, identity.versions, 2) require.Equal(t, identity.Email(), "rene@descartes.fr") @@ -136,44 +124,33 @@ func commitsAreSet(t *testing.T, identity *Identity) { // Test that the correct crypto keys are returned for a given lamport time func TestIdentity_ValidKeysAtTime(t *testing.T) { identity := Identity{ - id: entity.UnsetId, - versions: []*Version{ + versions: []*version{ { - time: 100, - name: "René Descartes", - email: "rene.descartes@example.com", + times: map[string]lamport.Time{"foo": 100}, keys: []*Key{ {PubKey: "pubkeyA"}, }, }, { - time: 200, - name: "René Descartes", - email: "rene.descartes@example.com", + times: map[string]lamport.Time{"foo": 200}, keys: []*Key{ {PubKey: "pubkeyB"}, }, }, { - time: 201, - name: "René Descartes", - email: "rene.descartes@example.com", + times: map[string]lamport.Time{"foo": 201}, keys: []*Key{ {PubKey: "pubkeyC"}, }, }, { - time: 201, - name: "René Descartes", - email: "rene.descartes@example.com", + times: map[string]lamport.Time{"foo": 201}, keys: []*Key{ {PubKey: "pubkeyD"}, }, }, { - time: 300, - name: "René Descartes", - email: "rene.descartes@example.com", + times: map[string]lamport.Time{"foo": 300}, keys: []*Key{ {PubKey: "pubkeyE"}, }, @@ -181,47 +158,48 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) { }, } - require.Nil(t, identity.ValidKeysAtTime(10)) - require.Equal(t, identity.ValidKeysAtTime(100), []*Key{{PubKey: "pubkeyA"}}) - require.Equal(t, identity.ValidKeysAtTime(140), []*Key{{PubKey: "pubkeyA"}}) - require.Equal(t, identity.ValidKeysAtTime(200), []*Key{{PubKey: "pubkeyB"}}) - require.Equal(t, identity.ValidKeysAtTime(201), []*Key{{PubKey: "pubkeyD"}}) - require.Equal(t, identity.ValidKeysAtTime(202), []*Key{{PubKey: "pubkeyD"}}) - require.Equal(t, identity.ValidKeysAtTime(300), []*Key{{PubKey: "pubkeyE"}}) - require.Equal(t, identity.ValidKeysAtTime(3000), []*Key{{PubKey: "pubkeyE"}}) + require.Nil(t, identity.ValidKeysAtTime("foo", 10)) + require.Equal(t, identity.ValidKeysAtTime("foo", 100), []*Key{{PubKey: "pubkeyA"}}) + require.Equal(t, identity.ValidKeysAtTime("foo", 140), []*Key{{PubKey: "pubkeyA"}}) + require.Equal(t, identity.ValidKeysAtTime("foo", 200), []*Key{{PubKey: "pubkeyB"}}) + require.Equal(t, identity.ValidKeysAtTime("foo", 201), []*Key{{PubKey: "pubkeyD"}}) + require.Equal(t, identity.ValidKeysAtTime("foo", 202), []*Key{{PubKey: "pubkeyD"}}) + require.Equal(t, identity.ValidKeysAtTime("foo", 300), []*Key{{PubKey: "pubkeyE"}}) + require.Equal(t, identity.ValidKeysAtTime("foo", 3000), []*Key{{PubKey: "pubkeyE"}}) } // Test the immutable or mutable metadata search func TestMetadata(t *testing.T) { - mockRepo := repository.NewMockRepo() + repo := makeIdentityTestRepo(t) - identity := NewIdentity("René Descartes", "rene.descartes@example.com") + identity, err := NewIdentity(repo, "René Descartes", "rene.descartes@example.com") + require.NoError(t, err) identity.SetMetadata("key1", "value1") assertHasKeyValue(t, identity.ImmutableMetadata(), "key1", "value1") assertHasKeyValue(t, identity.MutableMetadata(), "key1", "value1") - err := identity.Commit(mockRepo) + err = identity.Commit(repo) require.NoError(t, err) assertHasKeyValue(t, identity.ImmutableMetadata(), "key1", "value1") assertHasKeyValue(t, identity.MutableMetadata(), "key1", "value1") // try override - identity.addVersionForTest(&Version{ - name: "René Descartes", - email: "rene.descartes@example.com", + err = identity.Mutate(repo, func(orig *Mutator) { + orig.Email = "rene@descartes.fr" }) + require.NoError(t, err) identity.SetMetadata("key1", "value2") assertHasKeyValue(t, identity.ImmutableMetadata(), "key1", "value1") assertHasKeyValue(t, identity.MutableMetadata(), "key1", "value2") - err = identity.Commit(mockRepo) + err = identity.Commit(repo) require.NoError(t, err) // reload - loaded, err := ReadLocal(mockRepo, identity.id) + loaded, err := ReadLocal(repo, identity.Id()) require.NoError(t, err) assertHasKeyValue(t, loaded.ImmutableMetadata(), "key1", "value1") @@ -235,22 +213,15 @@ func assertHasKeyValue(t *testing.T, metadata map[string]string, key, value stri } func TestJSON(t *testing.T) { - mockRepo := repository.NewMockRepo() + repo := makeIdentityTestRepo(t) - identity := &Identity{ - id: entity.UnsetId, - versions: []*Version{ - { - name: "René Descartes", - email: "rene.descartes@example.com", - }, - }, - } + identity, err := NewIdentity(repo, "René Descartes", "rene.descartes@example.com") + require.NoError(t, err) // commit to make sure we have an Id - err := identity.Commit(mockRepo) + err = identity.Commit(repo) require.NoError(t, err) - require.NotEmpty(t, identity.id) + require.NotEmpty(t, identity.Id()) // serialize data, err := json.Marshal(identity) @@ -260,10 +231,10 @@ func TestJSON(t *testing.T) { var i Interface i, err = UnmarshalJSON(data) require.NoError(t, err) - require.Equal(t, identity.id, i.Id()) + require.Equal(t, identity.Id(), i.Id()) // make sure we can load the identity properly - i, err = ReadLocal(mockRepo, i.Id()) + i, err = ReadLocal(repo, i.Id()) require.NoError(t, err) } @@ -280,7 +251,9 @@ func TestIdentityRemove(t *testing.T) { require.NoError(t, err) // generate an identity for testing - rene := NewIdentity("René Descartes", "rene@descartes.fr") + rene, err := NewIdentity(repo, "René Descartes", "rene@descartes.fr") + require.NoError(t, err) + err = rene.Commit(repo) require.NoError(t, err) diff --git a/identity/interface.go b/identity/interface.go index a71749624a1efe0ed891bc2787fdf5c0d5dc5a2a..92a03c510982f3fa4cd71f411d5e2c9aa567b2c6 100644 --- a/identity/interface.go +++ b/identity/interface.go @@ -13,6 +13,10 @@ type Interface interface { // Can be empty. Name() string + // DisplayName return a non-empty string to display, representing the + // identity, based on the non-empty values. + DisplayName() string + // Email return the last version of the email // Can be empty. Email() string @@ -32,26 +36,22 @@ type Interface interface { // Can be empty. Keys() []*Key - // ValidKeysAtTime return the set of keys valid at a given lamport time + // ValidKeysAtTime return the set of keys valid at a given lamport time for a given clock of another entity // Can be empty. - ValidKeysAtTime(time lamport.Time) []*Key + ValidKeysAtTime(clockName string, time lamport.Time) []*Key - // DisplayName return a non-empty string to display, representing the - // identity, based on the non-empty values. - DisplayName() string + // LastModification return the timestamp at which the last version of the identity became valid. + LastModification() timestamp.Timestamp - // Validate check if the Identity data is valid - Validate() error + // LastModificationLamports return the lamport times at which the last version of the identity became valid. + LastModificationLamports() map[string]lamport.Time // IsProtected return true if the chain of git commits started to be signed. // If that's the case, only signed commit with a valid key for this identity can be added. IsProtected() bool - // LastModificationLamportTime return the Lamport time at which the last version of the identity became valid. - LastModificationLamport() lamport.Time - - // LastModification return the timestamp at which the last version of the identity became valid. - LastModification() timestamp.Timestamp + // Validate check if the Identity data is valid + Validate() error // Indicate that the in-memory state changed and need to be commit in the repository NeedCommit() bool diff --git a/identity/version.go b/identity/version.go index bbf93575eac8d450b7490fca442b14f3de3c9291..bbf0a3f5ec2ea0c0da3c45e8d53ca4c7afe68fc4 100644 --- a/identity/version.go +++ b/identity/version.go @@ -2,9 +2,11 @@ package identity import ( "crypto/rand" + "crypto/sha256" "encoding/json" "fmt" "strings" + "time" "github.com/pkg/errors" @@ -15,76 +17,133 @@ import ( ) // 1: original format -const formatVersion = 1 - -// Version is a complete set of information about an Identity at a point in time. -type Version struct { - // The lamport time at which this version become effective - // The reference time is the bug edition lamport clock - // It must be the first field in this struct due to https://github.com/golang/go/issues/599 - // - // TODO: BREAKING CHANGE - this need to actually be one edition lamport time **per entity** - // This is not a problem right now but will be when more entities are added (pull-request, config ...) - time lamport.Time - unixTime int64 +// 2: Identity Ids are generated from the first version serialized data instead of from the first git commit +const formatVersion = 2 + +// TODO ^^ +// version is a complete set of information about an Identity at a point in time. +type version struct { name string email string // as defined in git or from a bridge when importing the identity login string // from a bridge when importing the identity avatarURL string + // The lamport times of the other entities at which this version become effective + times map[string]lamport.Time + unixTime int64 + // The set of keys valid at that time, from this version onward, until they get removed // in a new version. This allow to have multiple key for the same identity (e.g. one per // device) as well as revoke key. keys []*Key - // This optional array is here to ensure a better randomness of the identity id to avoid collisions. + // mandatory random bytes to ensure a better randomness of the data of the first + // version of a bug, used to later generate the ID + // len(Nonce) should be > 20 and < 64 bytes // It has no functional purpose and should be ignored. - // It is advised to fill this array if there is not enough entropy, e.g. if there is no keys. + // TODO: optional? nonce []byte // A set of arbitrary key/value to store metadata about a version or about an Identity in general. metadata map[string]string + // Not serialized. Store the version's id in memory. + id entity.Id // Not serialized commitHash repository.Hash } -type VersionJSON struct { +func newVersion(repo repository.RepoClock, name string, email string, login string, avatarURL string, keys []*Key) (*version, error) { + clocks, err := repo.AllClocks() + if err != nil { + return nil, err + } + + times := make(map[string]lamport.Time) + for name, clock := range clocks { + times[name] = clock.Time() + } + + return &version{ + id: entity.UnsetId, + name: name, + email: email, + login: login, + avatarURL: avatarURL, + times: times, + unixTime: time.Now().Unix(), + keys: keys, + nonce: makeNonce(20), + }, nil +} + +type versionJSON struct { // Additional field to version the data FormatVersion uint `json:"version"` - Time lamport.Time `json:"time"` - UnixTime int64 `json:"unix_time"` - Name string `json:"name,omitempty"` - Email string `json:"email,omitempty"` - Login string `json:"login,omitempty"` - AvatarUrl string `json:"avatar_url,omitempty"` - Keys []*Key `json:"pub_keys,omitempty"` - Nonce []byte `json:"nonce,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` + Times map[string]lamport.Time `json:"times"` + UnixTime int64 `json:"unix_time"` + Name string `json:"name,omitempty"` + Email string `json:"email,omitempty"` + Login string `json:"login,omitempty"` + AvatarUrl string `json:"avatar_url,omitempty"` + Keys []*Key `json:"pub_keys,omitempty"` + Nonce []byte `json:"nonce"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// Id return the identifier of the version +func (v *version) Id() entity.Id { + if v.id == "" { + // something went really wrong + panic("version's id not set") + } + if v.id == entity.UnsetId { + // This means we are trying to get the version's Id *before* it has been stored. + // As the Id is computed based on the actual bytes written on the disk, we are going to predict + // those and then get the Id. This is safe as it will be the exact same code writing on disk later. + data, err := json.Marshal(v) + if err != nil { + panic(err) + } + v.id = deriveId(data) + } + return v.id +} + +func deriveId(data []byte) entity.Id { + sum := sha256.Sum256(data) + return entity.Id(fmt.Sprintf("%x", sum)) } // Make a deep copy -func (v *Version) Clone() *Version { - clone := &Version{ - name: v.name, - email: v.email, - avatarURL: v.avatarURL, - keys: make([]*Key, len(v.keys)), +func (v *version) Clone() *version { + // copy direct fields + clone := *v + + clone.times = make(map[string]lamport.Time) + for name, t := range v.times { + clone.times[name] = t } + clone.keys = make([]*Key, len(v.keys)) for i, key := range v.keys { clone.keys[i] = key.Clone() } - return clone + clone.nonce = make([]byte, len(v.nonce)) + copy(clone.nonce, v.nonce) + + // not copying metadata + + return &clone } -func (v *Version) MarshalJSON() ([]byte, error) { - return json.Marshal(VersionJSON{ +func (v *version) MarshalJSON() ([]byte, error) { + return json.Marshal(versionJSON{ FormatVersion: formatVersion, - Time: v.time, + Times: v.times, UnixTime: v.unixTime, Name: v.name, Email: v.email, @@ -96,8 +155,8 @@ func (v *Version) MarshalJSON() ([]byte, error) { }) } -func (v *Version) UnmarshalJSON(data []byte) error { - var aux VersionJSON +func (v *version) UnmarshalJSON(data []byte) error { + var aux versionJSON if err := json.Unmarshal(data, &aux); err != nil { return err @@ -110,7 +169,8 @@ func (v *Version) UnmarshalJSON(data []byte) error { return entity.NewErrNewFormatVersion(aux.FormatVersion) } - v.time = aux.Time + v.id = deriveId(data) + v.times = aux.Times v.unixTime = aux.UnixTime v.name = aux.Name v.email = aux.Email @@ -123,23 +183,18 @@ func (v *Version) UnmarshalJSON(data []byte) error { return nil } -func (v *Version) Validate() error { +func (v *version) Validate() error { // time must be set after a commit if v.commitHash != "" && v.unixTime == 0 { return fmt.Errorf("unix time not set") } - if v.commitHash != "" && v.time == 0 { - return fmt.Errorf("lamport time not set") - } if text.Empty(v.name) && text.Empty(v.login) { return fmt.Errorf("either name or login should be set") } - if strings.Contains(v.name, "\n") { return fmt.Errorf("name should be a single line") } - if !text.Safe(v.name) { return fmt.Errorf("name is not fully printable") } @@ -147,7 +202,6 @@ func (v *Version) Validate() error { if strings.Contains(v.login, "\n") { return fmt.Errorf("login should be a single line") } - if !text.Safe(v.login) { return fmt.Errorf("login is not fully printable") } @@ -155,7 +209,6 @@ func (v *Version) Validate() error { if strings.Contains(v.email, "\n") { return fmt.Errorf("email should be a single line") } - if !text.Safe(v.email) { return fmt.Errorf("email is not fully printable") } @@ -167,6 +220,9 @@ func (v *Version) Validate() error { if len(v.nonce) > 64 { return fmt.Errorf("nonce is too big") } + if len(v.nonce) < 20 { + return fmt.Errorf("nonce is too small") + } for _, k := range v.keys { if err := k.Validate(); err != nil { @@ -177,9 +233,9 @@ func (v *Version) Validate() error { return nil } -// Write will serialize and store the Version as a git blob and return +// Write will serialize and store the version as a git blob and return // its hash -func (v *Version) Write(repo repository.Repo) (repository.Hash, error) { +func (v *version) Write(repo repository.Repo) (repository.Hash, error) { // make sure we don't write invalid data err := v.Validate() if err != nil { @@ -187,17 +243,18 @@ func (v *Version) Write(repo repository.Repo) (repository.Hash, error) { } data, err := json.Marshal(v) - if err != nil { return "", err } hash, err := repo.StoreData(data) - if err != nil { return "", err } + // make sure we set the Id when writing in the repo + v.id = deriveId(data) + return hash, nil } @@ -211,22 +268,22 @@ func makeNonce(len int) []byte { } // SetMetadata store arbitrary metadata about a version or an Identity in general -// If the Version has been commit to git already, it won't be overwritten. -func (v *Version) SetMetadata(key string, value string) { +// If the version has been commit to git already, it won't be overwritten. +// Beware: changing the metadata on a version will change it's ID +func (v *version) SetMetadata(key string, value string) { if v.metadata == nil { v.metadata = make(map[string]string) } - v.metadata[key] = value } -// GetMetadata retrieve arbitrary metadata about the Version -func (v *Version) GetMetadata(key string) (string, bool) { +// GetMetadata retrieve arbitrary metadata about the version +func (v *version) GetMetadata(key string) (string, bool) { val, ok := v.metadata[key] return val, ok } -// AllMetadata return all metadata for this Version -func (v *Version) AllMetadata() map[string]string { +// AllMetadata return all metadata for this version +func (v *version) AllMetadata() map[string]string { return v.metadata } diff --git a/identity/version_test.go b/identity/version_test.go index 25848eb5e6ede496e4f5ed942596314854e45334..1efa0d03cb2ad00147008a7e3ab3f45b9cdac8ad 100644 --- a/identity/version_test.go +++ b/identity/version_test.go @@ -3,39 +3,82 @@ package identity import ( "encoding/json" "testing" + "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/lamport" ) +func makeIdentityTestRepo(t *testing.T) repository.ClockedRepo { + repo := repository.NewMockRepo() + + clock1, err := repo.GetOrCreateClock("foo") + require.NoError(t, err) + err = clock1.Witness(42) // clock goes to 43 + require.NoError(t, err) + + clock2, err := repo.GetOrCreateClock("bar") + require.NoError(t, err) + err = clock2.Witness(34) // clock goes to 35 + require.NoError(t, err) + + return repo +} + func TestVersionSerialize(t *testing.T) { - before := &Version{ + repo := makeIdentityTestRepo(t) + + keys := []*Key{ + { + Fingerprint: "fingerprint1", + PubKey: "pubkey1", + }, + { + Fingerprint: "fingerprint2", + PubKey: "pubkey2", + }, + } + + before, err := newVersion(repo, "name", "email", "login", "avatarUrl", keys) + require.NoError(t, err) + + before.SetMetadata("key1", "value1") + before.SetMetadata("key2", "value2") + + expected := &version{ + id: entity.UnsetId, name: "name", email: "email", + login: "login", avatarURL: "avatarUrl", - keys: []*Key{ - { - Fingerprint: "fingerprint1", - PubKey: "pubkey1", - }, - { - Fingerprint: "fingerprint2", - PubKey: "pubkey2", - }, + unixTime: time.Now().Unix(), + times: map[string]lamport.Time{ + "foo": 43, + "bar": 35, }, - nonce: makeNonce(20), + keys: keys, + nonce: before.nonce, metadata: map[string]string{ "key1": "value1", "key2": "value2", }, - time: 3, } + require.Equal(t, expected, before) + data, err := json.Marshal(before) assert.NoError(t, err) - var after Version + var after version err = json.Unmarshal(data, &after) assert.NoError(t, err) - assert.Equal(t, before, &after) + // make sure we now have an Id + expected.Id() + + assert.Equal(t, expected, &after) } From 7163b2283b4542a4d4abfe9a71963f122322bde7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 8 Nov 2020 19:15:06 +0100 Subject: [PATCH 04/42] bug: Id from first operation data, not git + remove root link --- bug/bug.go | 233 +++++++++++---------------------- bug/bug_actions_test.go | 20 +-- bug/bug_test.go | 28 ++-- bug/git_tree.go | 84 ++++++++++++ bug/op_add_comment_test.go | 4 +- bug/op_create.go | 26 +++- bug/op_create_test.go | 26 ++-- bug/op_edit_comment_test.go | 59 ++++----- bug/op_label_change_test.go | 14 +- bug/op_noop_test.go | 4 +- bug/op_set_metadata_test.go | 51 ++++---- bug/op_set_status_test.go | 14 +- bug/op_set_title_test.go | 14 +- bug/operation_iterator_test.go | 11 +- bug/operation_pack.go | 5 +- bug/operation_pack_test.go | 15 +-- bug/operation_test.go | 32 +++-- entity/refs.go | 4 +- identity/identity.go | 4 +- 19 files changed, 341 insertions(+), 307 deletions(-) create mode 100644 bug/git_tree.go diff --git a/bug/bug.go b/bug/bug.go index f6c35a2d6d5b925e54034df9be77b37d94d710d6..e67920f9aa40fe60837899300051401d60861de3 100644 --- a/bug/bug.go +++ b/bug/bug.go @@ -4,7 +4,6 @@ package bug import ( "encoding/json" "fmt" - "strings" "github.com/pkg/errors" @@ -18,7 +17,6 @@ const bugsRefPattern = "refs/bugs/" const bugsRemoteRefPattern = "refs/remotes/%s/bugs/" const opsEntryName = "ops" -const rootEntryName = "root" const mediaEntryName = "media" const createClockEntryPrefix = "create-clock-" @@ -57,7 +55,6 @@ type Bug struct { id entity.Id lastCommit repository.Hash - rootPack repository.Hash // all the committed operations packs []OperationPack @@ -71,7 +68,7 @@ type Bug struct { func NewBug() *Bug { // No id yet // No logical clock yet - return &Bug{} + return &Bug{id: entity.UnsetId} } // ReadLocal will read a local bug from its hash @@ -100,122 +97,77 @@ func ReadRemoteWithResolver(repo repository.ClockedRepo, identityResolver identi // read will read and parse a Bug from git func read(repo repository.ClockedRepo, identityResolver identity.Resolver, ref string) (*Bug, error) { - refSplit := strings.Split(ref, "/") - id := entity.Id(refSplit[len(refSplit)-1]) + id := entity.RefToId(ref) if err := id.Validate(); err != nil { return nil, errors.Wrap(err, "invalid ref ") } hashes, err := repo.ListCommits(ref) - - // TODO: this is not perfect, it might be a command invoke error if err != nil { return nil, ErrBugNotExist } + if len(hashes) == 0 { + return nil, fmt.Errorf("empty bug") + } bug := Bug{ - id: id, - editTime: 0, + id: id, } // Load each OperationPack for _, hash := range hashes { - entries, err := repo.ReadTree(hash) + tree, err := readTree(repo, hash) if err != nil { - return nil, errors.Wrap(err, "can't list git tree entries") - } - - bug.lastCommit = hash - - var opsEntry repository.TreeEntry - opsFound := false - var rootEntry repository.TreeEntry - rootFound := false - var createTime uint64 - var editTime uint64 - - for _, entry := range entries { - if entry.Name == opsEntryName { - opsEntry = entry - opsFound = true - continue - } - if entry.Name == rootEntryName { - rootEntry = entry - rootFound = true - } - if strings.HasPrefix(entry.Name, createClockEntryPrefix) { - n, err := fmt.Sscanf(entry.Name, createClockEntryPattern, &createTime) - if err != nil { - return nil, errors.Wrap(err, "can't read create lamport time") - } - if n != 1 { - return nil, fmt.Errorf("could not parse create time lamport value") - } - } - if strings.HasPrefix(entry.Name, editClockEntryPrefix) { - n, err := fmt.Sscanf(entry.Name, editClockEntryPattern, &editTime) - if err != nil { - return nil, errors.Wrap(err, "can't read edit lamport time") - } - if n != 1 { - return nil, fmt.Errorf("could not parse edit time lamport value") - } - } - } - - if !opsFound { - return nil, errors.New("invalid tree, missing the ops entry") - } - if !rootFound { - return nil, errors.New("invalid tree, missing the root entry") - } - - if bug.rootPack == "" { - bug.rootPack = rootEntry.Hash - bug.createTime = lamport.Time(createTime) + return nil, err } // Due to rebase, edit Lamport time are not necessarily ordered - if editTime > uint64(bug.editTime) { - bug.editTime = lamport.Time(editTime) + if tree.editTime > bug.editTime { + bug.editTime = tree.editTime } // Update the clocks - createClock, err := repo.GetOrCreateClock(creationClockName) + err = repo.Witness(creationClockName, bug.createTime) if err != nil { - return nil, err - } - if err := createClock.Witness(bug.createTime); err != nil { return nil, errors.Wrap(err, "failed to update create lamport clock") } - editClock, err := repo.GetOrCreateClock(editClockName) + err = repo.Witness(editClockName, bug.editTime) if err != nil { - return nil, err - } - if err := editClock.Witness(bug.editTime); err != nil { return nil, errors.Wrap(err, "failed to update edit lamport clock") } - data, err := repo.ReadData(opsEntry.Hash) + data, err := repo.ReadData(tree.opsEntry.Hash) if err != nil { return nil, errors.Wrap(err, "failed to read git blob data") } opp := &OperationPack{} err = json.Unmarshal(data, &opp) - if err != nil { return nil, errors.Wrap(err, "failed to decode OperationPack json") } // tag the pack with the commit hash opp.commitHash = hash + bug.lastCommit = hash + + // if it's the first OperationPack read + if len(bug.packs) == 0 { + bug.createTime = tree.createTime + } bug.packs = append(bug.packs, *opp) } + // Bug Id is the Id of the first operation + if len(bug.packs[0].Operations) == 0 { + return nil, fmt.Errorf("first OperationPack is empty") + } + if bug.id != bug.packs[0].Operations[0].Id() { + return nil, fmt.Errorf("bug ID doesn't match the first operation ID") + } + // Make sure that the identities are properly loaded err = bug.EnsureIdentities(identityResolver) if err != nil { @@ -367,8 +319,8 @@ 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.String() { + // The bug Id should be the id of the first operation + if bug.FirstOp().Id() != bug.id { return fmt.Errorf("bug id should be the first commit hash") } @@ -396,12 +348,17 @@ func (bug *Bug) Validate() error { // Append an operation into the staging area, to be committed later func (bug *Bug) Append(op Operation) { + if len(bug.packs) == 0 && len(bug.staging.Operations) == 0 { + if op.base().OperationType != CreateOp { + panic("first operation should be a Create") + } + bug.id = op.Id() + } bug.staging.Append(op) } // Commit write the staging area in Git and move the operations to the packs func (bug *Bug) Commit(repo repository.ClockedRepo) error { - if !bug.NeedCommit() { return fmt.Errorf("can't commit a bug with no pending operation") } @@ -410,38 +367,29 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error { return errors.Wrap(err, "can't commit a bug with invalid data") } - // Write the Ops as a Git blob containing the serialized array - hash, err := bug.staging.Write(repo) + // update clocks + var err error + bug.editTime, err = repo.Increment(editClockName) if err != nil { return err } + if bug.lastCommit == "" { + bug.createTime, err = repo.Increment(creationClockName) + if err != nil { + return err + } + } - if bug.rootPack == "" { - bug.rootPack = hash + // Write the Ops as a Git blob containing the serialized array + hash, err := bug.staging.Write(repo) + if err != nil { + return err } // Make a Git tree referencing this blob tree := []repository.TreeEntry{ // the last pack of ops {ObjectType: repository.Blob, Hash: hash, Name: opsEntryName}, - // always the first pack of ops (might be the same) - {ObjectType: repository.Blob, Hash: bug.rootPack, Name: rootEntryName}, - } - - // Reference, if any, all the files required by the ops - // Git will check that they actually exist in the storage and will make sure - // to push/pull them as needed. - mediaTree := makeMediaTree(bug.staging) - if len(mediaTree) > 0 { - mediaTreeHash, err := repo.StoreTree(mediaTree) - if err != nil { - return err - } - tree = append(tree, repository.TreeEntry{ - ObjectType: repository.Tree, - Hash: mediaTreeHash, - Name: mediaEntryName, - }) } // Store the logical clocks as well @@ -454,31 +402,12 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error { if err != nil { return err } - - editClock, err := repo.GetOrCreateClock(editClockName) - if err != nil { - return err - } - bug.editTime, err = editClock.Increment() - if err != nil { - return err - } - tree = append(tree, repository.TreeEntry{ ObjectType: repository.Blob, Hash: emptyBlobHash, Name: fmt.Sprintf(editClockEntryPattern, bug.editTime), }) if bug.lastCommit == "" { - createClock, err := repo.GetOrCreateClock(creationClockName) - if err != nil { - return err - } - bug.createTime, err = createClock.Increment() - if err != nil { - return err - } - tree = append(tree, repository.TreeEntry{ ObjectType: repository.Blob, Hash: emptyBlobHash, @@ -486,6 +415,22 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error { }) } + // Reference, if any, all the files required by the ops + // Git will check that they actually exist in the storage and will make sure + // to push/pull them as needed. + mediaTree := makeMediaTree(bug.staging) + if len(mediaTree) > 0 { + mediaTreeHash, err := repo.StoreTree(mediaTree) + if err != nil { + return err + } + tree = append(tree, repository.TreeEntry{ + ObjectType: repository.Tree, + Hash: mediaTreeHash, + Name: mediaEntryName, + }) + } + // Store the tree hash, err = repo.StoreTree(tree) if err != nil { @@ -498,33 +443,25 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error { } else { hash, err = repo.StoreCommit(hash) } - if err != nil { return err } bug.lastCommit = hash + bug.staging.commitHash = hash + bug.packs = append(bug.packs, bug.staging) + bug.staging = OperationPack{} - // if it was the first commit, use the commit hash as bug id - if bug.id == "" { - bug.id = entity.Id(hash) + // if it was the first commit, use the Id of the first op (create) + if bug.id == "" || bug.id == entity.UnsetId { + bug.id = bug.packs[0].Operations[0].Id() } // Create or update the Git reference for this bug // When pushing later, the remote will ensure that this ref update // is fast-forward, that is no data has been overwritten ref := fmt.Sprintf("%s%s", bugsRefPattern, bug.id) - err = repo.UpdateRef(ref, hash) - - if err != nil { - return err - } - - bug.staging.commitHash = hash - bug.packs = append(bug.packs, bug.staging) - bug.staging = OperationPack{} - - return nil + return repo.UpdateRef(ref, hash) } func (bug *Bug) CommitAsNeeded(repo repository.ClockedRepo) error { @@ -538,30 +475,6 @@ func (bug *Bug) NeedCommit() bool { return !bug.staging.IsEmpty() } -func makeMediaTree(pack OperationPack) []repository.TreeEntry { - var tree []repository.TreeEntry - counter := 0 - added := make(map[repository.Hash]interface{}) - - for _, ops := range pack.Operations { - for _, file := range ops.GetFiles() { - if _, has := added[file]; !has { - tree = append(tree, repository.TreeEntry{ - ObjectType: repository.Blob, - Hash: file, - // The name is not important here, we only need to - // reference the blob. - Name: fmt.Sprintf("file%d", counter), - }) - counter++ - added[file] = struct{}{} - } - } - } - - return tree -} - // Merge a different version of the same bug by rebasing operations of this bug // that are not present in the other on top of the chain of operations of the // other version. @@ -657,9 +570,9 @@ func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) { // Id return the Bug identifier func (bug *Bug) Id() entity.Id { - if bug.id == "" { + if bug.id == "" || bug.id == entity.UnsetId { // simply panic as it would be a coding error - // (using an id of a bug not stored yet) + // (using an id of a bug without operation yet) panic("no id yet") } return bug.id diff --git a/bug/bug_actions_test.go b/bug/bug_actions_test.go index df35a5e5134dce992ac0f25b7772d9affeccdacc..7a9673d6f420c22caa3ee521da7bf1c0fe1a223e 100644 --- a/bug/bug_actions_test.go +++ b/bug/bug_actions_test.go @@ -15,8 +15,9 @@ func TestPushPull(t *testing.T) { repoA, repoB, remote := repository.SetupReposAndRemote() defer repository.CleanupTestRepos(repoA, repoB, remote) - reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := reneA.Commit(repoA) + reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") + require.NoError(t, err) + err = reneA.Commit(repoA) require.NoError(t, err) bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message") @@ -92,8 +93,9 @@ func _RebaseTheirs(t testing.TB) { repoA, repoB, remote := repository.SetupReposAndRemote() defer repository.CleanupTestRepos(repoA, repoB, remote) - reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := reneA.Commit(repoA) + reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") + require.NoError(t, err) + err = reneA.Commit(repoA) require.NoError(t, err) bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message") @@ -172,8 +174,9 @@ func _RebaseOurs(t testing.TB) { repoA, repoB, remote := repository.SetupReposAndRemote() defer repository.CleanupTestRepos(repoA, repoB, remote) - reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := reneA.Commit(repoA) + reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") + require.NoError(t, err) + err = reneA.Commit(repoA) require.NoError(t, err) bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message") @@ -263,8 +266,9 @@ func _RebaseConflict(t testing.TB) { repoA, repoB, remote := repository.SetupReposAndRemote() defer repository.CleanupTestRepos(repoA, repoB, remote) - reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := reneA.Commit(repoA) + reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") + require.NoError(t, err) + err = reneA.Commit(repoA) require.NoError(t, err) bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message") diff --git a/bug/bug_test.go b/bug/bug_test.go index 047fe386d812b58a2ebf9ccabbc69da9253ab009..a8987ac1143795982601034562f75679e0d75e80 100644 --- a/bug/bug_test.go +++ b/bug/bug_test.go @@ -12,19 +12,20 @@ import ( ) func TestBugId(t *testing.T) { - mockRepo := repository.NewMockRepo() + repo := repository.NewMockRepo() bug1 := NewBug() - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := rene.Commit(mockRepo) + rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") + require.NoError(t, err) + err = rene.Commit(repo) require.NoError(t, err) createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil) bug1.Append(createOp) - err = bug1.Commit(mockRepo) + err = bug1.Commit(repo) if err != nil { t.Fatal(err) @@ -34,12 +35,13 @@ func TestBugId(t *testing.T) { } func TestBugValidity(t *testing.T) { - mockRepo := repository.NewMockRepo() + repo := repository.NewMockRepo() bug1 := NewBug() - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := rene.Commit(mockRepo) + rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") + require.NoError(t, err) + err = rene.Commit(repo) require.NoError(t, err) createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil) @@ -54,7 +56,7 @@ func TestBugValidity(t *testing.T) { t.Fatal("Bug with just a CreateOp should be valid") } - err = bug1.Commit(mockRepo) + err = bug1.Commit(repo) if err != nil { t.Fatal(err) } @@ -65,7 +67,7 @@ func TestBugValidity(t *testing.T) { t.Fatal("Bug with multiple CreateOp should be invalid") } - err = bug1.Commit(mockRepo) + err = bug1.Commit(repo) if err == nil { t.Fatal("Invalid bug should not commit") } @@ -76,8 +78,9 @@ func TestBugCommitLoad(t *testing.T) { bug1 := NewBug() - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := rene.Commit(repo) + rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") + require.NoError(t, err) + err = rene.Commit(repo) require.NoError(t, err) createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil) @@ -137,7 +140,8 @@ func TestBugRemove(t *testing.T) { require.NoError(t, err) // generate a bunch of bugs - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") + rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") + require.NoError(t, err) err = rene.Commit(repo) require.NoError(t, err) diff --git a/bug/git_tree.go b/bug/git_tree.go new file mode 100644 index 0000000000000000000000000000000000000000..a5583bda4411affb342d83c84a915bae2c1f94eb --- /dev/null +++ b/bug/git_tree.go @@ -0,0 +1,84 @@ +package bug + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" + + "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/lamport" +) + +type gitTree struct { + opsEntry repository.TreeEntry + createTime lamport.Time + editTime lamport.Time +} + +func readTree(repo repository.RepoData, hash repository.Hash) (*gitTree, error) { + tree := &gitTree{} + + entries, err := repo.ReadTree(hash) + if err != nil { + return nil, errors.Wrap(err, "can't list git tree entries") + } + + opsFound := false + + for _, entry := range entries { + if entry.Name == opsEntryName { + tree.opsEntry = entry + opsFound = true + continue + } + if strings.HasPrefix(entry.Name, createClockEntryPrefix) { + n, err := fmt.Sscanf(entry.Name, createClockEntryPattern, &tree.createTime) + if err != nil { + return nil, errors.Wrap(err, "can't read create lamport time") + } + if n != 1 { + return nil, fmt.Errorf("could not parse create time lamport value") + } + } + if strings.HasPrefix(entry.Name, editClockEntryPrefix) { + n, err := fmt.Sscanf(entry.Name, editClockEntryPattern, &tree.editTime) + if err != nil { + return nil, errors.Wrap(err, "can't read edit lamport time") + } + if n != 1 { + return nil, fmt.Errorf("could not parse edit time lamport value") + } + } + } + + if !opsFound { + return nil, errors.New("invalid tree, missing the ops entry") + } + + return tree, nil +} + +func makeMediaTree(pack OperationPack) []repository.TreeEntry { + var tree []repository.TreeEntry + counter := 0 + added := make(map[repository.Hash]interface{}) + + for _, ops := range pack.Operations { + for _, file := range ops.GetFiles() { + if _, has := added[file]; !has { + tree = append(tree, repository.TreeEntry{ + ObjectType: repository.Blob, + Hash: file, + // The name is not important here, we only need to + // reference the blob. + Name: fmt.Sprintf("file%d", counter), + }) + counter++ + added[file] = struct{}{} + } + } + } + + return tree +} diff --git a/bug/op_add_comment_test.go b/bug/op_add_comment_test.go index 3f9d02f1d49271ba431daceafd0988d7884b838f..3b41d62d3b83323f9b8083d409d632fb6d57854c 100644 --- a/bug/op_add_comment_test.go +++ b/bug/op_add_comment_test.go @@ -14,8 +14,8 @@ import ( func TestAddCommentSerialize(t *testing.T) { repo := repository.NewMockRepo() - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := rene.Commit(repo) + + rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") require.NoError(t, err) unix := time.Now().Unix() diff --git a/bug/op_create.go b/bug/op_create.go index 9bb40d35ded864c2155f368f489ddd1e1db5f6b3..3c8ce6587c9f3d0003f3a50a29e784b6dd7b85c5 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -1,6 +1,7 @@ package bug import ( + "crypto/rand" "encoding/json" "fmt" "strings" @@ -17,6 +18,10 @@ var _ Operation = &CreateOperation{} // CreateOperation define the initial creation of a bug type CreateOperation struct { OpBase + // mandatory random bytes to ensure a better randomness of the data of the first + // operation of a bug, used to later generate the ID + // len(Nonce) should be > 20 and < 64 bytes + Nonce []byte `json:"nonce"` Title string `json:"title"` Message string `json:"message"` Files []repository.Hash `json:"files"` @@ -66,14 +71,19 @@ func (op *CreateOperation) Validate() error { return err } + if len(op.Nonce) > 64 { + return fmt.Errorf("create nonce is too big") + } + if len(op.Nonce) < 20 { + return fmt.Errorf("create nonce is too small") + } + if text.Empty(op.Title) { return fmt.Errorf("title is empty") } - if strings.Contains(op.Title, "\n") { return fmt.Errorf("title should be a single line") } - if !text.Safe(op.Title) { return fmt.Errorf("title is not fully printable") } @@ -98,6 +108,7 @@ func (op *CreateOperation) UnmarshalJSON(data []byte) error { } aux := struct { + Nonce []byte `json:"nonce"` Title string `json:"title"` Message string `json:"message"` Files []repository.Hash `json:"files"` @@ -109,6 +120,7 @@ func (op *CreateOperation) UnmarshalJSON(data []byte) error { } op.OpBase = base + op.Nonce = aux.Nonce op.Title = aux.Title op.Message = aux.Message op.Files = aux.Files @@ -119,9 +131,19 @@ func (op *CreateOperation) UnmarshalJSON(data []byte) error { // Sign post method for gqlgen func (op *CreateOperation) IsAuthored() {} +func makeNonce(len int) []byte { + result := make([]byte, len) + _, err := rand.Read(result) + if err != nil { + panic(err) + } + return result +} + func NewCreateOp(author identity.Interface, unixTime int64, title, message string, files []repository.Hash) *CreateOperation { return &CreateOperation{ OpBase: newOpBase(CreateOp, author, unixTime), + Nonce: makeNonce(20), Title: title, Message: message, Files: files, diff --git a/bug/op_create_test.go b/bug/op_create_test.go index 2d28a208bd61e55f403589b7602357076604bc6b..533aec2e917c4fc56e40065e987dd1a5a8014fba 100644 --- a/bug/op_create_test.go +++ b/bug/op_create_test.go @@ -5,17 +5,21 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/timestamp" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestCreate(t *testing.T) { snapshot := Snapshot{} - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") + repo := repository.NewMockRepoClock() + + rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") + require.NoError(t, err) + unix := time.Now().Unix() create := NewCreateOp(rene, unix, "title", "message", nil) @@ -23,7 +27,7 @@ func TestCreate(t *testing.T) { create.Apply(&snapshot) id := create.Id() - assert.NoError(t, id.Validate()) + require.NoError(t, id.Validate()) comment := Comment{ id: id, @@ -48,31 +52,31 @@ func TestCreate(t *testing.T) { }, } - assert.Equal(t, expected, snapshot) + require.Equal(t, expected, snapshot) } func TestCreateSerialize(t *testing.T) { repo := repository.NewMockRepo() - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := rene.Commit(repo) + + rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") require.NoError(t, err) unix := time.Now().Unix() before := NewCreateOp(rene, unix, "title", "message", nil) data, err := json.Marshal(before) - assert.NoError(t, err) + require.NoError(t, err) var after CreateOperation err = json.Unmarshal(data, &after) - assert.NoError(t, err) + require.NoError(t, err) // enforce creating the ID before.Id() // Replace the identity stub with the real thing - assert.Equal(t, rene.Id(), after.base().Author.Id()) + require.Equal(t, rene.Id(), after.base().Author.Id()) after.Author = rene - assert.Equal(t, before, &after) + require.Equal(t, before, &after) } diff --git a/bug/op_edit_comment_test.go b/bug/op_edit_comment_test.go index 263111f96d5e61e9e503f14c1b60d72d27102eeb..92ee7539b2291a8ed209b268d9e08bdf77f2343b 100644 --- a/bug/op_edit_comment_test.go +++ b/bug/op_edit_comment_test.go @@ -5,7 +5,6 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/MichaelMure/git-bug/identity" @@ -16,8 +15,8 @@ func TestEdit(t *testing.T) { snapshot := Snapshot{} repo := repository.NewMockRepo() - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := rene.Commit(repo) + + rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") require.NoError(t, err) unix := time.Now().Unix() @@ -47,59 +46,59 @@ func TestEdit(t *testing.T) { edit := NewEditCommentOp(rene, unix, id1, "create edited", nil) edit.Apply(&snapshot) - assert.Equal(t, len(snapshot.Timeline), 4) - assert.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2) - assert.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 1) - assert.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 1) - assert.Equal(t, snapshot.Comments[0].Message, "create edited") - assert.Equal(t, snapshot.Comments[1].Message, "comment 1") - assert.Equal(t, snapshot.Comments[2].Message, "comment 2") + require.Equal(t, len(snapshot.Timeline), 4) + require.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2) + require.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 1) + require.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 1) + require.Equal(t, snapshot.Comments[0].Message, "create edited") + require.Equal(t, snapshot.Comments[1].Message, "comment 1") + require.Equal(t, snapshot.Comments[2].Message, "comment 2") edit2 := NewEditCommentOp(rene, unix, id2, "comment 1 edited", nil) edit2.Apply(&snapshot) - assert.Equal(t, len(snapshot.Timeline), 4) - assert.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2) - assert.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 2) - assert.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 1) - assert.Equal(t, snapshot.Comments[0].Message, "create edited") - assert.Equal(t, snapshot.Comments[1].Message, "comment 1 edited") - assert.Equal(t, snapshot.Comments[2].Message, "comment 2") + require.Equal(t, len(snapshot.Timeline), 4) + require.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2) + require.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 2) + require.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 1) + require.Equal(t, snapshot.Comments[0].Message, "create edited") + require.Equal(t, snapshot.Comments[1].Message, "comment 1 edited") + require.Equal(t, snapshot.Comments[2].Message, "comment 2") edit3 := NewEditCommentOp(rene, unix, id3, "comment 2 edited", nil) edit3.Apply(&snapshot) - assert.Equal(t, len(snapshot.Timeline), 4) - assert.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2) - assert.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 2) - assert.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 2) - assert.Equal(t, snapshot.Comments[0].Message, "create edited") - assert.Equal(t, snapshot.Comments[1].Message, "comment 1 edited") - assert.Equal(t, snapshot.Comments[2].Message, "comment 2 edited") + require.Equal(t, len(snapshot.Timeline), 4) + require.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2) + require.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 2) + require.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 2) + require.Equal(t, snapshot.Comments[0].Message, "create edited") + require.Equal(t, snapshot.Comments[1].Message, "comment 1 edited") + require.Equal(t, snapshot.Comments[2].Message, "comment 2 edited") } func TestEditCommentSerialize(t *testing.T) { repo := repository.NewMockRepo() - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := rene.Commit(repo) + + rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") require.NoError(t, err) unix := time.Now().Unix() before := NewEditCommentOp(rene, unix, "target", "message", nil) data, err := json.Marshal(before) - assert.NoError(t, err) + require.NoError(t, err) var after EditCommentOperation err = json.Unmarshal(data, &after) - assert.NoError(t, err) + require.NoError(t, err) // enforce creating the ID before.Id() // Replace the identity stub with the real thing - assert.Equal(t, rene.Id(), after.base().Author.Id()) + require.Equal(t, rene.Id(), after.base().Author.Id()) after.Author = rene - assert.Equal(t, before, &after) + require.Equal(t, before, &after) } diff --git a/bug/op_label_change_test.go b/bug/op_label_change_test.go index ea73368cedcbba62e72084ca8acbbd939f3c7511..96716ffe490e412663d7305c5b134773a34756a2 100644 --- a/bug/op_label_change_test.go +++ b/bug/op_label_change_test.go @@ -9,32 +9,30 @@ import ( "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" - - "github.com/stretchr/testify/assert" ) func TestLabelChangeSerialize(t *testing.T) { repo := repository.NewMockRepo() - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := rene.Commit(repo) + + rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") require.NoError(t, err) unix := time.Now().Unix() before := NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"}) data, err := json.Marshal(before) - assert.NoError(t, err) + require.NoError(t, err) var after LabelChangeOperation err = json.Unmarshal(data, &after) - assert.NoError(t, err) + require.NoError(t, err) // enforce creating the ID before.Id() // Replace the identity stub with the real thing - assert.Equal(t, rene.Id(), after.base().Author.Id()) + require.Equal(t, rene.Id(), after.base().Author.Id()) after.Author = rene - assert.Equal(t, before, &after) + require.Equal(t, before, &after) } diff --git a/bug/op_noop_test.go b/bug/op_noop_test.go index 812851ea2a10bb9dbdcc668eac2f0463f3a69f16..ce2f98afa4089f3de66bf23d22dbf0540c72a6ec 100644 --- a/bug/op_noop_test.go +++ b/bug/op_noop_test.go @@ -15,8 +15,8 @@ import ( func TestNoopSerialize(t *testing.T) { repo := repository.NewMockRepo() - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := rene.Commit(repo) + + rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") require.NoError(t, err) unix := time.Now().Unix() diff --git a/bug/op_set_metadata_test.go b/bug/op_set_metadata_test.go index ba068f61e27db0f44b6d4402c93608fffe5874f1..c90f192af1ecc2d525e1ac46eab3825e24ec58e6 100644 --- a/bug/op_set_metadata_test.go +++ b/bug/op_set_metadata_test.go @@ -8,7 +8,6 @@ import ( "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -16,8 +15,8 @@ func TestSetMetadata(t *testing.T) { snapshot := Snapshot{} repo := repository.NewMockRepo() - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := rene.Commit(repo) + + rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") require.NoError(t, err) unix := time.Now().Unix() @@ -47,15 +46,15 @@ func TestSetMetadata(t *testing.T) { snapshot.Operations = append(snapshot.Operations, op1) createMetadata := snapshot.Operations[0].AllMetadata() - assert.Equal(t, len(createMetadata), 2) + require.Equal(t, len(createMetadata), 2) // original key is not overrided - assert.Equal(t, createMetadata["key"], "value") + require.Equal(t, createMetadata["key"], "value") // new key is set - assert.Equal(t, createMetadata["key2"], "value") + require.Equal(t, createMetadata["key2"], "value") commentMetadata := snapshot.Operations[1].AllMetadata() - assert.Equal(t, len(commentMetadata), 1) - assert.Equal(t, commentMetadata["key2"], "value2") + require.Equal(t, len(commentMetadata), 1) + require.Equal(t, commentMetadata["key2"], "value2") op2 := NewSetMetadataOp(rene, unix, id2, map[string]string{ "key2": "value", @@ -66,16 +65,16 @@ func TestSetMetadata(t *testing.T) { snapshot.Operations = append(snapshot.Operations, op2) createMetadata = snapshot.Operations[0].AllMetadata() - assert.Equal(t, len(createMetadata), 2) - assert.Equal(t, createMetadata["key"], "value") - assert.Equal(t, createMetadata["key2"], "value") + require.Equal(t, len(createMetadata), 2) + require.Equal(t, createMetadata["key"], "value") + require.Equal(t, createMetadata["key2"], "value") commentMetadata = snapshot.Operations[1].AllMetadata() - assert.Equal(t, len(commentMetadata), 2) + require.Equal(t, len(commentMetadata), 2) // original key is not overrided - assert.Equal(t, commentMetadata["key2"], "value2") + require.Equal(t, commentMetadata["key2"], "value2") // new key is set - assert.Equal(t, commentMetadata["key3"], "value3") + require.Equal(t, commentMetadata["key3"], "value3") op3 := NewSetMetadataOp(rene, unix, id1, map[string]string{ "key": "override", @@ -86,22 +85,22 @@ func TestSetMetadata(t *testing.T) { snapshot.Operations = append(snapshot.Operations, op3) createMetadata = snapshot.Operations[0].AllMetadata() - assert.Equal(t, len(createMetadata), 2) + require.Equal(t, len(createMetadata), 2) // original key is not overrided - assert.Equal(t, createMetadata["key"], "value") + require.Equal(t, createMetadata["key"], "value") // previously set key is not overrided - assert.Equal(t, createMetadata["key2"], "value") + require.Equal(t, createMetadata["key2"], "value") commentMetadata = snapshot.Operations[1].AllMetadata() - assert.Equal(t, len(commentMetadata), 2) - assert.Equal(t, commentMetadata["key2"], "value2") - assert.Equal(t, commentMetadata["key3"], "value3") + require.Equal(t, len(commentMetadata), 2) + require.Equal(t, commentMetadata["key2"], "value2") + require.Equal(t, commentMetadata["key3"], "value3") } func TestSetMetadataSerialize(t *testing.T) { repo := repository.NewMockRepo() - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := rene.Commit(repo) + + rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") require.NoError(t, err) unix := time.Now().Unix() @@ -111,18 +110,18 @@ func TestSetMetadataSerialize(t *testing.T) { }) data, err := json.Marshal(before) - assert.NoError(t, err) + require.NoError(t, err) var after SetMetadataOperation err = json.Unmarshal(data, &after) - assert.NoError(t, err) + require.NoError(t, err) // enforce creating the ID before.Id() // Replace the identity stub with the real thing - assert.Equal(t, rene.Id(), after.base().Author.Id()) + require.Equal(t, rene.Id(), after.base().Author.Id()) after.Author = rene - assert.Equal(t, before, &after) + require.Equal(t, before, &after) } diff --git a/bug/op_set_status_test.go b/bug/op_set_status_test.go index 0619c9135c208d37161e4734ec4c280f54e13021..3b26282f72d611609309985b8409c9a72b005b31 100644 --- a/bug/op_set_status_test.go +++ b/bug/op_set_status_test.go @@ -9,32 +9,30 @@ import ( "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" - - "github.com/stretchr/testify/assert" ) func TestSetStatusSerialize(t *testing.T) { repo := repository.NewMockRepo() - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := rene.Commit(repo) + + rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") require.NoError(t, err) unix := time.Now().Unix() before := NewSetStatusOp(rene, unix, ClosedStatus) data, err := json.Marshal(before) - assert.NoError(t, err) + require.NoError(t, err) var after SetStatusOperation err = json.Unmarshal(data, &after) - assert.NoError(t, err) + require.NoError(t, err) // enforce creating the ID before.Id() // Replace the identity stub with the real thing - assert.Equal(t, rene.Id(), after.base().Author.Id()) + require.Equal(t, rene.Id(), after.base().Author.Id()) after.Author = rene - assert.Equal(t, before, &after) + require.Equal(t, before, &after) } diff --git a/bug/op_set_title_test.go b/bug/op_set_title_test.go index df27ee35fd055cc270713f0f76c84c8a5261edfe..6ae325bedde03a597368173da104e11794153282 100644 --- a/bug/op_set_title_test.go +++ b/bug/op_set_title_test.go @@ -9,32 +9,30 @@ import ( "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" - - "github.com/stretchr/testify/assert" ) func TestSetTitleSerialize(t *testing.T) { repo := repository.NewMockRepo() - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := rene.Commit(repo) + + rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") require.NoError(t, err) unix := time.Now().Unix() before := NewSetTitleOp(rene, unix, "title", "was") data, err := json.Marshal(before) - assert.NoError(t, err) + require.NoError(t, err) var after SetTitleOperation err = json.Unmarshal(data, &after) - assert.NoError(t, err) + require.NoError(t, err) // enforce creating the ID before.Id() // Replace the identity stub with the real thing - assert.Equal(t, rene.Id(), after.base().Author.Id()) + require.Equal(t, rene.Id(), after.base().Author.Id()) after.Author = rene - assert.Equal(t, before, &after) + require.Equal(t, before, &after) } diff --git a/bug/operation_iterator_test.go b/bug/operation_iterator_test.go index e066ddd8e13a50e4fed66311f3d880205e33d171..81d87a5fd3de0faf52e77f5e7a6a11c12358a4e2 100644 --- a/bug/operation_iterator_test.go +++ b/bug/operation_iterator_test.go @@ -25,10 +25,11 @@ func ExampleOperationIterator() { } func TestOpIterator(t *testing.T) { - mockRepo := repository.NewMockRepo() + repo := repository.NewMockRepo() - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := rene.Commit(mockRepo) + rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") + require.NoError(t, err) + err = rene.Commit(repo) require.NoError(t, err) unix := time.Now().Unix() @@ -51,14 +52,14 @@ func TestOpIterator(t *testing.T) { bug1.Append(addCommentOp) bug1.Append(setStatusOp) bug1.Append(labelChangeOp) - err = bug1.Commit(mockRepo) + err = bug1.Commit(repo) require.NoError(t, err) // second pack bug1.Append(genTitleOp()) bug1.Append(genTitleOp()) bug1.Append(genTitleOp()) - err = bug1.Commit(mockRepo) + err = bug1.Commit(repo) require.NoError(t, err) // staging diff --git a/bug/operation_pack.go b/bug/operation_pack.go index 1a8ef0dbf65cb7b579643eb974dce1e911af377c..74d15f50888b41aa8e8583fb18d591231590e6b2 100644 --- a/bug/operation_pack.go +++ b/bug/operation_pack.go @@ -12,7 +12,8 @@ import ( // 1: original format // 2: no more legacy identities -const formatVersion = 2 +// 3: Ids are generated from the create operation serialized data instead of from the first git commit +const formatVersion = 3 // OperationPack represent an ordered set of operation to apply // to a Bug. These operations are stored in a single Git commit. @@ -158,13 +159,11 @@ func (opp *OperationPack) Write(repo repository.ClockedRepo) (repository.Hash, e } data, err := json.Marshal(opp) - if err != nil { return "", err } hash, err := repo.StoreData(data) - if err != nil { return "", err } diff --git a/bug/operation_pack_test.go b/bug/operation_pack_test.go index e1388240decd23482606cc1a262fb8747aebedc7..02d72f0f9f847d774f0a98658bdc3374f836c7c5 100644 --- a/bug/operation_pack_test.go +++ b/bug/operation_pack_test.go @@ -5,7 +5,6 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/MichaelMure/git-bug/identity" @@ -16,8 +15,8 @@ func TestOperationPackSerialize(t *testing.T) { opp := &OperationPack{} repo := repository.NewMockRepo() - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := rene.Commit(repo) + + rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") require.NoError(t, err) createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil) @@ -36,7 +35,7 @@ func TestOperationPackSerialize(t *testing.T) { opMeta.SetMetadata("key", "value") opp.Append(opMeta) - assert.Equal(t, 1, len(opMeta.Metadata)) + require.Equal(t, 1, len(opMeta.Metadata)) opFile := NewAddCommentOp(rene, time.Now().Unix(), "message", []repository.Hash{ "abcdef", @@ -44,19 +43,19 @@ func TestOperationPackSerialize(t *testing.T) { }) opp.Append(opFile) - assert.Equal(t, 2, len(opFile.Files)) + require.Equal(t, 2, len(opFile.Files)) data, err := json.Marshal(opp) - assert.NoError(t, err) + require.NoError(t, err) var opp2 *OperationPack err = json.Unmarshal(data, &opp2) - assert.NoError(t, err) + require.NoError(t, err) ensureIds(opp) ensureAuthors(t, opp, opp2) - assert.Equal(t, opp, opp2) + require.Equal(t, opp, opp2) } func ensureIds(opp *OperationPack) { diff --git a/bug/operation_test.go b/bug/operation_test.go index 91e1d936cfbe0c0c5cfdfab509551ab6e59c7487..f66938aded94d29f8941a267fa7b7c77a382fb2d 100644 --- a/bug/operation_test.go +++ b/bug/operation_test.go @@ -11,7 +11,16 @@ import ( ) func TestValidate(t *testing.T) { - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") + repo := repository.NewMockRepoClock() + + makeIdentity := func(t *testing.T, name, email string) *identity.Identity { + i, err := identity.NewIdentity(repo, name, email) + require.NoError(t, err) + return i + } + + rene := makeIdentity(t, "René Descartes", "rene@descartes.fr") + unix := time.Now().Unix() good := []Operation{ @@ -30,11 +39,11 @@ func TestValidate(t *testing.T) { bad := []Operation{ // opbase - 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), + NewSetStatusOp(makeIdentity(t, "", "rene@descartes.fr"), unix, ClosedStatus), + NewSetStatusOp(makeIdentity(t, "René Descartes\u001b", "rene@descartes.fr"), unix, ClosedStatus), + NewSetStatusOp(makeIdentity(t, "René Descartes", "rene@descartes.fr\u001b"), unix, ClosedStatus), + NewSetStatusOp(makeIdentity(t, "René \nDescartes", "rene@descartes.fr"), unix, ClosedStatus), + NewSetStatusOp(makeIdentity(t, "René Descartes", "rene@\ndescartes.fr"), unix, ClosedStatus), &CreateOperation{OpBase: OpBase{ Author: rene, UnixTime: 0, @@ -68,7 +77,11 @@ func TestValidate(t *testing.T) { } func TestMetadata(t *testing.T) { - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") + repo := repository.NewMockRepoClock() + + rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") + require.NoError(t, err) + op := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil) op.SetMetadata("key", "value") @@ -88,8 +101,9 @@ func TestID(t *testing.T) { } for _, repo := range repos { - rene := identity.NewIdentity("René Descartes", "rene@descartes.fr") - err := rene.Commit(repo) + rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") + require.NoError(t, err) + err = rene.Commit(repo) require.NoError(t, err) b, op, err := Create(rene, time.Now().Unix(), "title", "message") diff --git a/entity/refs.go b/entity/refs.go index 82b1741b5ac3581dd25b32b9147a750d7b032a9c..f505dbf01ee99480d9ddc349b1113ff510cf74ed 100644 --- a/entity/refs.go +++ b/entity/refs.go @@ -6,13 +6,13 @@ func RefsToIds(refs []string) []Id { ids := make([]Id, len(refs)) for i, ref := range refs { - ids[i] = refToId(ref) + ids[i] = RefToId(ref) } return ids } -func refToId(ref string) Id { +func RefToId(ref string) Id { split := strings.Split(ref, "/") return Id(split[len(split)-1]) } diff --git a/identity/identity.go b/identity/identity.go index 6352212dbc6539c3b45e626a54d12652384795a3..ef488712ebcc1e99ff08fc8efce0feed08da5d8c 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "reflect" - "strings" "github.com/pkg/errors" @@ -102,8 +101,7 @@ func ReadRemote(repo repository.Repo, remote string, id string) (*Identity, erro // read will load and parse an identity from git func read(repo repository.Repo, ref string) (*Identity, error) { - refSplit := strings.Split(ref, "/") - id := entity.Id(refSplit[len(refSplit)-1]) + id := entity.RefToId(ref) if err := id.Validate(); err != nil { return nil, errors.Wrap(err, "invalid ref") From ab57d74a312f325b9d889752aa92c00c395de20f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 8 Nov 2020 19:18:44 +0100 Subject: [PATCH 05/42] deal with the previous changes --- api/graphql/models/lazy_identity.go | 49 ++---------------- bridge/github/import.go | 2 + bridge/github/import_test.go | 64 +++++++++++------------ bridge/gitlab/import.go | 1 + bridge/gitlab/import_test.go | 72 +++++++++++++------------- bridge/jira/import.go | 1 + bridge/launchpad/import.go | 1 + cache/identity_cache.go | 8 ++- cache/repo_cache.go | 3 +- cache/repo_cache_identity.go | 13 +++-- commands/user.go | 14 +++-- commands/user_create.go | 2 +- doc/man/git-bug-user.1 | 2 +- doc/md/git-bug_user.md | 2 +- entity/id.go | 6 +-- entity/interface.go | 6 +++ go.sum | 1 + misc/powershell_completion/git-bug | 4 +- misc/random_bugs/create_random_bugs.go | 11 ++-- 19 files changed, 124 insertions(+), 138 deletions(-) diff --git a/api/graphql/models/lazy_identity.go b/api/graphql/models/lazy_identity.go index 344bb5f09190f14830c3ad829dd5340621ac5b89..002c38e4c4dc8a28b1001a51e4527d2fd9d9deae 100644 --- a/api/graphql/models/lazy_identity.go +++ b/api/graphql/models/lazy_identity.go @@ -7,8 +7,6 @@ import ( "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/util/lamport" - "github.com/MichaelMure/git-bug/util/timestamp" ) // IdentityWrapper is an interface used by the GraphQL resolvers to handle an identity. @@ -21,11 +19,8 @@ type IdentityWrapper interface { Login() (string, error) AvatarUrl() (string, error) Keys() ([]*identity.Key, error) - ValidKeysAtTime(time lamport.Time) ([]*identity.Key, error) DisplayName() string IsProtected() (bool, error) - LastModificationLamport() (lamport.Time, error) - LastModification() (timestamp.Timestamp, error) } var _ IdentityWrapper = &lazyIdentity{} @@ -69,6 +64,10 @@ func (li *lazyIdentity) Name() string { return li.excerpt.Name } +func (li *lazyIdentity) DisplayName() string { + return li.excerpt.DisplayName() +} + func (li *lazyIdentity) Email() (string, error) { id, err := li.load() if err != nil { @@ -101,18 +100,6 @@ func (li *lazyIdentity) Keys() ([]*identity.Key, error) { return id.Keys(), nil } -func (li *lazyIdentity) ValidKeysAtTime(time lamport.Time) ([]*identity.Key, error) { - id, err := li.load() - if err != nil { - return nil, err - } - return id.ValidKeysAtTime(time), nil -} - -func (li *lazyIdentity) DisplayName() string { - return li.excerpt.DisplayName() -} - func (li *lazyIdentity) IsProtected() (bool, error) { id, err := li.load() if err != nil { @@ -121,22 +108,6 @@ func (li *lazyIdentity) IsProtected() (bool, error) { return id.IsProtected(), nil } -func (li *lazyIdentity) LastModificationLamport() (lamport.Time, error) { - id, err := li.load() - if err != nil { - return 0, err - } - return id.LastModificationLamport(), nil -} - -func (li *lazyIdentity) LastModification() (timestamp.Timestamp, error) { - id, err := li.load() - if err != nil { - return 0, err - } - return id.LastModification(), nil -} - var _ IdentityWrapper = &loadedIdentity{} type loadedIdentity struct { @@ -163,18 +134,6 @@ func (l loadedIdentity) Keys() ([]*identity.Key, error) { return l.Interface.Keys(), nil } -func (l loadedIdentity) ValidKeysAtTime(time lamport.Time) ([]*identity.Key, error) { - return l.Interface.ValidKeysAtTime(time), nil -} - func (l loadedIdentity) IsProtected() (bool, error) { return l.Interface.IsProtected(), nil } - -func (l loadedIdentity) LastModificationLamport() (lamport.Time, error) { - return l.Interface.LastModificationLamport(), nil -} - -func (l loadedIdentity) LastModification() (timestamp.Timestamp, error) { - return l.Interface.LastModification(), nil -} diff --git a/bridge/github/import.go b/bridge/github/import.go index e8a4d3cb9fdb17ae154561187580684496252228..af62746fef1c56eca743ccba9eddb402d90a97b0 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -551,6 +551,7 @@ func (gi *githubImporter) ensurePerson(repo *cache.RepoCache, actor *actor) (*ca email, string(actor.Login), string(actor.AvatarUrl), + nil, map[string]string{ metaKeyGithubLogin: string(actor.Login), }, @@ -598,6 +599,7 @@ func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*cache.IdentityCache, "", string(q.User.Login), string(q.User.AvatarUrl), + nil, map[string]string{ metaKeyGithubLogin: string(q.User.Login), }, diff --git a/bridge/github/import_test.go b/bridge/github/import_test.go index 2295806f1689c5f1e03dbd8b34154a86209e5f5b..3d0004c17a4ae972aa752c3572a11dd644c56528 100644 --- a/bridge/github/import_test.go +++ b/bridge/github/import_test.go @@ -7,7 +7,6 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/MichaelMure/git-bug/bridge/core" @@ -20,7 +19,22 @@ import ( ) func Test_Importer(t *testing.T) { - author := identity.NewIdentity("Michael Muré", "batolettre@gmail.com") + envToken := os.Getenv("GITHUB_TOKEN_PRIVATE") + if envToken == "" { + t.Skip("Env var GITHUB_TOKEN_PRIVATE missing") + } + + repo := repository.CreateGoGitTestRepo(false) + defer repository.CleanupTestRepos(repo) + + backend, err := cache.NewRepoCache(repo) + require.NoError(t, err) + + defer backend.Close() + interrupt.RegisterCleaner(backend.Close) + + author, err := identity.NewIdentity(repo, "Michael Muré", "batolettre@gmail.com") + require.NoError(t, err) tests := []struct { name string @@ -127,20 +141,6 @@ func Test_Importer(t *testing.T) { }, } - repo := repository.CreateGoGitTestRepo(false) - defer repository.CleanupTestRepos(repo) - - backend, err := cache.NewRepoCache(repo) - require.NoError(t, err) - - defer backend.Close() - interrupt.RegisterCleaner(backend.Close) - - envToken := os.Getenv("GITHUB_TOKEN_PRIVATE") - if envToken == "" { - t.Skip("Env var GITHUB_TOKEN_PRIVATE missing") - } - login := "test-identity" author.SetMetadata(metaKeyGithubLogin, login) @@ -178,33 +178,33 @@ func Test_Importer(t *testing.T) { require.NoError(t, err) ops := b.Snapshot().Operations - assert.Len(t, tt.bug.Operations, len(b.Snapshot().Operations)) + require.Len(t, tt.bug.Operations, len(b.Snapshot().Operations)) for i, op := range tt.bug.Operations { require.IsType(t, ops[i], op) switch op.(type) { case *bug.CreateOperation: - assert.Equal(t, op.(*bug.CreateOperation).Title, ops[i].(*bug.CreateOperation).Title) - assert.Equal(t, op.(*bug.CreateOperation).Message, ops[i].(*bug.CreateOperation).Message) - assert.Equal(t, op.(*bug.CreateOperation).Author.Name(), ops[i].(*bug.CreateOperation).Author.Name()) + require.Equal(t, op.(*bug.CreateOperation).Title, ops[i].(*bug.CreateOperation).Title) + require.Equal(t, op.(*bug.CreateOperation).Message, ops[i].(*bug.CreateOperation).Message) + require.Equal(t, op.(*bug.CreateOperation).Author.Name(), ops[i].(*bug.CreateOperation).Author.Name()) case *bug.SetStatusOperation: - assert.Equal(t, op.(*bug.SetStatusOperation).Status, ops[i].(*bug.SetStatusOperation).Status) - assert.Equal(t, op.(*bug.SetStatusOperation).Author.Name(), ops[i].(*bug.SetStatusOperation).Author.Name()) + require.Equal(t, op.(*bug.SetStatusOperation).Status, ops[i].(*bug.SetStatusOperation).Status) + require.Equal(t, op.(*bug.SetStatusOperation).Author.Name(), ops[i].(*bug.SetStatusOperation).Author.Name()) case *bug.SetTitleOperation: - assert.Equal(t, op.(*bug.SetTitleOperation).Was, ops[i].(*bug.SetTitleOperation).Was) - assert.Equal(t, op.(*bug.SetTitleOperation).Title, ops[i].(*bug.SetTitleOperation).Title) - assert.Equal(t, op.(*bug.SetTitleOperation).Author.Name(), ops[i].(*bug.SetTitleOperation).Author.Name()) + require.Equal(t, op.(*bug.SetTitleOperation).Was, ops[i].(*bug.SetTitleOperation).Was) + require.Equal(t, op.(*bug.SetTitleOperation).Title, ops[i].(*bug.SetTitleOperation).Title) + require.Equal(t, op.(*bug.SetTitleOperation).Author.Name(), ops[i].(*bug.SetTitleOperation).Author.Name()) case *bug.LabelChangeOperation: - assert.ElementsMatch(t, op.(*bug.LabelChangeOperation).Added, ops[i].(*bug.LabelChangeOperation).Added) - assert.ElementsMatch(t, op.(*bug.LabelChangeOperation).Removed, ops[i].(*bug.LabelChangeOperation).Removed) - assert.Equal(t, op.(*bug.LabelChangeOperation).Author.Name(), ops[i].(*bug.LabelChangeOperation).Author.Name()) + require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Added, ops[i].(*bug.LabelChangeOperation).Added) + require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Removed, ops[i].(*bug.LabelChangeOperation).Removed) + require.Equal(t, op.(*bug.LabelChangeOperation).Author.Name(), ops[i].(*bug.LabelChangeOperation).Author.Name()) case *bug.AddCommentOperation: - assert.Equal(t, op.(*bug.AddCommentOperation).Message, ops[i].(*bug.AddCommentOperation).Message) - assert.Equal(t, op.(*bug.AddCommentOperation).Author.Name(), ops[i].(*bug.AddCommentOperation).Author.Name()) + require.Equal(t, op.(*bug.AddCommentOperation).Message, ops[i].(*bug.AddCommentOperation).Message) + require.Equal(t, op.(*bug.AddCommentOperation).Author.Name(), ops[i].(*bug.AddCommentOperation).Author.Name()) case *bug.EditCommentOperation: - assert.Equal(t, op.(*bug.EditCommentOperation).Message, ops[i].(*bug.EditCommentOperation).Message) - assert.Equal(t, op.(*bug.EditCommentOperation).Author.Name(), ops[i].(*bug.EditCommentOperation).Author.Name()) + require.Equal(t, op.(*bug.EditCommentOperation).Message, ops[i].(*bug.EditCommentOperation).Message) + require.Equal(t, op.(*bug.EditCommentOperation).Author.Name(), ops[i].(*bug.EditCommentOperation).Author.Name()) default: panic("unknown operation type") diff --git a/bridge/gitlab/import.go b/bridge/gitlab/import.go index cf4f0039e09224b6f5997b14833382d49ba98b7d..7939f4e48708a4d75e91af18cba78c37a4fb7619 100644 --- a/bridge/gitlab/import.go +++ b/bridge/gitlab/import.go @@ -406,6 +406,7 @@ func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.Id user.PublicEmail, user.Username, user.AvatarURL, + nil, map[string]string{ // because Gitlab metaKeyGitlabId: strconv.Itoa(id), diff --git a/bridge/gitlab/import_test.go b/bridge/gitlab/import_test.go index db550f081a68914f5bb0c08889c0dcdc2cac64c9..7e47e149c41a5d868c3d8aef9dee76dfc41c4c8c 100644 --- a/bridge/gitlab/import_test.go +++ b/bridge/gitlab/import_test.go @@ -7,7 +7,6 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/MichaelMure/git-bug/bridge/core" @@ -20,7 +19,27 @@ import ( ) func TestImport(t *testing.T) { - author := identity.NewIdentity("Amine Hilaly", "hilalyamine@gmail.com") + envToken := os.Getenv("GITLAB_API_TOKEN") + if envToken == "" { + t.Skip("Env var GITLAB_API_TOKEN missing") + } + + projectID := os.Getenv("GITLAB_PROJECT_ID") + if projectID == "" { + t.Skip("Env var GITLAB_PROJECT_ID missing") + } + + repo := repository.CreateGoGitTestRepo(false) + defer repository.CleanupTestRepos(repo) + + backend, err := cache.NewRepoCache(repo) + require.NoError(t, err) + + defer backend.Close() + interrupt.RegisterCleaner(backend.Close) + + author, err := identity.NewIdentity(repo, "Amine Hilaly", "hilalyamine@gmail.com") + require.NoError(t, err) tests := []struct { name string @@ -76,25 +95,6 @@ func TestImport(t *testing.T) { }, } - repo := repository.CreateGoGitTestRepo(false) - defer repository.CleanupTestRepos(repo) - - backend, err := cache.NewRepoCache(repo) - require.NoError(t, err) - - defer backend.Close() - interrupt.RegisterCleaner(backend.Close) - - envToken := os.Getenv("GITLAB_API_TOKEN") - if envToken == "" { - t.Skip("Env var GITLAB_API_TOKEN missing") - } - - projectID := os.Getenv("GITLAB_PROJECT_ID") - if projectID == "" { - t.Skip("Env var GITLAB_PROJECT_ID missing") - } - login := "test-identity" author.SetMetadata(metaKeyGitlabLogin, login) @@ -141,26 +141,26 @@ func TestImport(t *testing.T) { switch op.(type) { case *bug.CreateOperation: - assert.Equal(t, op.(*bug.CreateOperation).Title, ops[i].(*bug.CreateOperation).Title) - assert.Equal(t, op.(*bug.CreateOperation).Message, ops[i].(*bug.CreateOperation).Message) - assert.Equal(t, op.(*bug.CreateOperation).Author.Name(), ops[i].(*bug.CreateOperation).Author.Name()) + require.Equal(t, op.(*bug.CreateOperation).Title, ops[i].(*bug.CreateOperation).Title) + require.Equal(t, op.(*bug.CreateOperation).Message, ops[i].(*bug.CreateOperation).Message) + require.Equal(t, op.(*bug.CreateOperation).Author.Name(), ops[i].(*bug.CreateOperation).Author.Name()) case *bug.SetStatusOperation: - assert.Equal(t, op.(*bug.SetStatusOperation).Status, ops[i].(*bug.SetStatusOperation).Status) - assert.Equal(t, op.(*bug.SetStatusOperation).Author.Name(), ops[i].(*bug.SetStatusOperation).Author.Name()) + require.Equal(t, op.(*bug.SetStatusOperation).Status, ops[i].(*bug.SetStatusOperation).Status) + require.Equal(t, op.(*bug.SetStatusOperation).Author.Name(), ops[i].(*bug.SetStatusOperation).Author.Name()) case *bug.SetTitleOperation: - assert.Equal(t, op.(*bug.SetTitleOperation).Was, ops[i].(*bug.SetTitleOperation).Was) - assert.Equal(t, op.(*bug.SetTitleOperation).Title, ops[i].(*bug.SetTitleOperation).Title) - assert.Equal(t, op.(*bug.SetTitleOperation).Author.Name(), ops[i].(*bug.SetTitleOperation).Author.Name()) + require.Equal(t, op.(*bug.SetTitleOperation).Was, ops[i].(*bug.SetTitleOperation).Was) + require.Equal(t, op.(*bug.SetTitleOperation).Title, ops[i].(*bug.SetTitleOperation).Title) + require.Equal(t, op.(*bug.SetTitleOperation).Author.Name(), ops[i].(*bug.SetTitleOperation).Author.Name()) case *bug.LabelChangeOperation: - assert.ElementsMatch(t, op.(*bug.LabelChangeOperation).Added, ops[i].(*bug.LabelChangeOperation).Added) - assert.ElementsMatch(t, op.(*bug.LabelChangeOperation).Removed, ops[i].(*bug.LabelChangeOperation).Removed) - assert.Equal(t, op.(*bug.LabelChangeOperation).Author.Name(), ops[i].(*bug.LabelChangeOperation).Author.Name()) + require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Added, ops[i].(*bug.LabelChangeOperation).Added) + require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Removed, ops[i].(*bug.LabelChangeOperation).Removed) + require.Equal(t, op.(*bug.LabelChangeOperation).Author.Name(), ops[i].(*bug.LabelChangeOperation).Author.Name()) case *bug.AddCommentOperation: - assert.Equal(t, op.(*bug.AddCommentOperation).Message, ops[i].(*bug.AddCommentOperation).Message) - assert.Equal(t, op.(*bug.AddCommentOperation).Author.Name(), ops[i].(*bug.AddCommentOperation).Author.Name()) + require.Equal(t, op.(*bug.AddCommentOperation).Message, ops[i].(*bug.AddCommentOperation).Message) + require.Equal(t, op.(*bug.AddCommentOperation).Author.Name(), ops[i].(*bug.AddCommentOperation).Author.Name()) case *bug.EditCommentOperation: - assert.Equal(t, op.(*bug.EditCommentOperation).Message, ops[i].(*bug.EditCommentOperation).Message) - assert.Equal(t, op.(*bug.EditCommentOperation).Author.Name(), ops[i].(*bug.EditCommentOperation).Author.Name()) + require.Equal(t, op.(*bug.EditCommentOperation).Message, ops[i].(*bug.EditCommentOperation).Message) + require.Equal(t, op.(*bug.EditCommentOperation).Author.Name(), ops[i].(*bug.EditCommentOperation).Author.Name()) default: panic("unknown operation type") diff --git a/bridge/jira/import.go b/bridge/jira/import.go index b66b0fa346b6e0062eeb6eeb393c3e475f11bc84..00148bb62bbe2f2d9f22b0b570e44ee0a095dce2 100644 --- a/bridge/jira/import.go +++ b/bridge/jira/import.go @@ -196,6 +196,7 @@ func (ji *jiraImporter) ensurePerson(repo *cache.RepoCache, user User) (*cache.I user.EmailAddress, user.Key, "", + nil, map[string]string{ metaKeyJiraUser: user.Key, }, diff --git a/bridge/launchpad/import.go b/bridge/launchpad/import.go index ce50828b292b3c310c6b121a0258ef1a00701a2d..6b5667bae37b98f2bb666c90b9221936799245c4 100644 --- a/bridge/launchpad/import.go +++ b/bridge/launchpad/import.go @@ -35,6 +35,7 @@ func (li *launchpadImporter) ensurePerson(repo *cache.RepoCache, owner LPPerson) "", owner.Login, "", + nil, map[string]string{ metaKeyLaunchpadLogin: owner.Login, }, diff --git a/cache/identity_cache.go b/cache/identity_cache.go index 25e273b943f3e153e875757374bfc4dfa8e9d921..e419387f22569964a71afddcbf898af204c6e080 100644 --- a/cache/identity_cache.go +++ b/cache/identity_cache.go @@ -2,6 +2,7 @@ package cache import ( "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/repository" ) var _ identity.Interface = &IdentityCache{} @@ -23,8 +24,11 @@ func (i *IdentityCache) notifyUpdated() error { return i.repoCache.identityUpdated(i.Identity.Id()) } -func (i *IdentityCache) Mutate(f func(identity.Mutator) identity.Mutator) error { - i.Identity.Mutate(f) +func (i *IdentityCache) Mutate(repo repository.RepoClock, f func(*identity.Mutator)) error { + err := i.Identity.Mutate(repo, f) + if err != nil { + return err + } return i.notifyUpdated() } diff --git a/cache/repo_cache.go b/cache/repo_cache.go index b5b9ee543992e7077128af0572401d927f31f801..ab3e1bcb6b320085b0605f0cc0f49a902f692a27 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -18,7 +18,8 @@ import ( // 1: original format // 2: added cache for identities with a reference in the bug cache // 3: no more legacy identity -const formatVersion = 3 +// 4: entities make their IDs from data, not git commit +const formatVersion = 4 // The maximum number of bugs loaded in memory. After that, eviction will be done. const defaultMaxLoadedBugs = 1000 diff --git a/cache/repo_cache_identity.go b/cache/repo_cache_identity.go index 8df5b810b29c90546767f3dad0cc42be76345b40..75453cb8803b4ad1de638db189d3867731c40334 100644 --- a/cache/repo_cache_identity.go +++ b/cache/repo_cache_identity.go @@ -225,17 +225,20 @@ func (c *RepoCache) NewIdentityFromGitUserRaw(metadata map[string]string) (*Iden // NewIdentity create a new identity // The new identity is written in the repository (commit) func (c *RepoCache) NewIdentity(name string, email string) (*IdentityCache, error) { - return c.NewIdentityRaw(name, email, "", "", nil) + return c.NewIdentityRaw(name, email, "", "", nil, nil) } // NewIdentityFull create a new identity // The new identity is written in the repository (commit) -func (c *RepoCache) NewIdentityFull(name string, email string, login string, avatarUrl string) (*IdentityCache, error) { - return c.NewIdentityRaw(name, email, login, avatarUrl, nil) +func (c *RepoCache) NewIdentityFull(name string, email string, login string, avatarUrl string, keys []*identity.Key) (*IdentityCache, error) { + return c.NewIdentityRaw(name, email, login, avatarUrl, keys, nil) } -func (c *RepoCache) NewIdentityRaw(name string, email string, login string, avatarUrl string, metadata map[string]string) (*IdentityCache, error) { - i := identity.NewIdentityFull(name, email, login, avatarUrl) +func (c *RepoCache) NewIdentityRaw(name string, email string, login string, avatarUrl string, keys []*identity.Key, metadata map[string]string) (*IdentityCache, error) { + i, err := identity.NewIdentityFull(c.repo, name, email, login, avatarUrl, keys) + if err != nil { + return nil, err + } return c.finishIdentity(i, metadata) } diff --git a/commands/user.go b/commands/user.go index d4d3fecdb04695bc26f7103db68d8a17df50c94b..29c4e932016af3504698ba3ddb658dda4d65e04f 100644 --- a/commands/user.go +++ b/commands/user.go @@ -35,7 +35,7 @@ func newUserCommand() *cobra.Command { flags.SortFlags = false flags.StringVarP(&options.fields, "field", "f", "", - "Select field to display. Valid values are [email,humanId,id,lastModification,lastModificationLamport,login,metadata,name]") + "Select field to display. Valid values are [email,humanId,id,lastModification,lastModificationLamports,login,metadata,name]") return cmd } @@ -71,7 +71,9 @@ func runUser(env *Env, opts userOptions, args []string) error { env.out.Printf("%s\n", id.LastModification(). Time().Format("Mon Jan 2 15:04:05 2006 +0200")) case "lastModificationLamport": - env.out.Printf("%d\n", id.LastModificationLamport()) + for name, t := range id.LastModificationLamports() { + env.out.Printf("%s\n%d\n", name, t) + } case "metadata": for key, value := range id.ImmutableMetadata() { env.out.Printf("%s\n%s\n", key, value) @@ -90,9 +92,11 @@ func runUser(env *Env, opts userOptions, args []string) error { env.out.Printf("Name: %s\n", id.Name()) env.out.Printf("Email: %s\n", id.Email()) env.out.Printf("Login: %s\n", id.Login()) - env.out.Printf("Last modification: %s (lamport %d)\n", - id.LastModification().Time().Format("Mon Jan 2 15:04:05 2006 +0200"), - id.LastModificationLamport()) + env.out.Printf("Last modification: %s\n", id.LastModification().Time().Format("Mon Jan 2 15:04:05 2006 +0200")) + env.out.Printf("Last moditication (lamport):\n") + for name, t := range id.LastModificationLamports() { + env.out.Printf("\t%s: %d", name, t) + } env.out.Println("Metadata:") for key, value := range id.ImmutableMetadata() { env.out.Printf(" %s --> %s\n", key, value) diff --git a/commands/user_create.go b/commands/user_create.go index 3da712f3c837df9a462a1a3a20ab55be4ba610ee..5203d11cf98d90dff829318c62f6a3f7fcf2b76e 100644 --- a/commands/user_create.go +++ b/commands/user_create.go @@ -48,7 +48,7 @@ func runUserCreate(env *Env) error { return err } - id, err := env.backend.NewIdentityRaw(name, email, "", avatarURL, nil) + id, err := env.backend.NewIdentityRaw(name, email, "", avatarURL, nil, nil) if err != nil { return err } diff --git a/doc/man/git-bug-user.1 b/doc/man/git-bug-user.1 index 3992543332d05eb2675d4256b8f35d43a7d5ff15..c4ca0e5488a2477dc9941fe18626b9b86f93106f 100644 --- a/doc/man/git-bug-user.1 +++ b/doc/man/git-bug-user.1 @@ -19,7 +19,7 @@ Display or change the user identity. .SH OPTIONS .PP \fB\-f\fP, \fB\-\-field\fP="" - Select field to display. Valid values are [email,humanId,id,lastModification,lastModificationLamport,login,metadata,name] + Select field to display. Valid values are [email,humanId,id,lastModification,lastModificationLamports,login,metadata,name] .PP \fB\-h\fP, \fB\-\-help\fP[=false] diff --git a/doc/md/git-bug_user.md b/doc/md/git-bug_user.md index d9388def22b94e19a489e1738892fa8dd5fdf16d..302a1eda8e0cfb6f0c3309fb14ac0a94d83bfa47 100644 --- a/doc/md/git-bug_user.md +++ b/doc/md/git-bug_user.md @@ -9,7 +9,7 @@ git-bug user [USER-ID] [flags] ### Options ``` - -f, --field string Select field to display. Valid values are [email,humanId,id,lastModification,lastModificationLamport,login,metadata,name] + -f, --field string Select field to display. Valid values are [email,humanId,id,lastModification,lastModificationLamports,login,metadata,name] -h, --help help for user ``` diff --git a/entity/id.go b/entity/id.go index 1b78aacd3946ccf9bb747fbd6585859e8231edcd..08916987fe2a6d6690e2f78fc48655fdda9058b1 100644 --- a/entity/id.go +++ b/entity/id.go @@ -8,8 +8,8 @@ import ( "github.com/pkg/errors" ) -const IdLengthSHA1 = 40 -const IdLengthSHA256 = 64 +// sha-256 +const idLength = 64 const humanIdLength = 7 const UnsetId = Id("unset") @@ -55,7 +55,7 @@ func (i Id) MarshalGQL(w io.Writer) { // IsValid tell if the Id is valid func (i Id) Validate() error { - if len(i) != IdLengthSHA1 && len(i) != IdLengthSHA256 { + if len(i) != idLength { return fmt.Errorf("invalid length") } for _, r := range i { diff --git a/entity/interface.go b/entity/interface.go index dd5d69b1edf13a2a80df417ab4f66ac780af7864..fb4735e444d5f54934ec37133ea61c1ab74db171 100644 --- a/entity/interface.go +++ b/entity/interface.go @@ -2,5 +2,11 @@ package entity type Interface interface { // Id return the Entity identifier + // + // This Id need to be immutable without having to store the entity somewhere (ie, an entity only in memory + // should have a valid Id, and it should not change if further edit are done on this entity). + // How to achieve that is up to the entity itself. A common way would be to take a hash of an immutable data at + // the root of the entity. + // It is acceptable to use such a hash and keep mutating that data as long as Id() is not called. Id() Id } diff --git a/go.sum b/go.sum index c6875b9a514d286b2865d81cba4c5a6d49b88a54..9d0a8c825ce06b1577af362396ef22f3ea585eb5 100644 --- a/go.sum +++ b/go.sum @@ -727,6 +727,7 @@ golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/misc/powershell_completion/git-bug b/misc/powershell_completion/git-bug index c2aa0adf8bd018ac92e9c6e445362cd56b1cb9eb..29c282378441bba92d7974c442015ec2a256480c 100644 --- a/misc/powershell_completion/git-bug +++ b/misc/powershell_completion/git-bug @@ -212,8 +212,8 @@ Register-ArgumentCompleter -Native -CommandName 'git-bug' -ScriptBlock { break } 'git-bug;user' { - [CompletionResult]::new('-f', 'f', [CompletionResultType]::ParameterName, 'Select field to display. Valid values are [email,humanId,id,lastModification,lastModificationLamport,login,metadata,name]') - [CompletionResult]::new('--field', 'field', [CompletionResultType]::ParameterName, 'Select field to display. Valid values are [email,humanId,id,lastModification,lastModificationLamport,login,metadata,name]') + [CompletionResult]::new('-f', 'f', [CompletionResultType]::ParameterName, 'Select field to display. Valid values are [email,humanId,id,lastModification,lastModificationLamports,login,metadata,name]') + [CompletionResult]::new('--field', 'field', [CompletionResultType]::ParameterName, 'Select field to display. Valid values are [email,humanId,id,lastModification,lastModificationLamports,login,metadata,name]') [CompletionResult]::new('adopt', 'adopt', [CompletionResultType]::ParameterValue, 'Adopt an existing identity as your own.') [CompletionResult]::new('create', 'create', [CompletionResultType]::ParameterValue, 'Create a new identity.') [CompletionResult]::new('ls', 'ls', [CompletionResultType]::ParameterValue, 'List identities.') diff --git a/misc/random_bugs/create_random_bugs.go b/misc/random_bugs/create_random_bugs.go index 2de77722f8caa4f88dec9f8e08d95043ab3389ee..3d93135ece4bf3b78e28c2cdbd72234525f1669f 100644 --- a/misc/random_bugs/create_random_bugs.go +++ b/misc/random_bugs/create_random_bugs.go @@ -157,8 +157,8 @@ func GenerateRandomOperationPacksWithSeed(packNumber int, opNumber int, seed int return result } -func person() *identity.Identity { - return identity.NewIdentity(fake.FullName(), fake.EmailAddress()) +func person(repo repository.RepoClock) (*identity.Identity, error) { + return identity.NewIdentity(repo, fake.FullName(), fake.EmailAddress()) } var persons []*identity.Identity @@ -166,8 +166,11 @@ var persons []*identity.Identity func generateRandomPersons(repo repository.ClockedRepo, n int) { persons = make([]*identity.Identity, n) for i := range persons { - p := person() - err := p.Commit(repo) + p, err := person(repo) + if err != nil { + panic(err) + } + err = p.Commit(repo) if err != nil { panic(err) } From b01aa18d3925a23ba0ad32a322617de7dc9a299e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 8 Nov 2020 23:56:32 +0100 Subject: [PATCH 06/42] identity: PR fixes --- bridge/github/export_test.go | 2 +- bridge/github/import_test.go | 2 +- bridge/gitlab/export_test.go | 2 +- bridge/gitlab/import_test.go | 2 +- identity/identity_test.go | 10 ++++++++++ identity/version.go | 9 ++++++--- 6 files changed, 20 insertions(+), 7 deletions(-) diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go index 5b9a349581b1d90e94c46e74adc8865cc6a909c6..b7a36bcf7e82137de82567f142c637ef747728cf 100644 --- a/bridge/github/export_test.go +++ b/bridge/github/export_test.go @@ -126,7 +126,7 @@ func testCases(t *testing.T, repo *cache.RepoCache) []*testCase { } } -func TestPushPull(t *testing.T) { +func TestGithubPushPull(t *testing.T) { // repo owner envUser := os.Getenv("GITHUB_TEST_USER") diff --git a/bridge/github/import_test.go b/bridge/github/import_test.go index 3d0004c17a4ae972aa752c3572a11dd644c56528..84bf774e50d335d77eab6992aac8d412ab0bf0d3 100644 --- a/bridge/github/import_test.go +++ b/bridge/github/import_test.go @@ -18,7 +18,7 @@ import ( "github.com/MichaelMure/git-bug/util/interrupt" ) -func Test_Importer(t *testing.T) { +func TestGithubImporter(t *testing.T) { envToken := os.Getenv("GITHUB_TOKEN_PRIVATE") if envToken == "" { t.Skip("Env var GITHUB_TOKEN_PRIVATE missing") diff --git a/bridge/gitlab/export_test.go b/bridge/gitlab/export_test.go index 58f3d63cd995d7ec098ed7b293c27dba12201fc3..88b0d44eb5d71e6157567d4bd94feaca093be7fb 100644 --- a/bridge/gitlab/export_test.go +++ b/bridge/gitlab/export_test.go @@ -134,7 +134,7 @@ func testCases(t *testing.T, repo *cache.RepoCache) []*testCase { } } -func TestPushPull(t *testing.T) { +func TestGitlabPushPull(t *testing.T) { // token must have 'repo' and 'delete_repo' scopes envToken := os.Getenv("GITLAB_API_TOKEN") if envToken == "" { diff --git a/bridge/gitlab/import_test.go b/bridge/gitlab/import_test.go index 7e47e149c41a5d868c3d8aef9dee76dfc41c4c8c..2956ad8b68e866c06239a5fb964f002c6ac4c29e 100644 --- a/bridge/gitlab/import_test.go +++ b/bridge/gitlab/import_test.go @@ -18,7 +18,7 @@ import ( "github.com/MichaelMure/git-bug/util/interrupt" ) -func TestImport(t *testing.T) { +func TestGitlabImport(t *testing.T) { envToken := os.Getenv("GITLAB_API_TOKEN") if envToken == "" { t.Skip("Env var GITLAB_API_TOKEN missing") diff --git a/identity/identity_test.go b/identity/identity_test.go index 36d07be6a5fa8d3fdd888696c2b9797d7d0d834a..ad8317ce4fc3101c6168905a84e0f6238d6c5410 100644 --- a/identity/identity_test.go +++ b/identity/identity_test.go @@ -204,6 +204,16 @@ func TestMetadata(t *testing.T) { assertHasKeyValue(t, loaded.ImmutableMetadata(), "key1", "value1") assertHasKeyValue(t, loaded.MutableMetadata(), "key1", "value2") + + // set metadata after commit + versionCount := len(identity.versions) + identity.SetMetadata("foo", "bar") + require.True(t, identity.NeedCommit()) + require.Len(t, identity.versions, versionCount+1) + + err = identity.Commit(repo) + require.NoError(t, err) + require.Len(t, identity.versions, versionCount+1) } func assertHasKeyValue(t *testing.T, metadata map[string]string, key, value string) { diff --git a/identity/version.go b/identity/version.go index bbf0a3f5ec2ea0c0da3c45e8d53ca4c7afe68fc4..ae2474bf3650c089245ec83dd2548d152b0b8754 100644 --- a/identity/version.go +++ b/identity/version.go @@ -18,10 +18,9 @@ import ( // 1: original format // 2: Identity Ids are generated from the first version serialized data instead of from the first git commit +// + Identity hold multiple lamport clocks from other entities, instead of just bug edit const formatVersion = 2 -// TODO ^^ - // version is a complete set of information about an Identity at a point in time. type version struct { name string @@ -42,7 +41,7 @@ type version struct { // version of a bug, used to later generate the ID // len(Nonce) should be > 20 and < 64 bytes // It has no functional purpose and should be ignored. - // TODO: optional? + // TODO: optional after first version? nonce []byte // A set of arbitrary key/value to store metadata about a version or about an Identity in general. @@ -122,6 +121,10 @@ func (v *version) Clone() *version { // copy direct fields clone := *v + // reset some fields + clone.commitHash = "" + clone.id = entity.UnsetId + clone.times = make(map[string]lamport.Time) for name, t := range v.times { clone.times[name] = t From 2bf2b2d765c5003307544885b9321b32cc09d8bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Mon, 9 Nov 2020 00:34:48 +0100 Subject: [PATCH 07/42] entity: unique function to generate IDs --- bug/operation.go | 10 ++-------- entity/id.go | 14 ++++++++++++++ identity/version.go | 12 +++--------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/bug/operation.go b/bug/operation.go index 107c954ecfdf28304a1bcf4730305145075d8cdc..bdaa2016e6ee77c4ee593a85df7c7f26c5c3420c 100644 --- a/bug/operation.go +++ b/bug/operation.go @@ -1,7 +1,6 @@ package bug import ( - "crypto/sha256" "encoding/json" "fmt" "time" @@ -55,11 +54,6 @@ type Operation interface { IsOperation() } -func deriveId(data []byte) entity.Id { - sum := sha256.Sum256(data) - return entity.Id(fmt.Sprintf("%x", sum)) -} - func idOperation(op Operation) entity.Id { base := op.base() @@ -78,7 +72,7 @@ func idOperation(op Operation) entity.Id { panic(err) } - base.id = deriveId(data) + base.id = entity.DeriveId(data) } return base.id } @@ -109,7 +103,7 @@ func newOpBase(opType OperationType, author identity.Interface, unixTime int64) func (op *OpBase) UnmarshalJSON(data []byte) error { // Compute the Id when loading the op from disk. - op.id = deriveId(data) + op.id = entity.DeriveId(data) aux := struct { OperationType OperationType `json:"type"` diff --git a/entity/id.go b/entity/id.go index 08916987fe2a6d6690e2f78fc48655fdda9058b1..9e724012783807e555326fd990b813b8d2c59540 100644 --- a/entity/id.go +++ b/entity/id.go @@ -1,6 +1,7 @@ package entity import ( + "crypto/sha256" "fmt" "io" "strings" @@ -17,6 +18,15 @@ const UnsetId = Id("unset") // Id is an identifier for an entity or part of an entity type Id string +// DeriveId generate an Id from some data, taken from a root part of the entity. +func DeriveId(data []byte) Id { + // My understanding is that sha256 is enough to prevent collision (git use that, so ...?) + // If you read this code, I'd be happy to be schooled. + + sum := sha256.Sum256(data) + return Id(fmt.Sprintf("%x", sum)) +} + // String return the identifier as a string func (i Id) String() string { return string(i) @@ -55,6 +65,10 @@ func (i Id) MarshalGQL(w io.Writer) { // IsValid tell if the Id is valid func (i Id) Validate() error { + // Special case to + if len(i) == 40 { + return fmt.Errorf("outdated repository format, please use https://github.com/MichaelMure/git-bug-migration to upgrade") + } if len(i) != idLength { return fmt.Errorf("invalid length") } diff --git a/identity/version.go b/identity/version.go index ae2474bf3650c089245ec83dd2548d152b0b8754..ce5cc7d615443038bca0fbf966c862b61ca999ae 100644 --- a/identity/version.go +++ b/identity/version.go @@ -2,7 +2,6 @@ package identity import ( "crypto/rand" - "crypto/sha256" "encoding/json" "fmt" "strings" @@ -106,16 +105,11 @@ func (v *version) Id() entity.Id { if err != nil { panic(err) } - v.id = deriveId(data) + v.id = entity.DeriveId(data) } return v.id } -func deriveId(data []byte) entity.Id { - sum := sha256.Sum256(data) - return entity.Id(fmt.Sprintf("%x", sum)) -} - // Make a deep copy func (v *version) Clone() *version { // copy direct fields @@ -172,7 +166,7 @@ func (v *version) UnmarshalJSON(data []byte) error { return entity.NewErrNewFormatVersion(aux.FormatVersion) } - v.id = deriveId(data) + v.id = entity.DeriveId(data) v.times = aux.Times v.unixTime = aux.UnixTime v.name = aux.Name @@ -256,7 +250,7 @@ func (v *version) Write(repo repository.Repo) (repository.Hash, error) { } // make sure we set the Id when writing in the repo - v.id = deriveId(data) + v.id = entity.DeriveId(data) return hash, nil } From 497ec1376ab510af740910ed9c99b159809cf0e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Mon, 9 Nov 2020 00:35:06 +0100 Subject: [PATCH 08/42] bug: debug --- bug/bug.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bug/bug.go b/bug/bug.go index e67920f9aa40fe60837899300051401d60861de3..86227c6ba2892e399b2f102b43c774fa93cf99dc 100644 --- a/bug/bug.go +++ b/bug/bug.go @@ -321,6 +321,8 @@ func (bug *Bug) Validate() error { // The bug Id should be the id of the first operation if bug.FirstOp().Id() != bug.id { + fmt.Println("bug", bug.id.String()) + fmt.Println("op", bug.FirstOp().Id().String()) return fmt.Errorf("bug id should be the first commit hash") } From 2788c5fc87507974d3237d4edc233fda3f784b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 29 Nov 2020 20:22:09 +0100 Subject: [PATCH 09/42] bug: don't store the id in Bug, match how it's done for Identity --- bug/bug.go | 42 +++++++++--------------------------------- bug/op_create.go | 17 ++++++++++++++++- bug/with_snapshot.go | 2 +- 3 files changed, 26 insertions(+), 35 deletions(-) diff --git a/bug/bug.go b/bug/bug.go index 86227c6ba2892e399b2f102b43c774fa93cf99dc..fb36bfd859673395759b9fa688b7e64b50b76419 100644 --- a/bug/bug.go +++ b/bug/bug.go @@ -44,16 +44,12 @@ var _ entity.Interface = &Bug{} // how it will be persisted inside Git. This is the data structure // used to merge two different version of the same Bug. type Bug struct { - // A Lamport clock is a logical clock that allow to order event // inside a distributed system. // It must be the first field in this struct due to https://github.com/golang/go/issues/599 createTime lamport.Time editTime lamport.Time - // Id used as unique identifier - id entity.Id - lastCommit repository.Hash // all the committed operations @@ -66,9 +62,8 @@ type Bug struct { // NewBug create a new Bug func NewBug() *Bug { - // No id yet // No logical clock yet - return &Bug{id: entity.UnsetId} + return &Bug{} } // ReadLocal will read a local bug from its hash @@ -111,9 +106,7 @@ func read(repo repository.ClockedRepo, identityResolver identity.Resolver, ref s return nil, fmt.Errorf("empty bug") } - bug := Bug{ - id: id, - } + bug := Bug{} // Load each OperationPack for _, hash := range hashes { @@ -164,7 +157,7 @@ func read(repo repository.ClockedRepo, identityResolver identity.Resolver, ref s if len(bug.packs[0].Operations) == 0 { return nil, fmt.Errorf("first OperationPack is empty") } - if bug.id != bug.packs[0].Operations[0].Id() { + if id != bug.packs[0].Operations[0].Id() { return nil, fmt.Errorf("bug ID doesn't match the first operation ID") } @@ -319,13 +312,6 @@ func (bug *Bug) Validate() error { return fmt.Errorf("first operation should be a Create op") } - // The bug Id should be the id of the first operation - if bug.FirstOp().Id() != bug.id { - fmt.Println("bug", bug.id.String()) - fmt.Println("op", bug.FirstOp().Id().String()) - return fmt.Errorf("bug id should be the first commit hash") - } - // Check that there is no more CreateOp op // Check that there is no colliding operation's ID it := NewOperationIterator(bug) @@ -354,7 +340,6 @@ func (bug *Bug) Append(op Operation) { if op.base().OperationType != CreateOp { panic("first operation should be a Create") } - bug.id = op.Id() } bug.staging.Append(op) } @@ -454,15 +439,10 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error { bug.packs = append(bug.packs, bug.staging) bug.staging = OperationPack{} - // if it was the first commit, use the Id of the first op (create) - if bug.id == "" || bug.id == entity.UnsetId { - bug.id = bug.packs[0].Operations[0].Id() - } - // Create or update the Git reference for this bug // When pushing later, the remote will ensure that this ref update // is fast-forward, that is no data has been overwritten - ref := fmt.Sprintf("%s%s", bugsRefPattern, bug.id) + ref := fmt.Sprintf("%s%s", bugsRefPattern, bug.Id().String()) return repo.UpdateRef(ref, hash) } @@ -488,7 +468,7 @@ func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) { // Reading the other side is still necessary to validate remote data, at least // for new operations - if bug.id != otherBug.id { + if bug.Id() != otherBug.Id() { return false, errors.New("merging unrelated bugs is not supported") } @@ -562,7 +542,7 @@ func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) { bug.packs = newPacks // Update the git ref - err = repo.UpdateRef(bugsRefPattern+bug.id.String(), bug.lastCommit) + err = repo.UpdateRef(bugsRefPattern+bug.Id().String(), bug.lastCommit) if err != nil { return false, err } @@ -572,12 +552,8 @@ func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) { // Id return the Bug identifier func (bug *Bug) Id() entity.Id { - if bug.id == "" || bug.id == entity.UnsetId { - // simply panic as it would be a coding error - // (using an id of a bug without operation yet) - panic("no id yet") - } - return bug.id + // id is the id of the first operation + return bug.FirstOp().Id() } // CreateLamportTime return the Lamport time of creation @@ -629,7 +605,7 @@ func (bug *Bug) LastOp() Operation { // Compile a bug in a easily usable snapshot func (bug *Bug) Compile() Snapshot { snap := Snapshot{ - id: bug.id, + id: bug.Id(), Status: OpenStatus, } diff --git a/bug/op_create.go b/bug/op_create.go index 3c8ce6587c9f3d0003f3a50a29e784b6dd7b85c5..41e0fca1960aa2519e010857694979800fc8f098 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -38,6 +38,21 @@ func (op *CreateOperation) Id() entity.Id { return idOperation(op) } +// OVERRIDE +func (op *CreateOperation) SetMetadata(key string, value string) { + // sanity check: we make sure we are not in the following scenario: + // - the bug is created with a first operation + // - Id() is used + // - metadata are added, which will change the Id + // - Id() is used again + + if op.id != entity.UnsetId { + panic("usage of Id() after changing the first operation") + } + + op.OpBase.SetMetadata(key, value) +} + func (op *CreateOperation) Apply(snapshot *Snapshot) { snapshot.addActor(op.Author) snapshot.addParticipant(op.Author) @@ -95,7 +110,7 @@ func (op *CreateOperation) Validate() error { return nil } -// UnmarshalJSON is a two step JSON unmarshaling +// UnmarshalJSON is a two step JSON unmarshalling // This workaround is necessary to avoid the inner OpBase.MarshalJSON // overriding the outer op's MarshalJSON func (op *CreateOperation) UnmarshalJSON(data []byte) error { diff --git a/bug/with_snapshot.go b/bug/with_snapshot.go index 2b2439df85861a33c76598031797efc86c05628a..41192d3965964cb5831b78c689a2ea57f4d579e3 100644 --- a/bug/with_snapshot.go +++ b/bug/with_snapshot.go @@ -47,7 +47,7 @@ func (b *WithSnapshot) Commit(repo repository.ClockedRepo) error { return nil } - b.snap.id = b.Bug.id + b.snap.id = b.Bug.Id() return nil } From d96284da646cc1d3e3d7d3b2f7a1ab0e8e7a4d88 Mon Sep 17 00:00:00 2001 From: vince Date: Thu, 9 Jul 2020 14:59:47 +0800 Subject: [PATCH 10/42] Change the comment ID to use both bug and comment ID references. Add comment edit command This commit adds the comment edit command, which provides a CLI tool that allows a user to edit a comment. --- bug/comment.go | 46 +++++++++++++++++++ bug/comment_test.go | 27 ++++++++++++ bug/op_add_comment.go | 5 ++- bug/op_create.go | 5 ++- bug/snapshot.go | 5 +++ cache/repo_cache_bug.go | 47 ++++++++++++++++++++ commands/comment.go | 1 + commands/comment_edit.go | 71 ++++++++++++++++++++++++++++++ commands/show.go | 3 +- doc/man/git-bug-comment-edit.1 | 35 +++++++++++++++ doc/man/git-bug-comment.1 | 2 +- doc/md/git-bug_comment.md | 1 + doc/md/git-bug_comment_edit.md | 20 +++++++++ misc/bash_completion/git-bug | 33 ++++++++++++++ misc/powershell_completion/git-bug | 8 ++++ 15 files changed, 303 insertions(+), 6 deletions(-) create mode 100644 bug/comment_test.go create mode 100644 commands/comment_edit.go create mode 100644 doc/man/git-bug-comment-edit.1 create mode 100644 doc/md/git-bug_comment_edit.md diff --git a/bug/comment.go b/bug/comment.go index 4c9d118ebb4cb79f07dc953aedaa3748fd63a2fb..1a9ca05af7ded8ffcdd689f2fce3fb26b37a36fa 100644 --- a/bug/comment.go +++ b/bug/comment.go @@ -1,6 +1,8 @@ package bug import ( + "strings" + "github.com/dustin/go-humanize" "github.com/MichaelMure/git-bug/entity" @@ -31,6 +33,50 @@ func (c Comment) Id() entity.Id { return c.id } +const compiledCommentIdFormat = "BCBCBCBBBCBBBBCBBBBCBBBBCBBBBCBBBBCBBBBC" + +// DeriveCommentId compute a merged Id for a comment holding information from +// both the Bug's Id and the Comment's Id. This allow to later find efficiently +// a Comment because we can access the bug directly instead of searching for a +// Bug that has a Comment matching the Id. +// +// To allow the use of an arbitrary length prefix of this merged Id, Ids from Bug +// and Comment are interleaved with this irregular pattern to give the best chance +// to find the Comment even with a 7 character prefix. +// +// A complete merged Id hold 30 characters for the Bug and 10 for the Comment, +// which give a key space of 36^30 for the Bug (~5 * 10^46) and 36^10 for the +// Comment (~3 * 10^15). This asymmetry assume a reasonable number of Comment +// within a Bug, while still allowing for a vast key space for Bug (that is, a +// globally merged bug database) with a low risk of collision. +func DeriveCommentId(bugId entity.Id, commentId entity.Id) entity.Id { + var id strings.Builder + for _, char := range compiledCommentIdFormat { + if char == 'B' { + id.WriteByte(bugId[0]) + bugId = bugId[1:] + } else { + id.WriteByte(commentId[0]) + commentId = commentId[1:] + } + } + return entity.Id(id.String()) +} + +func SplitCommentId(prefix string) (bugPrefix string, commentPrefix string) { + var bugIdPrefix strings.Builder + var commentIdPrefix strings.Builder + + for i, char := range prefix { + if compiledCommentIdFormat[i] == 'B' { + bugIdPrefix.WriteRune(char) + } else { + commentIdPrefix.WriteRune(char) + } + } + return bugIdPrefix.String(), commentIdPrefix.String() +} + // FormatTimeRel format the UnixTime of the comment for human consumption func (c Comment) FormatTimeRel() string { return humanize.Time(c.UnixTime.Time()) diff --git a/bug/comment_test.go b/bug/comment_test.go new file mode 100644 index 0000000000000000000000000000000000000000..423d10d8fb8c47ceb474d3d5bb558fbfabd59af8 --- /dev/null +++ b/bug/comment_test.go @@ -0,0 +1,27 @@ +package bug + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/entity" +) + +func TestCommentId(t *testing.T) { + bugId := entity.Id("abcdefghijklmnopqrstuvwxyz1234__________") + opId := entity.Id("ABCDEFGHIJ______________________________") + expectedId := entity.Id("aAbBcCdefDghijEklmnFopqrGstuvHwxyzI1234J") + + mergedId := DeriveCommentId(bugId, opId) + require.Equal(t, expectedId, mergedId) + + // full length + splitBugId, splitCommentId := SplitCommentId(mergedId.String()) + require.Equal(t, string(bugId[:30]), splitBugId) + require.Equal(t, string(opId[:10]), splitCommentId) + + splitBugId, splitCommentId = SplitCommentId(string(expectedId[:6])) + require.Equal(t, string(bugId[:3]), splitBugId) + require.Equal(t, string(opId[:3]), splitCommentId) +} diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go index 3f19e42e784185336811160dae437e7c527fff3b..df426ee0c5c02c2780f1f25ac34c1b8bb289dec9 100644 --- a/bug/op_add_comment.go +++ b/bug/op_add_comment.go @@ -36,8 +36,9 @@ func (op *AddCommentOperation) Apply(snapshot *Snapshot) { snapshot.addActor(op.Author) snapshot.addParticipant(op.Author) + commentId := DeriveCommentId(snapshot.Id(), op.Id()) comment := Comment{ - id: op.Id(), + id: commentId, Message: op.Message, Author: op.Author, Files: op.Files, @@ -47,7 +48,7 @@ func (op *AddCommentOperation) Apply(snapshot *Snapshot) { snapshot.Comments = append(snapshot.Comments, comment) item := &AddCommentTimelineItem{ - CommentTimelineItem: NewCommentTimelineItem(op.Id(), comment), + CommentTimelineItem: NewCommentTimelineItem(commentId, comment), } snapshot.Timeline = append(snapshot.Timeline, item) diff --git a/bug/op_create.go b/bug/op_create.go index 41e0fca1960aa2519e010857694979800fc8f098..15fb69b5533f2fe96756018da17668da3648915a 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -59,8 +59,9 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) { snapshot.Title = op.Title + commentId := DeriveCommentId(snapshot.Id(), op.Id()) comment := Comment{ - id: op.Id(), + id: commentId, Message: op.Message, Author: op.Author, UnixTime: timestamp.Timestamp(op.UnixTime), @@ -72,7 +73,7 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) { snapshot.Timeline = []TimelineItem{ &CreateTimelineItem{ - CommentTimelineItem: NewCommentTimelineItem(op.Id(), comment), + CommentTimelineItem: NewCommentTimelineItem(commentId, comment), }, } } diff --git a/bug/snapshot.go b/bug/snapshot.go index 11df04b212994fcf810a33f263bb7be6dfd6a5d4..0005b93019e061fbe2f554dafbfc81a0f1bb8bab 100644 --- a/bug/snapshot.go +++ b/bug/snapshot.go @@ -28,6 +28,11 @@ type Snapshot struct { // Return the Bug identifier func (snap *Snapshot) Id() entity.Id { + if snap.id == "" { + // simply panic as it would be a coding error + // (using an id of a bug not stored yet) + panic("no id yet") + } return snap.id } diff --git a/cache/repo_cache_bug.go b/cache/repo_cache_bug.go index 1701f66d09fb6ae9c1ded3c0d9ead16bd2a65bf4..cfcbb72d6cc7bfdcbef91f9580ec75f96e0d5b78 100644 --- a/cache/repo_cache_bug.go +++ b/cache/repo_cache_bug.go @@ -261,6 +261,53 @@ func (c *RepoCache) resolveBugMatcher(f func(*BugExcerpt) bool) (entity.Id, erro return matching[0], nil } +// ResolveComment search for a Bug/Comment combination matching the merged +// bug/comment Id prefix. Returns the Bug containing the Comment and the Comment's +// Id. +func (c *RepoCache) ResolveComment(prefix string) (*BugCache, entity.Id, error) { + bugPrefix, _ := bug.SplitCommentId(prefix) + bugCandidate := make([]entity.Id, 0, 5) + + // build a list of possible matching bugs + c.muBug.RLock() + for _, excerpt := range c.bugExcerpts { + if excerpt.Id.HasPrefix(bugPrefix) { + bugCandidate = append(bugCandidate, excerpt.Id) + } + } + c.muBug.RUnlock() + + matchingBugIds := make([]entity.Id, 0, 5) + matchingCommentId := entity.UnsetId + var matchingBug *BugCache + + // search for matching comments + // searching every bug candidate allow for some collision with the bug prefix only, + // before being refined with the full comment prefix + for _, bugId := range bugCandidate { + b, err := c.ResolveBug(bugId) + if err != nil { + return nil, entity.UnsetId, err + } + + for _, comment := range b.Snapshot().Comments { + if comment.Id().HasPrefix(prefix) { + matchingBugIds = append(matchingBugIds, bugId) + matchingBug = b + matchingCommentId = comment.Id() + } + } + } + + if len(matchingBugIds) > 1 { + return nil, entity.UnsetId, entity.NewErrMultipleMatch("bug/comment", matchingBugIds) + } else if len(matchingBugIds) == 0 { + return nil, entity.UnsetId, errors.New("comment doesn't exist") + } + + return matchingBug, matchingCommentId, nil +} + // QueryBugs return the id of all Bug matching the given Query func (c *RepoCache) QueryBugs(q *query.Query) ([]entity.Id, error) { c.muBug.RLock() diff --git a/commands/comment.go b/commands/comment.go index d8995c3ed1e59aed1a503c9358870ad1b93d7276..eb90624aaa2db64751ac9d847f762879e4f7128c 100644 --- a/commands/comment.go +++ b/commands/comment.go @@ -22,6 +22,7 @@ func newCommentCommand() *cobra.Command { } cmd.AddCommand(newCommentAddCommand()) + cmd.AddCommand(newCommentEditCommand()) return cmd } diff --git a/commands/comment_edit.go b/commands/comment_edit.go new file mode 100644 index 0000000000000000000000000000000000000000..61132967b4225ac85b8c40a8b53cb1f98d65f7e9 --- /dev/null +++ b/commands/comment_edit.go @@ -0,0 +1,71 @@ +package commands + +import ( + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/input" +) + +type commentEditOptions struct { + messageFile string + message string +} + +func newCommentEditCommand() *cobra.Command { + env := newEnv() + options := commentEditOptions{} + + cmd := &cobra.Command{ + Use: "edit ", + Short: "Edit an existing comment on a bug.", + Args: cobra.ExactArgs(1), + PreRunE: loadBackendEnsureUser(env), + PostRunE: closeBackend(env), + RunE: func(cmd *cobra.Command, args []string) error { + return runCommentEdit(env, options, args) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringVarP(&options.messageFile, "file", "F", "", + "Take the message from the given file. Use - to read the message from the standard input") + + flags.StringVarP(&options.message, "message", "m", "", + "Provide the new message from the command line") + + return cmd +} + +func runCommentEdit(env *Env, opts commentEditOptions, args []string) error { + b, commentId, err := env.backend.ResolveComment(args[0]) + if err != nil { + return err + } + + if opts.messageFile != "" && opts.message == "" { + opts.message, err = input.BugCommentFileInput(opts.messageFile) + if err != nil { + return err + } + } + + if opts.messageFile == "" && opts.message == "" { + opts.message, err = input.BugCommentEditorInput(env.backend, "") + if err == input.ErrEmptyMessage { + env.err.Println("Empty message, aborting.") + return nil + } + if err != nil { + return err + } + } + + _, err = b.EditComment(commentId, opts.message) + if err != nil { + return err + } + + return b.Commit() +} diff --git a/commands/show.go b/commands/show.go index 9ebd1926df5f4d51c664d472189b4e90b262589a..10087f927e0e504c70a42408c9951beaee4485e7 100644 --- a/commands/show.go +++ b/commands/show.go @@ -158,8 +158,9 @@ func showDefaultFormatter(env *Env, snapshot *bug.Snapshot) error { for i, comment := range snapshot.Comments { var message string - env.out.Printf("%s#%d %s <%s>\n\n", + env.out.Printf("%s%s #%d %s <%s>\n\n", indent, + comment.Id().Human(), i, comment.Author.DisplayName(), comment.Author.Email(), diff --git a/doc/man/git-bug-comment-edit.1 b/doc/man/git-bug-comment-edit.1 new file mode 100644 index 0000000000000000000000000000000000000000..e3cb2daf0347848d58aa3dbaf473737eaf325a82 --- /dev/null +++ b/doc/man/git-bug-comment-edit.1 @@ -0,0 +1,35 @@ +.nh +.TH "GIT\-BUG" "1" "Apr 2019" "Generated from git\-bug's source code" "" + +.SH NAME +.PP +git\-bug\-comment\-edit \- Edit an existing comment on a bug. + + +.SH SYNOPSIS +.PP +\fBgit\-bug comment edit [flags]\fP + + +.SH DESCRIPTION +.PP +Edit an existing comment on a bug. + + +.SH OPTIONS +.PP +\fB\-F\fP, \fB\-\-file\fP="" + Take the message from the given file. Use \- to read the message from the standard input + +.PP +\fB\-m\fP, \fB\-\-message\fP="" + Provide the new message from the command line + +.PP +\fB\-h\fP, \fB\-\-help\fP[=false] + help for edit + + +.SH SEE ALSO +.PP +\fBgit\-bug\-comment(1)\fP diff --git a/doc/man/git-bug-comment.1 b/doc/man/git-bug-comment.1 index 7cad5a0d4c5ed57bffa9bb698b9c9825babc950c..cb0740bb8b55e10ba7a5ff5d337d9a6a1dfb2977 100644 --- a/doc/man/git-bug-comment.1 +++ b/doc/man/git-bug-comment.1 @@ -24,4 +24,4 @@ Display or add comments to a bug. .SH SEE ALSO .PP -\fBgit\-bug(1)\fP, \fBgit\-bug\-comment\-add(1)\fP +\fBgit\-bug(1)\fP, \fBgit\-bug\-comment\-add(1)\fP, \fBgit\-bug\-comment\-edit(1)\fP diff --git a/doc/md/git-bug_comment.md b/doc/md/git-bug_comment.md index 6ac7c45ba4ebc5c55687b5aadf6666529ac74454..48050a9795a30c740a62a408cd2a3401a43b1cf9 100644 --- a/doc/md/git-bug_comment.md +++ b/doc/md/git-bug_comment.md @@ -16,4 +16,5 @@ git-bug comment [ID] [flags] * [git-bug](git-bug.md) - A bug tracker embedded in Git. * [git-bug comment add](git-bug_comment_add.md) - Add a new comment to a bug. +* [git-bug comment edit](git-bug_comment_edit.md) - Edit an existing comment on a bug. diff --git a/doc/md/git-bug_comment_edit.md b/doc/md/git-bug_comment_edit.md new file mode 100644 index 0000000000000000000000000000000000000000..2546dff16df46b4048d4db12ebf39ca31741e7fe --- /dev/null +++ b/doc/md/git-bug_comment_edit.md @@ -0,0 +1,20 @@ +## git-bug comment edit + +Edit an existing comment on a bug. + +``` +git-bug comment edit [flags] +``` + +### Options + +``` + -F, --file string Take the message from the given file. Use - to read the message from the standard input + -m, --message string Provide the new message from the command line + -h, --help help for edit +``` + +### SEE ALSO + +* [git-bug comment](git-bug_comment.md) - Display or add comments to a bug. + diff --git a/misc/bash_completion/git-bug b/misc/bash_completion/git-bug index 912e87b487f826c9e838235b39f8d48fea9f27d6..b3103e885de41b64aa8d1eefdccc7f33d4b00d55 100644 --- a/misc/bash_completion/git-bug +++ b/misc/bash_completion/git-bug @@ -722,6 +722,38 @@ _git-bug_comment_add() noun_aliases=() } +_git-bug_comment_edit() +{ + last_command="git-bug_comment_edit" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--file=") + two_word_flags+=("--file") + two_word_flags+=("-F") + local_nonpersistent_flags+=("--file") + local_nonpersistent_flags+=("--file=") + local_nonpersistent_flags+=("-F") + flags+=("--message=") + two_word_flags+=("--message") + two_word_flags+=("-m") + local_nonpersistent_flags+=("--message") + local_nonpersistent_flags+=("--message=") + local_nonpersistent_flags+=("-m") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _git-bug_comment() { last_command="git-bug_comment" @@ -730,6 +762,7 @@ _git-bug_comment() commands=() commands+=("add") + commands+=("edit") flags=() two_word_flags=() diff --git a/misc/powershell_completion/git-bug b/misc/powershell_completion/git-bug index 29c282378441bba92d7974c442015ec2a256480c..5ff155156c4db45ad11e10386bcb317b9ca3f39b 100644 --- a/misc/powershell_completion/git-bug +++ b/misc/powershell_completion/git-bug @@ -118,6 +118,7 @@ Register-ArgumentCompleter -Native -CommandName 'git-bug' -ScriptBlock { } 'git-bug;comment' { [CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'Add a new comment to a bug.') + [CompletionResult]::new('edit', 'edit', [CompletionResultType]::ParameterValue, 'Edit an existing comment on a bug.') break } 'git-bug;comment;add' { @@ -127,6 +128,13 @@ Register-ArgumentCompleter -Native -CommandName 'git-bug' -ScriptBlock { [CompletionResult]::new('--message', 'message', [CompletionResultType]::ParameterName, 'Provide the new message from the command line') break } + 'git-bug;comment;edit' { + [CompletionResult]::new('-F', 'F', [CompletionResultType]::ParameterName, 'Take the message from the given file. Use - to read the message from the standard input') + [CompletionResult]::new('--file', 'file', [CompletionResultType]::ParameterName, 'Take the message from the given file. Use - to read the message from the standard input') + [CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Provide the new message from the command line') + [CompletionResult]::new('--message', 'message', [CompletionResultType]::ParameterName, 'Provide the new message from the command line') + break + } 'git-bug;deselect' { break } From fcf43915e1736fe0b56f8f06386f68d9b56da7a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Mon, 30 Nov 2020 00:41:50 +0100 Subject: [PATCH 11/42] bug: fix tests --- bug/op_create.go | 7 +++++++ bug/op_create_test.go | 7 +++++-- bug/op_edit_comment_test.go | 30 +++++++++++++++--------------- bug/op_set_metadata_test.go | 12 ++++++------ 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/bug/op_create.go b/bug/op_create.go index 15fb69b5533f2fe96756018da17668da3648915a..044ddd72be697cf15f3500b96454a91c0a127071 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -54,6 +54,13 @@ func (op *CreateOperation) SetMetadata(key string, value string) { } func (op *CreateOperation) Apply(snapshot *Snapshot) { + // sanity check: will fail when adding a second Create + if snapshot.id != "" && snapshot.id != entity.UnsetId && snapshot.id != op.Id() { + panic("adding a second Create operation") + } + + snapshot.id = op.Id() + snapshot.addActor(op.Author) snapshot.addParticipant(op.Author) diff --git a/bug/op_create_test.go b/bug/op_create_test.go index 533aec2e917c4fc56e40065e987dd1a5a8014fba..73a657784f899cc2ac1859cc83b4a06074f1be98 100644 --- a/bug/op_create_test.go +++ b/bug/op_create_test.go @@ -29,14 +29,17 @@ func TestCreate(t *testing.T) { id := create.Id() require.NoError(t, id.Validate()) + commentId := DeriveCommentId(create.Id(), create.Id()) + comment := Comment{ - id: id, + id: commentId, Author: rene, Message: "message", UnixTime: timestamp.Timestamp(create.UnixTime), } expected := Snapshot{ + id: create.Id(), Title: "title", Comments: []Comment{ comment, @@ -47,7 +50,7 @@ func TestCreate(t *testing.T) { CreateTime: create.Time(), Timeline: []TimelineItem{ &CreateTimelineItem{ - CommentTimelineItem: NewCommentTimelineItem(id, comment), + CommentTimelineItem: NewCommentTimelineItem(commentId, comment), }, }, } diff --git a/bug/op_edit_comment_test.go b/bug/op_edit_comment_test.go index 92ee7539b2291a8ed209b268d9e08bdf77f2343b..a7330932924c29c174066fbe50f3ab993e1f6c3a 100644 --- a/bug/op_edit_comment_test.go +++ b/bug/op_edit_comment_test.go @@ -43,35 +43,35 @@ func TestEdit(t *testing.T) { id3 := comment2.Id() require.NoError(t, id3.Validate()) - edit := NewEditCommentOp(rene, unix, id1, "create edited", nil) + edit := NewEditCommentOp(rene, unix, snapshot.Comments[0].Id(), "create edited", nil) edit.Apply(&snapshot) - require.Equal(t, len(snapshot.Timeline), 4) - require.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2) - require.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 1) - require.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 1) + require.Len(t, snapshot.Timeline, 4) + require.Len(t, snapshot.Timeline[0].(*CreateTimelineItem).History, 2) + require.Len(t, snapshot.Timeline[1].(*AddCommentTimelineItem).History, 1) + require.Len(t, snapshot.Timeline[3].(*AddCommentTimelineItem).History, 1) require.Equal(t, snapshot.Comments[0].Message, "create edited") require.Equal(t, snapshot.Comments[1].Message, "comment 1") require.Equal(t, snapshot.Comments[2].Message, "comment 2") - edit2 := NewEditCommentOp(rene, unix, id2, "comment 1 edited", nil) + edit2 := NewEditCommentOp(rene, unix, snapshot.Comments[1].Id(), "comment 1 edited", nil) edit2.Apply(&snapshot) - require.Equal(t, len(snapshot.Timeline), 4) - require.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2) - require.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 2) - require.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 1) + require.Len(t, snapshot.Timeline, 4) + require.Len(t, snapshot.Timeline[0].(*CreateTimelineItem).History, 2) + require.Len(t, snapshot.Timeline[1].(*AddCommentTimelineItem).History, 2) + require.Len(t, snapshot.Timeline[3].(*AddCommentTimelineItem).History, 1) require.Equal(t, snapshot.Comments[0].Message, "create edited") require.Equal(t, snapshot.Comments[1].Message, "comment 1 edited") require.Equal(t, snapshot.Comments[2].Message, "comment 2") - edit3 := NewEditCommentOp(rene, unix, id3, "comment 2 edited", nil) + edit3 := NewEditCommentOp(rene, unix, snapshot.Comments[2].Id(), "comment 2 edited", nil) edit3.Apply(&snapshot) - require.Equal(t, len(snapshot.Timeline), 4) - require.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2) - require.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 2) - require.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 2) + require.Len(t, snapshot.Timeline, 4) + require.Len(t, snapshot.Timeline[0].(*CreateTimelineItem).History, 2) + require.Len(t, snapshot.Timeline[1].(*AddCommentTimelineItem).History, 2) + require.Len(t, snapshot.Timeline[3].(*AddCommentTimelineItem).History, 2) require.Equal(t, snapshot.Comments[0].Message, "create edited") require.Equal(t, snapshot.Comments[1].Message, "comment 1 edited") require.Equal(t, snapshot.Comments[2].Message, "comment 2 edited") diff --git a/bug/op_set_metadata_test.go b/bug/op_set_metadata_test.go index c90f192af1ecc2d525e1ac46eab3825e24ec58e6..c0c91617c1926f1abcd7a2049f2350655694b481 100644 --- a/bug/op_set_metadata_test.go +++ b/bug/op_set_metadata_test.go @@ -46,14 +46,14 @@ func TestSetMetadata(t *testing.T) { snapshot.Operations = append(snapshot.Operations, op1) createMetadata := snapshot.Operations[0].AllMetadata() - require.Equal(t, len(createMetadata), 2) + require.Len(t, createMetadata, 2) // original key is not overrided require.Equal(t, createMetadata["key"], "value") // new key is set require.Equal(t, createMetadata["key2"], "value") commentMetadata := snapshot.Operations[1].AllMetadata() - require.Equal(t, len(commentMetadata), 1) + require.Len(t, commentMetadata, 1) require.Equal(t, commentMetadata["key2"], "value2") op2 := NewSetMetadataOp(rene, unix, id2, map[string]string{ @@ -65,12 +65,12 @@ func TestSetMetadata(t *testing.T) { snapshot.Operations = append(snapshot.Operations, op2) createMetadata = snapshot.Operations[0].AllMetadata() - require.Equal(t, len(createMetadata), 2) + require.Len(t, createMetadata, 2) require.Equal(t, createMetadata["key"], "value") require.Equal(t, createMetadata["key2"], "value") commentMetadata = snapshot.Operations[1].AllMetadata() - require.Equal(t, len(commentMetadata), 2) + require.Len(t, commentMetadata, 2) // original key is not overrided require.Equal(t, commentMetadata["key2"], "value2") // new key is set @@ -85,14 +85,14 @@ func TestSetMetadata(t *testing.T) { snapshot.Operations = append(snapshot.Operations, op3) createMetadata = snapshot.Operations[0].AllMetadata() - require.Equal(t, len(createMetadata), 2) + require.Len(t, createMetadata, 2) // original key is not overrided require.Equal(t, createMetadata["key"], "value") // previously set key is not overrided require.Equal(t, createMetadata["key2"], "value") commentMetadata = snapshot.Operations[1].AllMetadata() - require.Equal(t, len(commentMetadata), 2) + require.Len(t, commentMetadata, 2) require.Equal(t, commentMetadata["key2"], "value2") require.Equal(t, commentMetadata["key3"], "value3") } From db7074301b6af895b1a47ecd12a5028ac809abfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Mon, 30 Nov 2020 01:55:30 +0100 Subject: [PATCH 12/42] entity: generalize the combined Ids, use 64 length --- bug/comment.go | 46 ------------------------ bug/comment_test.go | 27 -------------- bug/op_add_comment.go | 2 +- bug/op_create.go | 2 +- bug/op_create_test.go | 3 +- cache/repo_cache_bug.go | 2 +- entity/id.go | 2 +- entity/id_interleaved.go | 68 +++++++++++++++++++++++++++++++++++ entity/id_interleaved_test.go | 36 +++++++++++++++++++ 9 files changed, 110 insertions(+), 78 deletions(-) delete mode 100644 bug/comment_test.go create mode 100644 entity/id_interleaved.go create mode 100644 entity/id_interleaved_test.go diff --git a/bug/comment.go b/bug/comment.go index 1a9ca05af7ded8ffcdd689f2fce3fb26b37a36fa..4c9d118ebb4cb79f07dc953aedaa3748fd63a2fb 100644 --- a/bug/comment.go +++ b/bug/comment.go @@ -1,8 +1,6 @@ package bug import ( - "strings" - "github.com/dustin/go-humanize" "github.com/MichaelMure/git-bug/entity" @@ -33,50 +31,6 @@ func (c Comment) Id() entity.Id { return c.id } -const compiledCommentIdFormat = "BCBCBCBBBCBBBBCBBBBCBBBBCBBBBCBBBBCBBBBC" - -// DeriveCommentId compute a merged Id for a comment holding information from -// both the Bug's Id and the Comment's Id. This allow to later find efficiently -// a Comment because we can access the bug directly instead of searching for a -// Bug that has a Comment matching the Id. -// -// To allow the use of an arbitrary length prefix of this merged Id, Ids from Bug -// and Comment are interleaved with this irregular pattern to give the best chance -// to find the Comment even with a 7 character prefix. -// -// A complete merged Id hold 30 characters for the Bug and 10 for the Comment, -// which give a key space of 36^30 for the Bug (~5 * 10^46) and 36^10 for the -// Comment (~3 * 10^15). This asymmetry assume a reasonable number of Comment -// within a Bug, while still allowing for a vast key space for Bug (that is, a -// globally merged bug database) with a low risk of collision. -func DeriveCommentId(bugId entity.Id, commentId entity.Id) entity.Id { - var id strings.Builder - for _, char := range compiledCommentIdFormat { - if char == 'B' { - id.WriteByte(bugId[0]) - bugId = bugId[1:] - } else { - id.WriteByte(commentId[0]) - commentId = commentId[1:] - } - } - return entity.Id(id.String()) -} - -func SplitCommentId(prefix string) (bugPrefix string, commentPrefix string) { - var bugIdPrefix strings.Builder - var commentIdPrefix strings.Builder - - for i, char := range prefix { - if compiledCommentIdFormat[i] == 'B' { - bugIdPrefix.WriteRune(char) - } else { - commentIdPrefix.WriteRune(char) - } - } - return bugIdPrefix.String(), commentIdPrefix.String() -} - // FormatTimeRel format the UnixTime of the comment for human consumption func (c Comment) FormatTimeRel() string { return humanize.Time(c.UnixTime.Time()) diff --git a/bug/comment_test.go b/bug/comment_test.go deleted file mode 100644 index 423d10d8fb8c47ceb474d3d5bb558fbfabd59af8..0000000000000000000000000000000000000000 --- a/bug/comment_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package bug - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/entity" -) - -func TestCommentId(t *testing.T) { - bugId := entity.Id("abcdefghijklmnopqrstuvwxyz1234__________") - opId := entity.Id("ABCDEFGHIJ______________________________") - expectedId := entity.Id("aAbBcCdefDghijEklmnFopqrGstuvHwxyzI1234J") - - mergedId := DeriveCommentId(bugId, opId) - require.Equal(t, expectedId, mergedId) - - // full length - splitBugId, splitCommentId := SplitCommentId(mergedId.String()) - require.Equal(t, string(bugId[:30]), splitBugId) - require.Equal(t, string(opId[:10]), splitCommentId) - - splitBugId, splitCommentId = SplitCommentId(string(expectedId[:6])) - require.Equal(t, string(bugId[:3]), splitBugId) - require.Equal(t, string(opId[:3]), splitCommentId) -} diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go index df426ee0c5c02c2780f1f25ac34c1b8bb289dec9..e52c46fdef3b6ad44f9960296c1016f6553026f7 100644 --- a/bug/op_add_comment.go +++ b/bug/op_add_comment.go @@ -36,7 +36,7 @@ func (op *AddCommentOperation) Apply(snapshot *Snapshot) { snapshot.addActor(op.Author) snapshot.addParticipant(op.Author) - commentId := DeriveCommentId(snapshot.Id(), op.Id()) + commentId := entity.CombineIds(snapshot.Id(), op.Id()) comment := Comment{ id: commentId, Message: op.Message, diff --git a/bug/op_create.go b/bug/op_create.go index 044ddd72be697cf15f3500b96454a91c0a127071..1e944d1362a8e95b16caf7d4e70a1275ca408265 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -66,7 +66,7 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) { snapshot.Title = op.Title - commentId := DeriveCommentId(snapshot.Id(), op.Id()) + commentId := entity.CombineIds(snapshot.Id(), op.Id()) comment := Comment{ id: commentId, Message: op.Message, diff --git a/bug/op_create_test.go b/bug/op_create_test.go index 73a657784f899cc2ac1859cc83b4a06074f1be98..456357c4bcc7c1dd3c90a6d5526786dae9012009 100644 --- a/bug/op_create_test.go +++ b/bug/op_create_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/timestamp" @@ -29,7 +30,7 @@ func TestCreate(t *testing.T) { id := create.Id() require.NoError(t, id.Validate()) - commentId := DeriveCommentId(create.Id(), create.Id()) + commentId := entity.CombineIds(create.Id(), create.Id()) comment := Comment{ id: commentId, diff --git a/cache/repo_cache_bug.go b/cache/repo_cache_bug.go index cfcbb72d6cc7bfdcbef91f9580ec75f96e0d5b78..90b9a892bf1e23308de82e47e3cd7599ad4743f4 100644 --- a/cache/repo_cache_bug.go +++ b/cache/repo_cache_bug.go @@ -265,7 +265,7 @@ func (c *RepoCache) resolveBugMatcher(f func(*BugExcerpt) bool) (entity.Id, erro // bug/comment Id prefix. Returns the Bug containing the Comment and the Comment's // Id. func (c *RepoCache) ResolveComment(prefix string) (*BugCache, entity.Id, error) { - bugPrefix, _ := bug.SplitCommentId(prefix) + bugPrefix, _ := entity.SeparateIds(prefix) bugCandidate := make([]entity.Id, 0, 5) // build a list of possible matching bugs diff --git a/entity/id.go b/entity/id.go index 9e724012783807e555326fd990b813b8d2c59540..b602452e7112298fda2a6dea5d8723a3388c5312 100644 --- a/entity/id.go +++ b/entity/id.go @@ -65,7 +65,7 @@ func (i Id) MarshalGQL(w io.Writer) { // IsValid tell if the Id is valid func (i Id) Validate() error { - // Special case to + // Special case to detect outdated repo if len(i) == 40 { return fmt.Errorf("outdated repository format, please use https://github.com/MichaelMure/git-bug-migration to upgrade") } diff --git a/entity/id_interleaved.go b/entity/id_interleaved.go new file mode 100644 index 0000000000000000000000000000000000000000..5423afeed133b1dd908b533d0392d889fa81500a --- /dev/null +++ b/entity/id_interleaved.go @@ -0,0 +1,68 @@ +package entity + +import ( + "strings" +) + +// CombineIds compute a merged Id holding information from both the primary Id +// and the secondary Id. +// +// This allow to later find efficiently a secondary element because we can access +// the primary one directly instead of searching for a primary that has a +// secondary matching the Id. +// +// An example usage is Comment in a Bug. The interleaved Id will hold part of the +// Bug Id and part of the Comment Id. +// +// To allow the use of an arbitrary length prefix of this Id, Ids from primary +// and secondary are interleaved with this irregular pattern to give the +// best chance to find the secondary even with a 7 character prefix. +// +// Format is: PSPSPSPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPP +// +// A complete interleaved Id hold 50 characters for the primary and 14 for the +// secondary, which give a key space of 36^50 for the primary (~6 * 10^77) and +// 36^14 for the secondary (~6 * 10^21). This asymmetry assume a reasonable number +// of secondary within a primary Entity, while still allowing for a vast key space +// for the primary (that is, a globally merged database) with a low risk of collision. +// +// Here is the breakdown of several common prefix length: +// +// 5: 3P, 2S +// 7: 4P, 3S +// 10: 6P, 4S +// 16: 11P, 5S +func CombineIds(primary Id, secondary Id) Id { + var id strings.Builder + + for i := 0; i < idLength; i++ { + switch { + default: + id.WriteByte(primary[0]) + primary = primary[1:] + case i == 1, i == 3, i == 5, i == 9, i >= 10 && i%5 == 4: + id.WriteByte(secondary[0]) + secondary = secondary[1:] + } + } + + return Id(id.String()) +} + +// SeparateIds extract primary and secondary prefix from an arbitrary length prefix +// of an Id created with CombineIds. +func SeparateIds(prefix string) (primaryPrefix string, secondaryPrefix string) { + var primary strings.Builder + var secondary strings.Builder + + for i, r := range prefix { + switch { + default: + primary.WriteRune(r) + case i == 1, i == 3, i == 5, i == 9, i >= 10 && i%5 == 4: + secondary.WriteRune(r) + } + } + + return primary.String(), secondary.String() +} diff --git a/entity/id_interleaved_test.go b/entity/id_interleaved_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ef9218c97001635aab19fa1578560fee5b76f8a6 --- /dev/null +++ b/entity/id_interleaved_test.go @@ -0,0 +1,36 @@ +package entity + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestInterleaved(t *testing.T) { + primary := Id("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWX______________") + secondary := Id("YZ0123456789+/________________________________________________") + expectedId := Id("aYbZc0def1ghij2klmn3opqr4stuv5wxyz6ABCD7EFGH8IJKL9MNOP+QRST/UVWX") + + interleaved := CombineIds(primary, secondary) + require.Equal(t, expectedId, interleaved) + + // full length + splitPrimary, splitSecondary := SeparateIds(interleaved.String()) + require.Equal(t, string(primary[:50]), splitPrimary) + require.Equal(t, string(secondary[:14]), splitSecondary) + + // partial + splitPrimary, splitSecondary = SeparateIds(string(expectedId[:7])) + require.Equal(t, string(primary[:4]), splitPrimary) + require.Equal(t, string(secondary[:3]), splitSecondary) + + // partial + splitPrimary, splitSecondary = SeparateIds(string(expectedId[:10])) + require.Equal(t, string(primary[:6]), splitPrimary) + require.Equal(t, string(secondary[:4]), splitSecondary) + + // partial + splitPrimary, splitSecondary = SeparateIds(string(expectedId[:16])) + require.Equal(t, string(primary[:11]), splitPrimary) + require.Equal(t, string(secondary[:5]), splitSecondary) +} From bb8a214df37cab33032ce709a924e6282127d446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 10 Dec 2020 12:42:20 +0100 Subject: [PATCH 13/42] command: fix "comment edit" usage --- commands/comment_edit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/comment_edit.go b/commands/comment_edit.go index 61132967b4225ac85b8c40a8b53cb1f98d65f7e9..6a86f37e32271c5253314d9a6bc97bc414e9278e 100644 --- a/commands/comment_edit.go +++ b/commands/comment_edit.go @@ -16,7 +16,7 @@ func newCommentEditCommand() *cobra.Command { options := commentEditOptions{} cmd := &cobra.Command{ - Use: "edit ", + Use: "edit [COMMENT_ID]", Short: "Edit an existing comment on a bug.", Args: cobra.ExactArgs(1), PreRunE: loadBackendEnsureUser(env), From 5f6a39145d9ac109d430190d0d352544d27b6561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Mon, 21 Dec 2020 11:04:17 +0100 Subject: [PATCH 14/42] entity: add error to signal invalid format --- entity/err.go | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/entity/err.go b/entity/err.go index 90304d03a526e958c848d8d94f6d9b4ad8dbf880..9222e4dac460118d074a524b0c37c99440af80d3 100644 --- a/entity/err.go +++ b/entity/err.go @@ -31,28 +31,31 @@ func IsErrMultipleMatch(err error) bool { return ok } -// ErrOldFormatVersion indicate that the read data has a too old format. -type ErrOldFormatVersion struct { - formatVersion uint +type ErrInvalidFormat struct { + version uint + expected uint } -func NewErrOldFormatVersion(formatVersion uint) *ErrOldFormatVersion { - return &ErrOldFormatVersion{formatVersion: formatVersion} -} - -func (e ErrOldFormatVersion) Error() string { - return fmt.Sprintf("outdated repository format %v, please use https://github.com/MichaelMure/git-bug-migration to upgrade", e.formatVersion) -} - -// ErrNewFormatVersion indicate that the read data is too new for this software. -type ErrNewFormatVersion struct { - formatVersion uint +func NewErrInvalidFormat(version uint, expected uint) *ErrInvalidFormat { + return &ErrInvalidFormat{ + version: version, + expected: expected, + } } -func NewErrNewFormatVersion(formatVersion uint) *ErrNewFormatVersion { - return &ErrNewFormatVersion{formatVersion: formatVersion} +func NewErrUnknowFormat(expected uint) *ErrInvalidFormat { + return &ErrInvalidFormat{ + version: 0, + expected: expected, + } } -func (e ErrNewFormatVersion) Error() string { - return fmt.Sprintf("your version of git-bug is too old for this repository (version %v), please upgrade to the latest version", e.formatVersion) +func (e ErrInvalidFormat) Error() string { + if e.version == 0 { + return fmt.Sprintf("unreadable data, expected format version %v", e.expected) + } + if e.version < e.expected { + return fmt.Sprintf("outdated repository format %v, please use https://github.com/MichaelMure/git-bug-migration to upgrade to format version %v", e.version, e.expected) + } + return fmt.Sprintf("your version of git-bug is too old for this repository (format version %v, expected %v), please upgrade to the latest version", e.version, e.expected) } From 5c4e7de01281da51e32b4926dc0ef15b17a2d397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Mon, 21 Dec 2020 11:05:47 +0100 Subject: [PATCH 15/42] repository: partially add two new functions to RepoData --- repository/git.go | 8 ++++++++ repository/git_test.go | 10 +++------ repository/gogit.go | 27 ++++++++++++++++++++++++ repository/mock_repo.go | 42 +++++++++++++++++++++++++++++++------- repository/repo.go | 16 +++++++++++++-- repository/repo_testing.go | 9 ++++++++ 6 files changed, 96 insertions(+), 16 deletions(-) diff --git a/repository/git.go b/repository/git.go index 57c07c892e113908336aaafc861d2cf31d9e566b..e89bae87a7e240ae2b06e92d9c03f542015baa5b 100644 --- a/repository/git.go +++ b/repository/git.go @@ -35,6 +35,14 @@ type GitRepo struct { localStorage billy.Filesystem } +func (repo *GitRepo) ReadCommit(hash Hash) (Commit, error) { + panic("implement me") +} + +func (repo *GitRepo) ResolveRef(ref string) (Hash, error) { + panic("implement me") +} + // OpenGitRepo determines if the given working directory is inside of a git repository, // and returns the corresponding GitRepo instance if it is. func OpenGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) { diff --git a/repository/git_test.go b/repository/git_test.go index 1b36fd4cee0a0df2299cab670ce732a2bd86c9f0..6603e33907c7c52c68b8ce8b1632e2793d616dee 100644 --- a/repository/git_test.go +++ b/repository/git_test.go @@ -1,10 +1,6 @@ // Package repository contains helper methods for working with the Git repo. package repository -import ( - "testing" -) - -func TestGitRepo(t *testing.T) { - RepoTest(t, CreateTestRepo, CleanupTestRepos) -} +// func TestGitRepo(t *testing.T) { +// RepoTest(t, CreateTestRepo, CleanupTestRepos) +// } diff --git a/repository/gogit.go b/repository/gogit.go index 5abdef39bbca5c58dfb96a770f00d92c7404057e..64ccb773fb9bc49f6aa8003fcac897fe85fe6abd 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -595,6 +595,14 @@ func (repo *GoGitRepo) FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, err return Hash(commits[0].Hash.String()), nil } +func (repo *GoGitRepo) ResolveRef(ref string) (Hash, error) { + r, err := repo.r.Reference(plumbing.ReferenceName(ref), false) + if err != nil { + return "", err + } + return Hash(r.Hash().String()), nil +} + // UpdateRef will create or update a Git reference func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error { return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(ref), plumbing.NewHash(hash.String()))) @@ -679,6 +687,25 @@ func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) { return hashes, nil } +func (repo *GoGitRepo) ReadCommit(hash Hash) (Commit, error) { + commit, err := repo.r.CommitObject(plumbing.NewHash(hash.String())) + if err != nil { + return Commit{}, err + } + + parents := make([]Hash, len(commit.ParentHashes)) + for i, parentHash := range commit.ParentHashes { + parents[i] = Hash(parentHash.String()) + } + + return Commit{ + Hash: hash, + Parents: parents, + TreeHash: Hash(commit.TreeHash.String()), + }, nil + +} + func (repo *GoGitRepo) AllClocks() (map[string]lamport.Clock, error) { repo.clocksMutex.Lock() defer repo.clocksMutex.Unlock() diff --git a/repository/mock_repo.go b/repository/mock_repo.go index 974c3fb2aa47cffb6ba133ca8749f6498e263659..227e0f2c678cab63ae098b4d29c1c9743bb8bdff 100644 --- a/repository/mock_repo.go +++ b/repository/mock_repo.go @@ -179,7 +179,7 @@ var _ RepoData = &mockRepoData{} type commit struct { treeHash Hash - parent Hash + parents []Hash } type mockRepoData struct { @@ -247,11 +247,19 @@ func (r *mockRepoData) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, hash := Hash(fmt.Sprintf("%x", rawHash)) r.commits[hash] = commit{ treeHash: treeHash, - parent: parent, + parents: []Hash{parent}, } return hash, nil } +func (r *mockRepoData) ResolveRef(ref string) (Hash, error) { + h, ok := r.refs[ref] + if !ok { + return "", fmt.Errorf("unknown ref") + } + return h, nil +} + func (r *mockRepoData) UpdateRef(ref string, hash Hash) error { r.refs[ref] = hash return nil @@ -303,12 +311,29 @@ func (r *mockRepoData) ListCommits(ref string) ([]Hash, error) { } hashes = append([]Hash{hash}, hashes...) - hash = commit.parent + + if len(commit.parents) == 0 { + break + } + hash = commit.parents[0] } return hashes, nil } +func (r *mockRepoData) ReadCommit(hash Hash) (Commit, error) { + c, ok := r.commits[hash] + if !ok { + return Commit{}, fmt.Errorf("unknown commit") + } + + return Commit{ + Hash: hash, + Parents: c.parents, + TreeHash: c.treeHash, + }, nil +} + func (r *mockRepoData) ReadTree(hash Hash) ([]TreeEntry, error) { var data string @@ -340,8 +365,11 @@ func (r *mockRepoData) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) if !ok { return "", fmt.Errorf("unknown commit %v", hash1) } - ancestor1 = append(ancestor1, c.parent) - hash1 = c.parent + if len(c.parents) == 0 { + break + } + ancestor1 = append(ancestor1, c.parents[0]) + hash1 = c.parents[0] } for { @@ -356,11 +384,11 @@ func (r *mockRepoData) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) return "", fmt.Errorf("unknown commit %v", hash1) } - if c.parent == "" { + if c.parents[0] == "" { return "", fmt.Errorf("no ancestor found") } - hash2 = c.parent + hash2 = c.parents[0] } } diff --git a/repository/repo.go b/repository/repo.go index 625e01439659738fbdf6d04227da8529afdebefa..afd8ff77765780812463d54377bcadfe733e8796 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -88,6 +88,12 @@ type RepoBleve interface { ClearBleveIndex(name string) error } +type Commit struct { + Hash Hash + Parents []Hash + TreeHash Hash +} + // RepoData give access to the git data storage type RepoData interface { // FetchRefs fetch git refs from a remote @@ -115,11 +121,12 @@ type RepoData interface { // StoreCommit will store a Git commit with the given Git tree StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) + ReadCommit(hash Hash) (Commit, error) + // GetTreeHash return the git tree hash referenced in a commit GetTreeHash(commit Hash) (Hash, error) - // FindCommonAncestor will return the last common ancestor of two chain of commit - FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error) + ResolveRef(ref string) (Hash, error) // UpdateRef will create or update a Git reference UpdateRef(ref string, hash Hash) error @@ -136,7 +143,12 @@ type RepoData interface { // CopyRef will create a new reference with the same value as another one CopyRef(source string, dest string) error + // FindCommonAncestor will return the last common ancestor of two chain of commit + // Deprecated + FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error) + // ListCommits will return the list of tree hashes of a ref, in chronological order + // Deprecated ListCommits(ref string) ([]Hash, error) } diff --git a/repository/repo_testing.go b/repository/repo_testing.go index 2c8705d62066a0ed9b1b114a4fb47374291b54d4..1d3a31557edf0c597e4426c9b4b8267d0661e744 100644 --- a/repository/repo_testing.go +++ b/repository/repo_testing.go @@ -148,6 +148,11 @@ func RepoDataTest(t *testing.T, repo RepoData) { require.NoError(t, err) require.Equal(t, tree1read, tree1) + c2, err := repo.ReadCommit(commit2) + require.NoError(t, err) + c2expected := Commit{Hash: commit2, Parents: []Hash{commit1}, TreeHash: treeHash2} + require.Equal(t, c2expected, c2) + // Ref exist1, err := repo.RefExist("refs/bugs/ref1") @@ -161,6 +166,10 @@ func RepoDataTest(t *testing.T, repo RepoData) { require.NoError(t, err) require.True(t, exist1) + h, err := repo.ResolveRef("refs/bugs/ref1") + require.NoError(t, err) + require.Equal(t, commit2, h) + ls, err := repo.ListRefs("refs/bugs") require.NoError(t, err) require.ElementsMatch(t, []string{"refs/bugs/ref1"}, ls) From 9cca74cc334df94e37f3f3c76437da9a61e53bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Mon, 21 Dec 2020 11:10:43 +0100 Subject: [PATCH 16/42] entity: add embryo of a generic, DAG-enabled entity --- entity/entity.go | 181 +++++++++++++++++++++++++++++++++++++++ entity/entity_actions.go | 27 ++++++ entity/operation_pack.go | 125 +++++++++++++++++++++++++++ 3 files changed, 333 insertions(+) create mode 100644 entity/entity.go create mode 100644 entity/entity_actions.go create mode 100644 entity/operation_pack.go diff --git a/entity/entity.go b/entity/entity.go new file mode 100644 index 0000000000000000000000000000000000000000..c12dc2c461938693cf1770fa6a6aa19b49aa944e --- /dev/null +++ b/entity/entity.go @@ -0,0 +1,181 @@ +package entity + +import ( + "encoding/json" + "fmt" + "sort" + + "github.com/pkg/errors" + + "github.com/MichaelMure/git-bug/repository" +) + +type Operation interface { + // Id() Id + // MarshalJSON() ([]byte, error) + Validate() error + + base() *OpBase +} + +type OperationIterator struct { +} + +type Definition struct { + namespace string + operationUnmarshaler func(raw json.RawMessage) (Operation, error) + formatVersion uint +} + +type Entity struct { + Definition + + ops []Operation +} + +func New(definition Definition) *Entity { + return &Entity{ + Definition: definition, + } +} + +func Read(def Definition, repo repository.ClockedRepo, id Id) (*Entity, error) { + if err := id.Validate(); err != nil { + return nil, errors.Wrap(err, "invalid id") + } + + ref := fmt.Sprintf("refs/%s/%s", def.namespace, id.String()) + + rootHash, err := repo.ResolveRef(ref) + if err != nil { + return nil, err + } + + // Perform a depth-first search to get a topological order of the DAG where we discover the + // parents commit and go back in time up to the chronological root + + stack := make([]repository.Hash, 0, 32) + visited := make(map[repository.Hash]struct{}) + DFSOrder := make([]repository.Commit, 0, 32) + + stack = append(stack, rootHash) + + for len(stack) > 0 { + // pop + hash := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + if _, ok := visited[hash]; ok { + continue + } + + // mark as visited + visited[hash] = struct{}{} + + commit, err := repo.ReadCommit(hash) + if err != nil { + return nil, err + } + + DFSOrder = append(DFSOrder, commit) + + for _, parent := range commit.Parents { + stack = append(stack, parent) + } + } + + // Now, we can reverse this topological order and read the commits in an order where + // we are sure to have read all the chronological ancestors when we read a commit. + + // Next step is to: + // 1) read the operationPacks + // 2) make sure that the clocks causality respect the DAG topology. + + oppMap := make(map[repository.Hash]*operationPack) + var opsCount int + + rootCommit := DFSOrder[len(DFSOrder)-1] + rootOpp, err := readOperationPack(def, repo, rootCommit.TreeHash) + if err != nil { + return nil, err + } + oppMap[rootCommit.Hash] = rootOpp + + for i := len(DFSOrder) - 2; i >= 0; i-- { + commit := DFSOrder[i] + + // Verify DAG structure: single chronological root + if len(commit.Parents) == 0 { + return nil, fmt.Errorf("multiple root in the entity DAG") + } + + opp, err := readOperationPack(def, repo, commit.TreeHash) + if err != nil { + return nil, err + } + + // make sure that the lamport clocks causality match the DAG topology + for _, parentHash := range commit.Parents { + parentPack, ok := oppMap[parentHash] + if !ok { + panic("DFS failed") + } + + if parentPack.EditTime >= opp.EditTime { + return nil, fmt.Errorf("lamport clock ordering doesn't match the DAG") + } + + // to avoid an attack where clocks are pushed toward the uint64 rollover, make sure + // that the clocks don't jump too far in the future + if opp.EditTime-parentPack.EditTime > 10_000 { + return nil, fmt.Errorf("lamport clock jumping too far in the future, likely an attack") + } + } + + oppMap[commit.Hash] = opp + opsCount += len(opp.Operations) + } + + // Now that we know that the topological order and clocks are fine, we order the operationPacks + // based on the logical clocks, entirely ignoring the DAG topology + + oppSlice := make([]*operationPack, 0, len(oppMap)) + for _, pack := range oppMap { + oppSlice = append(oppSlice, pack) + } + sort.Slice(oppSlice, func(i, j int) bool { + // TODO: no secondary ordering? + return oppSlice[i].EditTime < oppSlice[i].EditTime + }) + + // Now that we ordered the operationPacks, we have the order of the Operations + + ops := make([]Operation, 0, opsCount) + for _, pack := range oppSlice { + for _, operation := range pack.Operations { + ops = append(ops, operation) + } + } + + return &Entity{ + Definition: def, + ops: ops, + }, nil +} + +func Remove() error { + panic("") +} + +func (e *Entity) Id() { + +} + +// return the ordered operations +func (e *Entity) Operations() []Operation { + return e.ops +} + +func (e *Entity) Commit() error { + panic("") +} diff --git a/entity/entity_actions.go b/entity/entity_actions.go new file mode 100644 index 0000000000000000000000000000000000000000..02e7648712a744f804d6145b0f579aca3a1adaa9 --- /dev/null +++ b/entity/entity_actions.go @@ -0,0 +1,27 @@ +package entity + +import ( + "fmt" + + "github.com/MichaelMure/git-bug/repository" +) + +func ListLocalIds(typename string, repo repository.RepoData) ([]Id, error) { + refs, err := repo.ListRefs(fmt.Sprintf("refs/%s/", typename)) + if err != nil { + return nil, err + } + return RefsToIds(refs), nil +} + +func Fetch() { + +} + +func Pull() { + +} + +func Push() { + +} diff --git a/entity/operation_pack.go b/entity/operation_pack.go new file mode 100644 index 0000000000000000000000000000000000000000..2377167f277b129941903d23da538ed0aa34641c --- /dev/null +++ b/entity/operation_pack.go @@ -0,0 +1,125 @@ +package entity + +import ( + "encoding/json" + "strconv" + "strings" + + "github.com/pkg/errors" + + "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/lamport" +) + +const opsEntryName = "ops" +const versionEntryPrefix = "version-" +const createClockEntryPrefix = "create-clock-" +const editClockEntryPrefix = "edit-clock-" + +type operationPack struct { + Operations []Operation + CreateTime lamport.Time + EditTime lamport.Time +} + +// func (opp *operationPack) MarshalJSON() ([]byte, error) { +// return json.Marshal(struct { +// Operations []Operation `json:"ops"` +// }{ +// Operations: opp.Operations, +// }) +// } + +func readOperationPack(def Definition, repo repository.RepoData, treeHash repository.Hash) (*operationPack, error) { + entries, err := repo.ReadTree(treeHash) + if err != nil { + return nil, err + } + + // check the format version first, fail early instead of trying to read something + // var version uint + // for _, entry := range entries { + // if strings.HasPrefix(entry.Name, versionEntryPrefix) { + // v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, versionEntryPrefix), 10, 64) + // if err != nil { + // return nil, errors.Wrap(err, "can't read format version") + // } + // if v > 1<<12 { + // return nil, fmt.Errorf("format version too big") + // } + // version = uint(v) + // break + // } + // } + // if version == 0 { + // return nil, NewErrUnknowFormat(def.formatVersion) + // } + // if version != def.formatVersion { + // return nil, NewErrInvalidFormat(version, def.formatVersion) + // } + + var ops []Operation + var createTime lamport.Time + var editTime lamport.Time + + for _, entry := range entries { + if entry.Name == opsEntryName { + data, err := repo.ReadData(entry.Hash) + if err != nil { + return nil, errors.Wrap(err, "failed to read git blob data") + } + + ops, err = unmarshallOperations(def, data) + if err != nil { + return nil, err + } + break + } + + if strings.HasPrefix(entry.Name, createClockEntryPrefix) { + v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, createClockEntryPrefix), 10, 64) + if err != nil { + return nil, errors.Wrap(err, "can't read creation lamport time") + } + createTime = lamport.Time(v) + } + + if strings.HasPrefix(entry.Name, editClockEntryPrefix) { + v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, editClockEntryPrefix), 10, 64) + if err != nil { + return nil, errors.Wrap(err, "can't read edit lamport time") + } + editTime = lamport.Time(v) + } + } + + return &operationPack{ + Operations: ops, + CreateTime: createTime, + EditTime: editTime, + }, nil +} + +func unmarshallOperations(def Definition, data []byte) ([]Operation, error) { + aux := struct { + Operations []json.RawMessage `json:"ops"` + }{} + + if err := json.Unmarshal(data, &aux); err != nil { + return nil, err + } + + ops := make([]Operation, 0, len(aux.Operations)) + + for _, raw := range aux.Operations { + // delegate to specialized unmarshal function + op, err := def.operationUnmarshaler(raw) + if err != nil { + return nil, err + } + + ops = append(ops, op) + } + + return ops, nil +} From 51ece149089f9075d3d6ba1bb09fda726efde8ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Mon, 21 Dec 2020 18:42:04 +0100 Subject: [PATCH 17/42] entity: clocks and write --- entity/entity.go | 196 ++++++++++++++++++++++++++++++++++----- entity/entity_actions.go | 4 + entity/entity_test.go | 107 +++++++++++++++++++++ entity/operation_pack.go | 132 ++++++++++++++++++++------ 4 files changed, 387 insertions(+), 52 deletions(-) create mode 100644 entity/entity_test.go diff --git a/entity/entity.go b/entity/entity.go index c12dc2c461938693cf1770fa6a6aa19b49aa944e..9ba536d68d00fc7910a6f4291bdc6019a94c7e45 100644 --- a/entity/entity.go +++ b/entity/entity.go @@ -8,34 +8,47 @@ import ( "github.com/pkg/errors" "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/lamport" ) +const refsPattern = "refs/%s/%s" +const creationClockPattern = "%s-create" +const editClockPattern = "%s-edit" + type Operation interface { - // Id() Id + Id() Id // MarshalJSON() ([]byte, error) Validate() error - - base() *OpBase } type OperationIterator struct { } type Definition struct { - namespace string + // the name of the entity (bug, pull-request, ...) + typename string + // the namespace in git (bugs, prs, ...) + namespace string + // a function decoding a JSON message into an Operation operationUnmarshaler func(raw json.RawMessage) (Operation, error) - formatVersion uint + // the expected format version number + formatVersion uint } type Entity struct { Definition - ops []Operation + ops []Operation + staging []Operation + + packClock lamport.Clock + lastCommit repository.Hash } func New(definition Definition) *Entity { return &Entity{ Definition: definition, + packClock: lamport.NewMemClock(), } } @@ -93,19 +106,15 @@ func Read(def Definition, repo repository.ClockedRepo, id Id) (*Entity, error) { oppMap := make(map[repository.Hash]*operationPack) var opsCount int + var packClock = lamport.NewMemClock() - rootCommit := DFSOrder[len(DFSOrder)-1] - rootOpp, err := readOperationPack(def, repo, rootCommit.TreeHash) - if err != nil { - return nil, err - } - oppMap[rootCommit.Hash] = rootOpp - - for i := len(DFSOrder) - 2; i >= 0; i-- { + for i := len(DFSOrder) - 1; i >= 0; i-- { commit := DFSOrder[i] + firstCommit := i == len(DFSOrder)-1 - // Verify DAG structure: single chronological root - if len(commit.Parents) == 0 { + // Verify DAG structure: single chronological root, so only the root + // can have no parents + if !firstCommit && len(commit.Parents) == 0 { return nil, fmt.Errorf("multiple root in the entity DAG") } @@ -114,6 +123,17 @@ func Read(def Definition, repo repository.ClockedRepo, id Id) (*Entity, error) { return nil, err } + // Check that the lamport clocks are set + if firstCommit && opp.CreateTime <= 0 { + return nil, fmt.Errorf("creation lamport time not set") + } + if opp.EditTime <= 0 { + return nil, fmt.Errorf("edition lamport time not set") + } + if opp.PackTime <= 0 { + return nil, fmt.Errorf("pack lamport time not set") + } + // make sure that the lamport clocks causality match the DAG topology for _, parentHash := range commit.Parents { parentPack, ok := oppMap[parentHash] @@ -136,6 +156,22 @@ func Read(def Definition, repo repository.ClockedRepo, id Id) (*Entity, error) { opsCount += len(opp.Operations) } + // The clocks are fine, we witness them + for _, opp := range oppMap { + err = repo.Witness(fmt.Sprintf(creationClockPattern, def.namespace), opp.CreateTime) + if err != nil { + return nil, err + } + err = repo.Witness(fmt.Sprintf(editClockPattern, def.namespace), opp.EditTime) + if err != nil { + return nil, err + } + err = packClock.Witness(opp.PackTime) + if err != nil { + return nil, err + } + } + // Now that we know that the topological order and clocks are fine, we order the operationPacks // based on the logical clocks, entirely ignoring the DAG topology @@ -145,7 +181,8 @@ func Read(def Definition, repo repository.ClockedRepo, id Id) (*Entity, error) { } sort.Slice(oppSlice, func(i, j int) bool { // TODO: no secondary ordering? - return oppSlice[i].EditTime < oppSlice[i].EditTime + // might be useful for stable ordering + return oppSlice[i].PackTime < oppSlice[i].PackTime }) // Now that we ordered the operationPacks, we have the order of the Operations @@ -160,22 +197,135 @@ func Read(def Definition, repo repository.ClockedRepo, id Id) (*Entity, error) { return &Entity{ Definition: def, ops: ops, + lastCommit: rootHash, }, nil } -func Remove() error { - panic("") +// Id return the Entity identifier +func (e *Entity) Id() Id { + // id is the id of the first operation + return e.FirstOp().Id() } -func (e *Entity) Id() { +func (e *Entity) Validate() error { + // non-empty + if len(e.ops) == 0 && len(e.staging) == 0 { + return fmt.Errorf("entity has no operations") + } + + // check if each operations are valid + for _, op := range e.ops { + if err := op.Validate(); err != nil { + return err + } + } + + // check if staging is valid if needed + for _, op := range e.staging { + if err := op.Validate(); err != nil { + return err + } + } + + // Check that there is no colliding operation's ID + ids := make(map[Id]struct{}) + for _, op := range e.Operations() { + if _, ok := ids[op.Id()]; ok { + return fmt.Errorf("id collision: %s", op.Id()) + } + ids[op.Id()] = struct{}{} + } + return nil } // return the ordered operations func (e *Entity) Operations() []Operation { - return e.ops + return append(e.ops, e.staging...) } -func (e *Entity) Commit() error { - panic("") +// Lookup for the very first operation of the Entity. +func (e *Entity) FirstOp() Operation { + for _, op := range e.ops { + return op + } + for _, op := range e.staging { + return op + } + return nil +} + +func (e *Entity) Append(op Operation) { + e.staging = append(e.staging, op) +} + +func (e *Entity) NeedCommit() bool { + return len(e.staging) > 0 +} + +func (e *Entity) CommitAdNeeded(repo repository.ClockedRepo) error { + if e.NeedCommit() { + return e.Commit(repo) + } + return nil +} + +func (e *Entity) Commit(repo repository.ClockedRepo) error { + if !e.NeedCommit() { + return fmt.Errorf("can't commit an entity with no pending operation") + } + + if err := e.Validate(); err != nil { + return errors.Wrapf(err, "can't commit a %s with invalid data", e.Definition.typename) + } + + // increment the various clocks for this new operationPack + packTime, err := e.packClock.Increment() + if err != nil { + return err + } + editTime, err := repo.Increment(fmt.Sprintf(editClockPattern, e.namespace)) + if err != nil { + return err + } + var creationTime lamport.Time + if e.lastCommit == "" { + creationTime, err = repo.Increment(fmt.Sprintf(creationClockPattern, e.namespace)) + if err != nil { + return err + } + } + + opp := &operationPack{ + Operations: e.staging, + CreateTime: creationTime, + EditTime: editTime, + PackTime: packTime, + } + + treeHash, err := opp.write(e.Definition, repo) + if err != nil { + return err + } + + // Write a Git commit referencing the tree, with the previous commit as parent + var commitHash repository.Hash + if e.lastCommit != "" { + commitHash, err = repo.StoreCommitWithParent(treeHash, e.lastCommit) + } else { + commitHash, err = repo.StoreCommit(treeHash) + } + if err != nil { + return err + } + + e.lastCommit = commitHash + e.ops = append(e.ops, e.staging...) + e.staging = nil + + // Create or update the Git reference for this entity + // When pushing later, the remote will ensure that this ref update + // is fast-forward, that is no data has been overwritten. + ref := fmt.Sprintf(refsPattern, e.namespace, e.Id().String()) + return repo.UpdateRef(ref, commitHash) } diff --git a/entity/entity_actions.go b/entity/entity_actions.go index 02e7648712a744f804d6145b0f579aca3a1adaa9..34e76a6252f374fbcbf2e60184851c54887febdb 100644 --- a/entity/entity_actions.go +++ b/entity/entity_actions.go @@ -25,3 +25,7 @@ func Pull() { func Push() { } + +func Remove() error { + panic("") +} diff --git a/entity/entity_test.go b/entity/entity_test.go new file mode 100644 index 0000000000000000000000000000000000000000..92a531796ac57d31c72e2528cbe6c9412693b2c5 --- /dev/null +++ b/entity/entity_test.go @@ -0,0 +1,107 @@ +package entity + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/repository" +) + +// func TestFoo(t *testing.T) { +// repo, err := repository.OpenGoGitRepo("~/dev/git-bug", nil) +// require.NoError(t, err) +// +// b, err := ReadBug(repo, Id("8b22e548c93a6ed23c31fd4e337c6286c3d1e5c9cae5537bc8e5842e11bd1099")) +// require.NoError(t, err) +// +// fmt.Println(b) +// } + +type op1 struct { + OperationType int `json:"type"` + Field1 string `json:"field_1"` +} + +func newOp1(field1 string) *op1 { + return &op1{OperationType: 1, Field1: field1} +} + +func (o op1) Id() Id { + data, _ := json.Marshal(o) + return DeriveId(data) +} + +func (o op1) Validate() error { return nil } + +type op2 struct { + OperationType int `json:"type"` + Field2 string `json:"field_2"` +} + +func newOp2(field2 string) *op2 { + return &op2{OperationType: 2, Field2: field2} +} + +func (o op2) Id() Id { + data, _ := json.Marshal(o) + return DeriveId(data) +} + +func (o op2) Validate() error { return nil } + +var def = Definition{ + typename: "foo", + namespace: "foos", + operationUnmarshaler: unmarshaller, + formatVersion: 1, +} + +func unmarshaller(raw json.RawMessage) (Operation, error) { + var t struct { + OperationType int `json:"type"` + } + + if err := json.Unmarshal(raw, &t); err != nil { + return nil, err + } + + switch t.OperationType { + case 1: + op := &op1{} + err := json.Unmarshal(raw, &op) + return op, err + case 2: + op := &op2{} + err := json.Unmarshal(raw, &op) + return op, err + default: + return nil, fmt.Errorf("unknown operation type %v", t.OperationType) + } +} + +func TestWriteRead(t *testing.T) { + repo := repository.NewMockRepo() + + entity := New(def) + require.False(t, entity.NeedCommit()) + + entity.Append(newOp1("foo")) + entity.Append(newOp2("bar")) + + require.True(t, entity.NeedCommit()) + require.NoError(t, entity.CommitAdNeeded(repo)) + require.False(t, entity.NeedCommit()) + + entity.Append(newOp2("foobar")) + require.True(t, entity.NeedCommit()) + require.NoError(t, entity.CommitAdNeeded(repo)) + require.False(t, entity.NeedCommit()) + + read, err := Read(def, repo, entity.Id()) + require.NoError(t, err) + + fmt.Println(*read) +} diff --git a/entity/operation_pack.go b/entity/operation_pack.go index 2377167f277b129941903d23da538ed0aa34641c..0a16dd61e9d044dfcbb063c16dd7af0b1893f3cd 100644 --- a/entity/operation_pack.go +++ b/entity/operation_pack.go @@ -2,6 +2,7 @@ package entity import ( "encoding/json" + "fmt" "strconv" "strings" @@ -11,25 +12,82 @@ import ( "github.com/MichaelMure/git-bug/util/lamport" ) +// TODO: extra data tree +const extraEntryName = "extra" + const opsEntryName = "ops" const versionEntryPrefix = "version-" const createClockEntryPrefix = "create-clock-" const editClockEntryPrefix = "edit-clock-" +const packClockEntryPrefix = "pack-clock-" type operationPack struct { Operations []Operation + // Encode the entity's logical time of creation across all entities of the same type. + // Only exist on the root operationPack CreateTime lamport.Time - EditTime lamport.Time + // Encode the entity's logical time of last edition across all entities of the same type. + // Exist on all operationPack + EditTime lamport.Time + // Encode the operationPack's logical time of creation withing this entity. + // Exist on all operationPack + PackTime lamport.Time } -// func (opp *operationPack) MarshalJSON() ([]byte, error) { -// return json.Marshal(struct { -// Operations []Operation `json:"ops"` -// }{ -// Operations: opp.Operations, -// }) -// } +func (opp operationPack) write(def Definition, repo repository.RepoData) (repository.Hash, error) { + // For different reason, we store the clocks and format version directly in the git tree. + // Version has to be accessible before any attempt to decode to return early with a unique error. + // Clocks could possibly be stored in the git blob but it's nice to separate data and metadata, and + // we are storing something directly in the tree already so why not. + // + // To have a valid Tree, we point the "fake" entries to always the same value, the empty blob. + emptyBlobHash, err := repo.StoreData([]byte{}) + if err != nil { + return "", err + } + + // Write the Ops as a Git blob containing the serialized array + data, err := json.Marshal(struct { + Operations []Operation `json:"ops"` + }{ + Operations: opp.Operations, + }) + if err != nil { + return "", err + } + hash, err := repo.StoreData(data) + if err != nil { + return "", err + } + + // Make a Git tree referencing this blob and encoding the other values: + // - format version + // - clocks + tree := []repository.TreeEntry{ + {ObjectType: repository.Blob, Hash: emptyBlobHash, + Name: fmt.Sprintf(versionEntryPrefix+"%d", def.formatVersion)}, + {ObjectType: repository.Blob, Hash: hash, + Name: opsEntryName}, + {ObjectType: repository.Blob, Hash: emptyBlobHash, + Name: fmt.Sprintf(editClockEntryPrefix+"%d", opp.EditTime)}, + {ObjectType: repository.Blob, Hash: emptyBlobHash, + Name: fmt.Sprintf(packClockEntryPrefix+"%d", opp.PackTime)}, + } + if opp.CreateTime > 0 { + tree = append(tree, repository.TreeEntry{ + ObjectType: repository.Blob, + Hash: emptyBlobHash, + Name: fmt.Sprintf(createClockEntryPrefix+"%d", opp.CreateTime), + }) + } + // Store the tree + return repo.StoreTree(tree) +} + +// readOperationPack read the operationPack encoded in git at the given Tree hash. +// +// Validity of the Lamport clocks is left for the caller to decide. func readOperationPack(def Definition, repo repository.RepoData, treeHash repository.Hash) (*operationPack, error) { entries, err := repo.ReadTree(treeHash) if err != nil { @@ -37,30 +95,31 @@ func readOperationPack(def Definition, repo repository.RepoData, treeHash reposi } // check the format version first, fail early instead of trying to read something - // var version uint - // for _, entry := range entries { - // if strings.HasPrefix(entry.Name, versionEntryPrefix) { - // v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, versionEntryPrefix), 10, 64) - // if err != nil { - // return nil, errors.Wrap(err, "can't read format version") - // } - // if v > 1<<12 { - // return nil, fmt.Errorf("format version too big") - // } - // version = uint(v) - // break - // } - // } - // if version == 0 { - // return nil, NewErrUnknowFormat(def.formatVersion) - // } - // if version != def.formatVersion { - // return nil, NewErrInvalidFormat(version, def.formatVersion) - // } + var version uint + for _, entry := range entries { + if strings.HasPrefix(entry.Name, versionEntryPrefix) { + v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, versionEntryPrefix), 10, 64) + if err != nil { + return nil, errors.Wrap(err, "can't read format version") + } + if v > 1<<12 { + return nil, fmt.Errorf("format version too big") + } + version = uint(v) + break + } + } + if version == 0 { + return nil, NewErrUnknowFormat(def.formatVersion) + } + if version != def.formatVersion { + return nil, NewErrInvalidFormat(version, def.formatVersion) + } var ops []Operation var createTime lamport.Time var editTime lamport.Time + var packTime lamport.Time for _, entry := range entries { if entry.Name == opsEntryName { @@ -73,7 +132,7 @@ func readOperationPack(def Definition, repo repository.RepoData, treeHash reposi if err != nil { return nil, err } - break + continue } if strings.HasPrefix(entry.Name, createClockEntryPrefix) { @@ -82,6 +141,7 @@ func readOperationPack(def Definition, repo repository.RepoData, treeHash reposi return nil, errors.Wrap(err, "can't read creation lamport time") } createTime = lamport.Time(v) + continue } if strings.HasPrefix(entry.Name, editClockEntryPrefix) { @@ -90,6 +150,16 @@ func readOperationPack(def Definition, repo repository.RepoData, treeHash reposi return nil, errors.Wrap(err, "can't read edit lamport time") } editTime = lamport.Time(v) + continue + } + + if strings.HasPrefix(entry.Name, packClockEntryPrefix) { + v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, packClockEntryPrefix), 10, 64) + if err != nil { + return nil, errors.Wrap(err, "can't read pack lamport time") + } + packTime = lamport.Time(v) + continue } } @@ -97,9 +167,13 @@ func readOperationPack(def Definition, repo repository.RepoData, treeHash reposi Operations: ops, CreateTime: createTime, EditTime: editTime, + PackTime: packTime, }, nil } +// unmarshallOperations delegate the unmarshalling of the Operation's JSON to the decoding +// function provided by the concrete entity. This gives access to the concrete type of each +// Operation. func unmarshallOperations(def Definition, data []byte) ([]Operation, error) { aux := struct { Operations []json.RawMessage `json:"ops"` From 4ef92efeb905102d37b81fafa0ac2173594ef30a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Fri, 25 Dec 2020 11:38:01 +0100 Subject: [PATCH 18/42] entity: total ordering of operations --- bug/bug_actions.go | 1 - entity/entity.go | 29 +++++++++++++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/bug/bug_actions.go b/bug/bug_actions.go index 21ce3733099308006be6ec2e525e7da200c553e1..aa82356d76c9cf9c34a9218f6a1e6435782e455a 100644 --- a/bug/bug_actions.go +++ b/bug/bug_actions.go @@ -68,7 +68,6 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeRes remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote) remoteRefs, err := repo.ListRefs(remoteRefSpec) - if err != nil { out <- entity.MergeResult{Err: err} return diff --git a/entity/entity.go b/entity/entity.go index 9ba536d68d00fc7910a6f4291bdc6019a94c7e45..a1e8e57e62f2099ba15d58ba549daaf3c8440f9d 100644 --- a/entity/entity.go +++ b/entity/entity.go @@ -21,9 +21,7 @@ type Operation interface { Validate() error } -type OperationIterator struct { -} - +// Definition hold the details defining one specialization of an Entity. type Definition struct { // the name of the entity (bug, pull-request, ...) typename string @@ -59,6 +57,11 @@ func Read(def Definition, repo repository.ClockedRepo, id Id) (*Entity, error) { ref := fmt.Sprintf("refs/%s/%s", def.namespace, id.String()) + return read(def, repo, ref) +} + +// read fetch from git and decode an Entity at an arbitrary git reference. +func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, error) { rootHash, err := repo.ResolveRef(ref) if err != nil { return nil, err @@ -180,9 +183,22 @@ func Read(def Definition, repo repository.ClockedRepo, id Id) (*Entity, error) { oppSlice = append(oppSlice, pack) } sort.Slice(oppSlice, func(i, j int) bool { - // TODO: no secondary ordering? - // might be useful for stable ordering - return oppSlice[i].PackTime < oppSlice[i].PackTime + // Primary ordering with the dedicated "pack" Lamport time that encode causality + // within the entity + if oppSlice[i].PackTime != oppSlice[j].PackTime { + return oppSlice[i].PackTime < oppSlice[i].PackTime + } + // We have equal PackTime, which means we had a concurrent edition. We can't tell which exactly + // came first. As a secondary arbitrary ordering, we can use the EditTime. It's unlikely to be + // enough but it can give us an edge to approach what really happened. + if oppSlice[i].EditTime != oppSlice[j].EditTime { + return oppSlice[i].EditTime < oppSlice[j].EditTime + } + // Well, what now? We still need a total ordering, the most stable possible. + // As a last resort, we can order based on a hash of the serialized Operations in the + // operationPack. It doesn't carry much meaning but it's unbiased and hard to abuse. + // This is a lexicographic ordering. + return oppSlice[i].Id < oppSlice[j].Id }) // Now that we ordered the operationPacks, we have the order of the Operations @@ -270,6 +286,7 @@ func (e *Entity) CommitAdNeeded(repo repository.ClockedRepo) error { return nil } +// TODO: support commit signature func (e *Entity) Commit(repo repository.ClockedRepo) error { if !e.NeedCommit() { return fmt.Errorf("can't commit an entity with no pending operation") From 8d63c983c982f93cc48d3996d6bd097ddeeb327f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 3 Jan 2021 23:59:25 +0100 Subject: [PATCH 19/42] WIP --- bug/bug.go | 4 +- entity/TODO | 8 + entity/dag/common_test.go | 137 +++++++ entity/{ => dag}/entity.go | 139 +++++--- entity/dag/entity_actions.go | 227 ++++++++++++ entity/dag/entity_test.go | 117 ++++++ entity/dag/operation.go | 31 ++ entity/dag/operation_pack.go | 294 +++++++++++++++ entity/dag/operation_pack_test.go | 44 +++ entity/doc.go | 8 - entity/entity_actions.go | 31 -- entity/entity_test.go | 107 ------ entity/merge.go | 14 +- entity/operation_pack.go | 199 ----------- entity/refs.go | 2 + identity/identity.go | 11 +- identity/identity_actions_test.go | 2 +- identity/identity_stub.go | 4 + identity/identity_test.go | 50 ++- identity/interface.go | 3 + identity/key.go | 187 +++++++++- identity/key_test.go | 21 ++ identity/version_test.go | 20 +- repository/common.go | 120 +++++++ repository/git.go | 570 ------------------------------ repository/git_cli.go | 56 --- repository/git_config.go | 221 ------------ repository/git_test.go | 6 - repository/git_testing.go | 72 ---- repository/gogit.go | 92 +++-- repository/gogit_testing.go | 8 +- repository/keyring.go | 10 + repository/mock_repo.go | 177 +++++----- repository/mock_repo_test.go | 4 +- repository/repo.go | 27 +- repository/repo_testing.go | 24 +- util/lamport/clock_testing.go | 4 +- util/lamport/mem_clock.go | 12 +- 38 files changed, 1547 insertions(+), 1516 deletions(-) create mode 100644 entity/TODO create mode 100644 entity/dag/common_test.go rename entity/{ => dag}/entity.go (66%) create mode 100644 entity/dag/entity_actions.go create mode 100644 entity/dag/entity_test.go create mode 100644 entity/dag/operation.go create mode 100644 entity/dag/operation_pack.go create mode 100644 entity/dag/operation_pack_test.go delete mode 100644 entity/doc.go delete mode 100644 entity/entity_actions.go delete mode 100644 entity/entity_test.go delete mode 100644 entity/operation_pack.go create mode 100644 identity/key_test.go create mode 100644 repository/common.go delete mode 100644 repository/git.go delete mode 100644 repository/git_cli.go delete mode 100644 repository/git_config.go delete mode 100644 repository/git_test.go delete mode 100644 repository/git_testing.go diff --git a/bug/bug.go b/bug/bug.go index fb36bfd859673395759b9fa688b7e64b50b76419..0c66f8acdd7358320c0f91400492cfd70eaea6bf 100644 --- a/bug/bug.go +++ b/bug/bug.go @@ -426,7 +426,7 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error { // Write a Git commit referencing the tree, with the previous commit as parent if bug.lastCommit != "" { - hash, err = repo.StoreCommitWithParent(hash, bug.lastCommit) + hash, err = repo.StoreCommit(hash, bug.lastCommit) } else { hash, err = repo.StoreCommit(hash) } @@ -524,7 +524,7 @@ func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) { } // create a new commit with the correct ancestor - hash, err := repo.StoreCommitWithParent(treeHash, bug.lastCommit) + hash, err := repo.StoreCommit(treeHash, bug.lastCommit) if err != nil { return false, err diff --git a/entity/TODO b/entity/TODO new file mode 100644 index 0000000000000000000000000000000000000000..fd3c97105ebac0ba2cc7b3b515d1933fa1a1da20 --- /dev/null +++ b/entity/TODO @@ -0,0 +1,8 @@ +- is the pack Lamport clock really useful vs only topological sort? + - topological order is enforced on the clocks, so what's the point? + - is EditTime equivalent to PackTime? no, avoid the gaps. Is it better? +- how to do commit signature? +- how to avoid id collision between Operations? +- write tests for actions +- migrate Bug to the new structure +- migrate Identity to the new structure? \ No newline at end of file diff --git a/entity/dag/common_test.go b/entity/dag/common_test.go new file mode 100644 index 0000000000000000000000000000000000000000..29f1279e680eb45de85dd3ec312bfb0336becd5d --- /dev/null +++ b/entity/dag/common_test.go @@ -0,0 +1,137 @@ +package dag + +import ( + "encoding/json" + "fmt" + + "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/repository" +) + +// This file contains an example dummy entity to be used in the tests + +/* + Operations +*/ + +type op1 struct { + author identity.Interface + + OperationType int `json:"type"` + Field1 string `json:"field_1"` +} + +func newOp1(author identity.Interface, field1 string) *op1 { + return &op1{author: author, OperationType: 1, Field1: field1} +} + +func (o op1) Id() entity.Id { + data, _ := json.Marshal(o) + return entity.DeriveId(data) +} + +func (o op1) Author() identity.Interface { + return o.author +} + +func (o op1) Validate() error { return nil } + +type op2 struct { + author identity.Interface + + OperationType int `json:"type"` + Field2 string `json:"field_2"` +} + +func newOp2(author identity.Interface, field2 string) *op2 { + return &op2{author: author, OperationType: 2, Field2: field2} +} + +func (o op2) Id() entity.Id { + data, _ := json.Marshal(o) + return entity.DeriveId(data) +} + +func (o op2) Author() identity.Interface { + return o.author +} + +func (o op2) Validate() error { return nil } + +func unmarshaler(author identity.Interface, raw json.RawMessage) (Operation, error) { + var t struct { + OperationType int `json:"type"` + } + + if err := json.Unmarshal(raw, &t); err != nil { + return nil, err + } + + switch t.OperationType { + case 1: + op := &op1{} + err := json.Unmarshal(raw, &op) + op.author = author + return op, err + case 2: + op := &op2{} + err := json.Unmarshal(raw, &op) + op.author = author + return op, err + default: + return nil, fmt.Errorf("unknown operation type %v", t.OperationType) + } +} + +/* + Identities + repo + definition +*/ + +func makeTestContext() (repository.ClockedRepo, identity.Interface, identity.Interface, Definition) { + repo := repository.NewMockRepo() + + id1, err := identity.NewIdentity(repo, "name1", "email1") + if err != nil { + panic(err) + } + err = id1.Commit(repo) + if err != nil { + panic(err) + } + id2, err := identity.NewIdentity(repo, "name2", "email2") + if err != nil { + panic(err) + } + err = id2.Commit(repo) + if err != nil { + panic(err) + } + + resolver := identityResolverFunc(func(id entity.Id) (identity.Interface, error) { + switch id { + case id1.Id(): + return id1, nil + case id2.Id(): + return id2, nil + default: + return nil, identity.ErrIdentityNotExist + } + }) + + def := Definition{ + typename: "foo", + namespace: "foos", + operationUnmarshaler: unmarshaler, + identityResolver: resolver, + formatVersion: 1, + } + + return repo, id1, id2, def +} + +type identityResolverFunc func(id entity.Id) (identity.Interface, error) + +func (fn identityResolverFunc) ResolveIdentity(id entity.Id) (identity.Interface, error) { + return fn(id) +} diff --git a/entity/entity.go b/entity/dag/entity.go similarity index 66% rename from entity/entity.go rename to entity/dag/entity.go index a1e8e57e62f2099ba15d58ba549daaf3c8440f9d..78347fa09473e9932b382ad69484e1d36a274f9c 100644 --- a/entity/entity.go +++ b/entity/dag/entity.go @@ -1,4 +1,6 @@ -package entity +// Package dag contains the base common code to define an entity stored +// in a chain of git objects, supporting actions like Push, Pull and Merge. +package dag import ( "encoding/json" @@ -7,6 +9,8 @@ import ( "github.com/pkg/errors" + "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/lamport" ) @@ -15,12 +19,6 @@ const refsPattern = "refs/%s/%s" const creationClockPattern = "%s-create" const editClockPattern = "%s-edit" -type Operation interface { - Id() Id - // MarshalJSON() ([]byte, error) - Validate() error -} - // Definition hold the details defining one specialization of an Entity. type Definition struct { // the name of the entity (bug, pull-request, ...) @@ -28,29 +26,40 @@ type Definition struct { // the namespace in git (bugs, prs, ...) namespace string // a function decoding a JSON message into an Operation - operationUnmarshaler func(raw json.RawMessage) (Operation, error) - // the expected format version number + operationUnmarshaler func(author identity.Interface, raw json.RawMessage) (Operation, error) + // a function loading an identity.Identity from its Id + identityResolver identity.Resolver + // the expected format version number, that can be used for data migration/upgrade formatVersion uint } +// Entity is a data structure stored in a chain of git objects, supporting actions like Push, Pull and Merge. type Entity struct { Definition - ops []Operation + // operations that are already stored in the repository + ops []Operation + // operations not yet stored in the repository staging []Operation - packClock lamport.Clock + // TODO: add here createTime and editTime + + // // TODO: doesn't seems to actually be useful over the topological sort ? Timestamp can be generated from graph depth + // // TODO: maybe EditTime is better because it could spread ops in consecutive groups on the logical timeline --> avoid interleaving + // packClock lamport.Clock lastCommit repository.Hash } +// New create an empty Entity func New(definition Definition) *Entity { return &Entity{ Definition: definition, - packClock: lamport.NewMemClock(), + // packClock: lamport.NewMemClock(), } } -func Read(def Definition, repo repository.ClockedRepo, id Id) (*Entity, error) { +// Read will read and decode a stored Entity from a repository +func Read(def Definition, repo repository.ClockedRepo, id entity.Id) (*Entity, error) { if err := id.Validate(); err != nil { return nil, errors.Wrap(err, "invalid id") } @@ -109,32 +118,33 @@ func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, err oppMap := make(map[repository.Hash]*operationPack) var opsCount int - var packClock = lamport.NewMemClock() + // var packClock = lamport.NewMemClock() for i := len(DFSOrder) - 1; i >= 0; i-- { commit := DFSOrder[i] - firstCommit := i == len(DFSOrder)-1 + isFirstCommit := i == len(DFSOrder)-1 + isMerge := len(commit.Parents) > 1 // Verify DAG structure: single chronological root, so only the root - // can have no parents - if !firstCommit && len(commit.Parents) == 0 { - return nil, fmt.Errorf("multiple root in the entity DAG") + // can have no parents. Said otherwise, the DAG need to have exactly + // one leaf. + if !isFirstCommit && len(commit.Parents) == 0 { + return nil, fmt.Errorf("multiple leafs in the entity DAG") } - opp, err := readOperationPack(def, repo, commit.TreeHash) + opp, err := readOperationPack(def, repo, commit) if err != nil { return nil, err } - // Check that the lamport clocks are set - if firstCommit && opp.CreateTime <= 0 { - return nil, fmt.Errorf("creation lamport time not set") - } - if opp.EditTime <= 0 { - return nil, fmt.Errorf("edition lamport time not set") + err = opp.Validate() + if err != nil { + return nil, err } - if opp.PackTime <= 0 { - return nil, fmt.Errorf("pack lamport time not set") + + // Check that the create lamport clock is set (not checked in Validate() as it's optional) + if isFirstCommit && opp.CreateTime <= 0 { + return nil, fmt.Errorf("creation lamport time not set") } // make sure that the lamport clocks causality match the DAG topology @@ -150,9 +160,13 @@ func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, err // to avoid an attack where clocks are pushed toward the uint64 rollover, make sure // that the clocks don't jump too far in the future - if opp.EditTime-parentPack.EditTime > 10_000 { + // we ignore merge commits here to allow merging after a loooong time without breaking anything, + // as long as there is one valid chain of small hops, it's fine. + if !isMerge && opp.EditTime-parentPack.EditTime > 1_000_000 { return nil, fmt.Errorf("lamport clock jumping too far in the future, likely an attack") } + + // TODO: PackTime is not checked } oppMap[commit.Hash] = opp @@ -169,10 +183,10 @@ func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, err if err != nil { return nil, err } - err = packClock.Witness(opp.PackTime) - if err != nil { - return nil, err - } + // err = packClock.Witness(opp.PackTime) + // if err != nil { + // return nil, err + // } } // Now that we know that the topological order and clocks are fine, we order the operationPacks @@ -185,20 +199,20 @@ func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, err sort.Slice(oppSlice, func(i, j int) bool { // Primary ordering with the dedicated "pack" Lamport time that encode causality // within the entity - if oppSlice[i].PackTime != oppSlice[j].PackTime { - return oppSlice[i].PackTime < oppSlice[i].PackTime - } + // if oppSlice[i].PackTime != oppSlice[j].PackTime { + // return oppSlice[i].PackTime < oppSlice[i].PackTime + // } // We have equal PackTime, which means we had a concurrent edition. We can't tell which exactly // came first. As a secondary arbitrary ordering, we can use the EditTime. It's unlikely to be // enough but it can give us an edge to approach what really happened. if oppSlice[i].EditTime != oppSlice[j].EditTime { return oppSlice[i].EditTime < oppSlice[j].EditTime } - // Well, what now? We still need a total ordering, the most stable possible. + // Well, what now? We still need a total ordering and the most stable possible. // As a last resort, we can order based on a hash of the serialized Operations in the // operationPack. It doesn't carry much meaning but it's unbiased and hard to abuse. - // This is a lexicographic ordering. - return oppSlice[i].Id < oppSlice[j].Id + // This is a lexicographic ordering on the stringified ID. + return oppSlice[i].Id() < oppSlice[j].Id() }) // Now that we ordered the operationPacks, we have the order of the Operations @@ -213,16 +227,18 @@ func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, err return &Entity{ Definition: def, ops: ops, + // packClock: packClock, lastCommit: rootHash, }, nil } // Id return the Entity identifier -func (e *Entity) Id() Id { +func (e *Entity) Id() entity.Id { // id is the id of the first operation return e.FirstOp().Id() } +// Validate check if the Entity data is valid func (e *Entity) Validate() error { // non-empty if len(e.ops) == 0 && len(e.staging) == 0 { @@ -244,7 +260,7 @@ func (e *Entity) Validate() error { } // Check that there is no colliding operation's ID - ids := make(map[Id]struct{}) + ids := make(map[entity.Id]struct{}) for _, op := range e.Operations() { if _, ok := ids[op.Id()]; ok { return fmt.Errorf("id collision: %s", op.Id()) @@ -255,12 +271,12 @@ func (e *Entity) Validate() error { return nil } -// return the ordered operations +// Operations return the ordered operations func (e *Entity) Operations() []Operation { return append(e.ops, e.staging...) } -// Lookup for the very first operation of the Entity. +// FirstOp lookup for the very first operation of the Entity func (e *Entity) FirstOp() Operation { for _, op := range e.ops { return op @@ -271,14 +287,29 @@ func (e *Entity) FirstOp() Operation { return nil } +// LastOp lookup for the very last operation of the Entity +func (e *Entity) LastOp() Operation { + if len(e.staging) > 0 { + return e.staging[len(e.staging)-1] + } + if len(e.ops) > 0 { + return e.ops[len(e.ops)-1] + } + return nil +} + +// Append add a new Operation to the Entity func (e *Entity) Append(op Operation) { e.staging = append(e.staging, op) } +// NeedCommit indicate if the in-memory state changed and need to be commit in the repository func (e *Entity) NeedCommit() bool { return len(e.staging) > 0 } +// CommitAdNeeded execute a Commit only if necessary. This function is useful to avoid getting an error if the Entity +// is already in sync with the repository. func (e *Entity) CommitAdNeeded(repo repository.ClockedRepo) error { if e.NeedCommit() { return e.Commit(repo) @@ -286,6 +317,7 @@ func (e *Entity) CommitAdNeeded(repo repository.ClockedRepo) error { return nil } +// Commit write the appended operations in the repository // TODO: support commit signature func (e *Entity) Commit(repo repository.ClockedRepo) error { if !e.NeedCommit() { @@ -296,11 +328,19 @@ func (e *Entity) Commit(repo repository.ClockedRepo) error { return errors.Wrapf(err, "can't commit a %s with invalid data", e.Definition.typename) } - // increment the various clocks for this new operationPack - packTime, err := e.packClock.Increment() - if err != nil { - return err + var author identity.Interface + for _, op := range e.staging { + if author != nil && op.Author() != author { + return fmt.Errorf("operations with different author") + } + author = op.Author() } + + // increment the various clocks for this new operationPack + // packTime, err := e.packClock.Increment() + // if err != nil { + // return err + // } editTime, err := repo.Increment(fmt.Sprintf(editClockPattern, e.namespace)) if err != nil { return err @@ -314,13 +354,14 @@ func (e *Entity) Commit(repo repository.ClockedRepo) error { } opp := &operationPack{ + Author: author, Operations: e.staging, CreateTime: creationTime, EditTime: editTime, - PackTime: packTime, + // PackTime: packTime, } - treeHash, err := opp.write(e.Definition, repo) + treeHash, err := opp.Write(e.Definition, repo) if err != nil { return err } @@ -328,7 +369,7 @@ func (e *Entity) Commit(repo repository.ClockedRepo) error { // Write a Git commit referencing the tree, with the previous commit as parent var commitHash repository.Hash if e.lastCommit != "" { - commitHash, err = repo.StoreCommitWithParent(treeHash, e.lastCommit) + commitHash, err = repo.StoreCommit(treeHash, e.lastCommit) } else { commitHash, err = repo.StoreCommit(treeHash) } diff --git a/entity/dag/entity_actions.go b/entity/dag/entity_actions.go new file mode 100644 index 0000000000000000000000000000000000000000..8dcf91e663b03faef02a1c9ef70a2372e1b22dd2 --- /dev/null +++ b/entity/dag/entity_actions.go @@ -0,0 +1,227 @@ +package dag + +import ( + "fmt" + + "github.com/pkg/errors" + + "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/repository" +) + +func ListLocalIds(typename string, repo repository.RepoData) ([]entity.Id, error) { + refs, err := repo.ListRefs(fmt.Sprintf("refs/%s/", typename)) + if err != nil { + return nil, err + } + return entity.RefsToIds(refs), nil +} + +// 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//*:refs/remotes///*" + 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 +func Push(def Definition, repo repository.Repo, remote string) (string, error) { + // "refs//*:refs//*" + refspec := fmt.Sprintf("refs/%s/*:refs/%s/*", + def.namespace, def.namespace) + + return repo.PushRefs(remote, refspec) +} + +// Pull will do a Fetch + MergeAll +// Contrary to MergeAll, this function will return an error if a merge fail. +func Pull(def Definition, repo repository.ClockedRepo, remote string) error { + _, err := Fetch(def, repo, remote) + if err != nil { + return err + } + + for merge := range MergeAll(def, repo, remote) { + if merge.Err != nil { + return merge.Err + } + if merge.Status == entity.MergeStatusInvalid { + return errors.Errorf("merge failure: %s", merge.Reason) + } + } + + return nil +} + +func MergeAll(def Definition, repo repository.ClockedRepo, remote string) <-chan entity.MergeResult { + out := make(chan entity.MergeResult) + + // no caching for the merge, we load everything from git even if that means multiple + // copy of the same entity in memory. The cache layer will intercept the results to + // invalidate entities if necessary. + + go func() { + defer close(out) + + remoteRefSpec := fmt.Sprintf("refs/remotes/%s/%s/", remote, def.namespace) + remoteRefs, err := repo.ListRefs(remoteRefSpec) + if err != nil { + out <- entity.MergeResult{Err: err} + return + } + + for _, remoteRef := range remoteRefs { + out <- merge(def, repo, remoteRef) + } + }() + + return out +} + +func merge(def Definition, repo repository.ClockedRepo, remoteRef string) entity.MergeResult { + id := entity.RefToId(remoteRef) + + if err := id.Validate(); err != nil { + return entity.NewMergeInvalidStatus(id, errors.Wrap(err, "invalid ref").Error()) + } + + remoteEntity, err := read(def, repo, remoteRef) + if err != nil { + return entity.NewMergeInvalidStatus(id, + errors.Wrapf(err, "remote %s is not readable", def.typename).Error()) + } + + // Check for error in remote data + if err := remoteEntity.Validate(); err != nil { + return entity.NewMergeInvalidStatus(id, + errors.Wrapf(err, "remote %s data is invalid", def.typename).Error()) + } + + localRef := fmt.Sprintf("refs/%s/%s", def.namespace, id.String()) + + localExist, err := repo.RefExist(localRef) + if err != nil { + return entity.NewMergeError(err, id) + } + + // the bug is not local yet, simply create the reference + if !localExist { + err := repo.CopyRef(remoteRef, localRef) + if err != nil { + return entity.NewMergeError(err, id) + } + + return entity.NewMergeStatus(entity.MergeStatusNew, id, remoteEntity) + } + + // var updated bool + // err = repo.MergeRef(localRef, remoteRef, func() repository.Hash { + // updated = true + // + // }) + // if err != nil { + // return entity.NewMergeError(err, id) + // } + // + // if updated { + // return entity.NewMergeStatus(entity.MergeStatusUpdated, id, ) + // } else { + // return entity.NewMergeStatus(entity.MergeStatusNothing, id, ) + // } + + localCommit, err := repo.ResolveRef(localRef) + if err != nil { + return entity.NewMergeError(err, id) + } + + remoteCommit, err := repo.ResolveRef(remoteRef) + if err != nil { + return entity.NewMergeError(err, id) + } + + if localCommit == remoteCommit { + // nothing to merge + return entity.NewMergeStatus(entity.MergeStatusNothing, id, remoteEntity) + } + + // fast-forward is possible if otherRef include ref + + remoteCommits, err := repo.ListCommits(remoteRef) + if err != nil { + return entity.NewMergeError(err, id) + } + + fastForwardPossible := false + for _, hash := range remoteCommits { + if hash == localCommit { + fastForwardPossible = true + break + } + } + + if fastForwardPossible { + err = repo.UpdateRef(localRef, remoteCommit) + if err != nil { + return entity.NewMergeError(err, id) + } + return entity.NewMergeStatus(entity.MergeStatusUpdated, id, remoteEntity) + } + + // fast-forward is not possible, we need to create a merge commit + // For simplicity when reading and to have clocks that record this change, we store + // an empty operationPack. + // First step is to collect those clocks. + + localEntity, err := read(def, repo, localRef) + if err != nil { + return entity.NewMergeError(err, id) + } + + // err = localEntity.packClock.Witness(remoteEntity.packClock.Time()) + // if err != nil { + // return entity.NewMergeError(err, id) + // } + // + // packTime, err := localEntity.packClock.Increment() + // if err != nil { + // return entity.NewMergeError(err, id) + // } + + editTime, err := repo.Increment(fmt.Sprintf(editClockPattern, def.namespace)) + if err != nil { + return entity.NewMergeError(err, id) + } + + opp := &operationPack{ + Operations: nil, + CreateTime: 0, + EditTime: editTime, + // PackTime: packTime, + } + + treeHash, err := opp.Write(def, repo) + if err != nil { + return entity.NewMergeError(err, id) + } + + // Create the merge commit with two parents + newHash, err := repo.StoreCommit(treeHash, localCommit, remoteCommit) + if err != nil { + return entity.NewMergeError(err, id) + } + + // finally update the ref + err = repo.UpdateRef(localRef, newHash) + if err != nil { + return entity.NewMergeError(err, id) + } + + return entity.NewMergeStatus(entity.MergeStatusUpdated, id, localEntity) +} + +func Remove() error { + panic("") +} diff --git a/entity/dag/entity_test.go b/entity/dag/entity_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c5c835670abd9140c3824e616051a7b059b1538d --- /dev/null +++ b/entity/dag/entity_test.go @@ -0,0 +1,117 @@ +package dag + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestWriteRead(t *testing.T) { + repo, id1, id2, def := makeTestContext() + + entity := New(def) + require.False(t, entity.NeedCommit()) + + entity.Append(newOp1(id1, "foo")) + entity.Append(newOp2(id1, "bar")) + + require.True(t, entity.NeedCommit()) + require.NoError(t, entity.CommitAdNeeded(repo)) + require.False(t, entity.NeedCommit()) + + entity.Append(newOp2(id2, "foobar")) + require.True(t, entity.NeedCommit()) + require.NoError(t, entity.CommitAdNeeded(repo)) + require.False(t, entity.NeedCommit()) + + read, err := Read(def, repo, entity.Id()) + require.NoError(t, err) + + assertEqualEntities(t, entity, read) +} + +func assertEqualEntities(t *testing.T, a, b *Entity) { + // testify doesn't support comparing functions and systematically fail if they are not nil + // so we have to set them to nil temporarily + + backOpUnA := a.Definition.operationUnmarshaler + backOpUnB := b.Definition.operationUnmarshaler + + a.Definition.operationUnmarshaler = nil + b.Definition.operationUnmarshaler = nil + + backIdResA := a.Definition.identityResolver + backIdResB := b.Definition.identityResolver + + a.Definition.identityResolver = nil + b.Definition.identityResolver = nil + + defer func() { + a.Definition.operationUnmarshaler = backOpUnA + b.Definition.operationUnmarshaler = backOpUnB + a.Definition.identityResolver = backIdResA + b.Definition.identityResolver = backIdResB + }() + + require.Equal(t, a, b) +} + +// // Merge +// +// merge1 := makeCommit(t, repo) +// merge1 = makeCommit(t, repo, merge1) +// err = repo.UpdateRef("merge1", merge1) +// require.NoError(t, err) +// +// err = repo.UpdateRef("merge2", merge1) +// require.NoError(t, err) +// +// // identical merge +// err = repo.MergeRef("merge1", "merge2") +// require.NoError(t, err) +// +// refMerge1, err := repo.ResolveRef("merge1") +// require.NoError(t, err) +// require.Equal(t, merge1, refMerge1) +// refMerge2, err := repo.ResolveRef("merge2") +// require.NoError(t, err) +// require.Equal(t, merge1, refMerge2) +// +// // fast-forward merge +// merge2 := makeCommit(t, repo, merge1) +// merge2 = makeCommit(t, repo, merge2) +// +// err = repo.UpdateRef("merge2", merge2) +// require.NoError(t, err) +// +// err = repo.MergeRef("merge1", "merge2") +// require.NoError(t, err) +// +// refMerge1, err = repo.ResolveRef("merge1") +// require.NoError(t, err) +// require.Equal(t, merge2, refMerge1) +// refMerge2, err = repo.ResolveRef("merge2") +// require.NoError(t, err) +// require.Equal(t, merge2, refMerge2) +// +// // merge commit +// merge1 = makeCommit(t, repo, merge1) +// err = repo.UpdateRef("merge1", merge1) +// require.NoError(t, err) +// +// merge2 = makeCommit(t, repo, merge2) +// err = repo.UpdateRef("merge2", merge2) +// require.NoError(t, err) +// +// err = repo.MergeRef("merge1", "merge2") +// require.NoError(t, err) +// +// refMerge1, err = repo.ResolveRef("merge1") +// require.NoError(t, err) +// require.NotEqual(t, merge1, refMerge1) +// commitRefMerge1, err := repo.ReadCommit(refMerge1) +// require.NoError(t, err) +// require.ElementsMatch(t, commitRefMerge1.Parents, []Hash{merge1, merge2}) +// refMerge2, err = repo.ResolveRef("merge2") +// require.NoError(t, err) +// require.Equal(t, merge2, refMerge2) diff --git a/entity/dag/operation.go b/entity/dag/operation.go new file mode 100644 index 0000000000000000000000000000000000000000..9fcc055be607c7a0c064a95e3d3d9affdc160b84 --- /dev/null +++ b/entity/dag/operation.go @@ -0,0 +1,31 @@ +package dag + +import ( + "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/identity" +) + +// Operation is a piece of data defining a change to reflect on the state of an Entity. +// What this Operation or Entity's state looks like is not of the resort of this package as it only deals with the +// data structure and storage. +type Operation interface { + // Id return the Operation identifier + // Some care need to be taken to define a correct Id derivation and enough entropy in the data used to avoid + // collisions. Notably: + // - the Id of the first Operation will be used as the Id of the Entity. Collision need to be avoided across Entities. + // - collisions can also happen within the set of Operations of an Entity. Simple Operation might not have enough + // entropy to yield unique Ids. + // A common way to derive an Id will be to use the DeriveId function on the serialized operation data. + Id() entity.Id + // Validate check if the Operation data is valid + Validate() error + + Author() identity.Interface +} + +type operationBase struct { + author identity.Interface + + // Not serialized. Store the op's id in memory. + id entity.Id +} diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go new file mode 100644 index 0000000000000000000000000000000000000000..7cf4ee580a7d97796cf3d74a1dc59f6f102e7a6c --- /dev/null +++ b/entity/dag/operation_pack.go @@ -0,0 +1,294 @@ +package dag + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/pkg/errors" + "golang.org/x/crypto/openpgp" + + "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/lamport" +) + +// TODO: extra data tree +const extraEntryName = "extra" + +const opsEntryName = "ops" +const versionEntryPrefix = "version-" +const createClockEntryPrefix = "create-clock-" +const editClockEntryPrefix = "edit-clock-" +const packClockEntryPrefix = "pack-clock-" + +// operationPack is a wrapper structure to store multiple operations in a single git blob. +// Additionally, it holds and store the metadata for those operations. +type operationPack struct { + // An identifier, taken from a hash of the serialized Operations. + id entity.Id + + // The author of the Operations. Must be the same author for all the Operations. + Author identity.Interface + // The list of Operation stored in the operationPack + Operations []Operation + // Encode the entity's logical time of creation across all entities of the same type. + // Only exist on the root operationPack + CreateTime lamport.Time + // Encode the entity's logical time of last edition across all entities of the same type. + // Exist on all operationPack + EditTime lamport.Time + // // Encode the operationPack's logical time of creation withing this entity. + // // Exist on all operationPack + // PackTime lamport.Time +} + +func (opp *operationPack) Id() entity.Id { + if opp.id == "" || opp.id == entity.UnsetId { + // This means we are trying to get the opp's Id *before* it has been stored. + // As the Id is computed based on the actual bytes written on the disk, we are going to predict + // those and then get the Id. This is safe as it will be the exact same code writing on disk later. + + data, err := json.Marshal(opp) + if err != nil { + panic(err) + } + opp.id = entity.DeriveId(data) + } + + return opp.id +} + +func (opp *operationPack) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Author identity.Interface `json:"author"` + Operations []Operation `json:"ops"` + }{ + Author: opp.Author, + Operations: opp.Operations, + }) +} + +func (opp *operationPack) Validate() error { + if opp.Author == nil { + return fmt.Errorf("missing author") + } + for _, op := range opp.Operations { + if op.Author() != opp.Author { + return fmt.Errorf("operation has different author than the operationPack's") + } + } + if opp.EditTime == 0 { + return fmt.Errorf("lamport edit time is zero") + } + return nil +} + +func (opp *operationPack) Write(def Definition, repo repository.RepoData, parentCommit ...repository.Hash) (repository.Hash, error) { + if err := opp.Validate(); err != nil { + return "", err + } + + // For different reason, we store the clocks and format version directly in the git tree. + // Version has to be accessible before any attempt to decode to return early with a unique error. + // Clocks could possibly be stored in the git blob but it's nice to separate data and metadata, and + // we are storing something directly in the tree already so why not. + // + // To have a valid Tree, we point the "fake" entries to always the same value, the empty blob. + emptyBlobHash, err := repo.StoreData([]byte{}) + if err != nil { + return "", err + } + + // Write the Ops as a Git blob containing the serialized array of operations + data, err := json.Marshal(opp) + if err != nil { + return "", err + } + + // compute the Id while we have the serialized data + opp.id = entity.DeriveId(data) + + hash, err := repo.StoreData(data) + if err != nil { + return "", err + } + + // Make a Git tree referencing this blob and encoding the other values: + // - format version + // - clocks + tree := []repository.TreeEntry{ + {ObjectType: repository.Blob, Hash: emptyBlobHash, + Name: fmt.Sprintf(versionEntryPrefix+"%d", def.formatVersion)}, + {ObjectType: repository.Blob, Hash: hash, + Name: opsEntryName}, + {ObjectType: repository.Blob, Hash: emptyBlobHash, + Name: fmt.Sprintf(editClockEntryPrefix+"%d", opp.EditTime)}, + // {ObjectType: repository.Blob, Hash: emptyBlobHash, + // Name: fmt.Sprintf(packClockEntryPrefix+"%d", opp.PackTime)}, + } + if opp.CreateTime > 0 { + tree = append(tree, repository.TreeEntry{ + ObjectType: repository.Blob, + Hash: emptyBlobHash, + Name: fmt.Sprintf(createClockEntryPrefix+"%d", opp.CreateTime), + }) + } + + // Store the tree + treeHash, err := repo.StoreTree(tree) + if err != nil { + return "", err + } + + // Write a Git commit referencing the tree, with the previous commit as parent + // If we have keys, sign. + var commitHash repository.Hash + + // Sign the commit if we have a key + if opp.Author.SigningKey() != nil { + commitHash, err = repo.StoreSignedCommit(treeHash, opp.Author.SigningKey().PGPEntity(), parentCommit...) + } else { + commitHash, err = repo.StoreCommit(treeHash, parentCommit...) + } + + if err != nil { + return "", err + } + + return commitHash, nil +} + +// readOperationPack read the operationPack encoded in git at the given Tree hash. +// +// Validity of the Lamport clocks is left for the caller to decide. +func readOperationPack(def Definition, repo repository.RepoData, commit repository.Commit) (*operationPack, error) { + entries, err := repo.ReadTree(commit.TreeHash) + if err != nil { + return nil, err + } + + // check the format version first, fail early instead of trying to read something + var version uint + for _, entry := range entries { + if strings.HasPrefix(entry.Name, versionEntryPrefix) { + v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, versionEntryPrefix), 10, 64) + if err != nil { + return nil, errors.Wrap(err, "can't read format version") + } + if v > 1<<12 { + return nil, fmt.Errorf("format version too big") + } + version = uint(v) + break + } + } + if version == 0 { + return nil, entity.NewErrUnknowFormat(def.formatVersion) + } + if version != def.formatVersion { + return nil, entity.NewErrInvalidFormat(version, def.formatVersion) + } + + var id entity.Id + var author identity.Interface + var ops []Operation + var createTime lamport.Time + var editTime lamport.Time + // var packTime lamport.Time + + for _, entry := range entries { + switch { + case entry.Name == opsEntryName: + data, err := repo.ReadData(entry.Hash) + if err != nil { + return nil, errors.Wrap(err, "failed to read git blob data") + } + ops, author, err = unmarshallPack(def, data) + if err != nil { + return nil, err + } + id = entity.DeriveId(data) + + case strings.HasPrefix(entry.Name, createClockEntryPrefix): + v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, createClockEntryPrefix), 10, 64) + if err != nil { + return nil, errors.Wrap(err, "can't read creation lamport time") + } + createTime = lamport.Time(v) + + case strings.HasPrefix(entry.Name, editClockEntryPrefix): + v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, editClockEntryPrefix), 10, 64) + if err != nil { + return nil, errors.Wrap(err, "can't read edit lamport time") + } + editTime = lamport.Time(v) + + // case strings.HasPrefix(entry.Name, packClockEntryPrefix): + // found &= 1 << 3 + // + // v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, packClockEntryPrefix), 10, 64) + // if err != nil { + // return nil, errors.Wrap(err, "can't read pack lamport time") + // } + // packTime = lamport.Time(v) + } + } + + // Verify signature if we expect one + keys := author.ValidKeysAtTime(fmt.Sprintf(editClockPattern, def.namespace), editTime) + if len(keys) > 0 { + keyring := identity.PGPKeyring(keys) + _, err = openpgp.CheckDetachedSignature(keyring, commit.SignedData, commit.Signature) + if err != nil { + return nil, fmt.Errorf("signature failure: %v", err) + } + } + + return &operationPack{ + id: id, + Author: author, + Operations: ops, + CreateTime: createTime, + EditTime: editTime, + // PackTime: packTime, + }, nil +} + +// unmarshallPack delegate the unmarshalling of the Operation's JSON to the decoding +// function provided by the concrete entity. This gives access to the concrete type of each +// Operation. +func unmarshallPack(def Definition, data []byte) ([]Operation, identity.Interface, error) { + aux := struct { + Author identity.IdentityStub `json:"author"` + Operations []json.RawMessage `json:"ops"` + }{} + + if err := json.Unmarshal(data, &aux); err != nil { + return nil, nil, err + } + + if aux.Author.Id() == "" || aux.Author.Id() == entity.UnsetId { + return nil, nil, fmt.Errorf("missing author") + } + + author, err := def.identityResolver.ResolveIdentity(aux.Author.Id()) + if err != nil { + return nil, nil, err + } + + ops := make([]Operation, 0, len(aux.Operations)) + + for _, raw := range aux.Operations { + // delegate to specialized unmarshal function + op, err := def.operationUnmarshaler(author, raw) + if err != nil { + return nil, nil, err + } + ops = append(ops, op) + } + + return ops, author, nil +} diff --git a/entity/dag/operation_pack_test.go b/entity/dag/operation_pack_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ad2a985967c91d76d6b7f5743c50986ae81ed472 --- /dev/null +++ b/entity/dag/operation_pack_test.go @@ -0,0 +1,44 @@ +package dag + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOperationPackReadWrite(t *testing.T) { + repo, id1, _, def := makeTestContext() + + opp := &operationPack{ + Author: id1, + Operations: []Operation{ + newOp1(id1, "foo"), + newOp2(id1, "bar"), + }, + CreateTime: 123, + EditTime: 456, + } + + commitHash, err := opp.Write(def, repo) + require.NoError(t, err) + + commit, err := repo.ReadCommit(commitHash) + require.NoError(t, err) + + opp2, err := readOperationPack(def, repo, commit) + require.NoError(t, err) + + require.Equal(t, opp, opp2) + + // make sure we get the same Id with the same data + opp3 := &operationPack{ + Author: id1, + Operations: []Operation{ + newOp1(id1, "foo"), + newOp2(id1, "bar"), + }, + CreateTime: 123, + EditTime: 456, + } + require.Equal(t, opp.Id(), opp3.Id()) +} diff --git a/entity/doc.go b/entity/doc.go deleted file mode 100644 index 4682d545a9925e9b00be01515f9a03dac76ebc25..0000000000000000000000000000000000000000 --- a/entity/doc.go +++ /dev/null @@ -1,8 +0,0 @@ -// Package entity contains the base common code to define an entity stored -// in a chain of git objects, supporting actions like Push, Pull and Merge. -package entity - -// TODO: Bug and Identity are very similar, right ? I expect that this package -// will eventually hold the common code to define an entity and the related -// helpers, errors and so on. When this work is done, it will become easier -// to add new entities, for example to support pull requests. diff --git a/entity/entity_actions.go b/entity/entity_actions.go deleted file mode 100644 index 34e76a6252f374fbcbf2e60184851c54887febdb..0000000000000000000000000000000000000000 --- a/entity/entity_actions.go +++ /dev/null @@ -1,31 +0,0 @@ -package entity - -import ( - "fmt" - - "github.com/MichaelMure/git-bug/repository" -) - -func ListLocalIds(typename string, repo repository.RepoData) ([]Id, error) { - refs, err := repo.ListRefs(fmt.Sprintf("refs/%s/", typename)) - if err != nil { - return nil, err - } - return RefsToIds(refs), nil -} - -func Fetch() { - -} - -func Pull() { - -} - -func Push() { - -} - -func Remove() error { - panic("") -} diff --git a/entity/entity_test.go b/entity/entity_test.go deleted file mode 100644 index 92a531796ac57d31c72e2528cbe6c9412693b2c5..0000000000000000000000000000000000000000 --- a/entity/entity_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package entity - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/repository" -) - -// func TestFoo(t *testing.T) { -// repo, err := repository.OpenGoGitRepo("~/dev/git-bug", nil) -// require.NoError(t, err) -// -// b, err := ReadBug(repo, Id("8b22e548c93a6ed23c31fd4e337c6286c3d1e5c9cae5537bc8e5842e11bd1099")) -// require.NoError(t, err) -// -// fmt.Println(b) -// } - -type op1 struct { - OperationType int `json:"type"` - Field1 string `json:"field_1"` -} - -func newOp1(field1 string) *op1 { - return &op1{OperationType: 1, Field1: field1} -} - -func (o op1) Id() Id { - data, _ := json.Marshal(o) - return DeriveId(data) -} - -func (o op1) Validate() error { return nil } - -type op2 struct { - OperationType int `json:"type"` - Field2 string `json:"field_2"` -} - -func newOp2(field2 string) *op2 { - return &op2{OperationType: 2, Field2: field2} -} - -func (o op2) Id() Id { - data, _ := json.Marshal(o) - return DeriveId(data) -} - -func (o op2) Validate() error { return nil } - -var def = Definition{ - typename: "foo", - namespace: "foos", - operationUnmarshaler: unmarshaller, - formatVersion: 1, -} - -func unmarshaller(raw json.RawMessage) (Operation, error) { - var t struct { - OperationType int `json:"type"` - } - - if err := json.Unmarshal(raw, &t); err != nil { - return nil, err - } - - switch t.OperationType { - case 1: - op := &op1{} - err := json.Unmarshal(raw, &op) - return op, err - case 2: - op := &op2{} - err := json.Unmarshal(raw, &op) - return op, err - default: - return nil, fmt.Errorf("unknown operation type %v", t.OperationType) - } -} - -func TestWriteRead(t *testing.T) { - repo := repository.NewMockRepo() - - entity := New(def) - require.False(t, entity.NeedCommit()) - - entity.Append(newOp1("foo")) - entity.Append(newOp2("bar")) - - require.True(t, entity.NeedCommit()) - require.NoError(t, entity.CommitAdNeeded(repo)) - require.False(t, entity.NeedCommit()) - - entity.Append(newOp2("foobar")) - require.True(t, entity.NeedCommit()) - require.NoError(t, entity.CommitAdNeeded(repo)) - require.False(t, entity.NeedCommit()) - - read, err := Read(def, repo, entity.Id()) - require.NoError(t, err) - - fmt.Println(*read) -} diff --git a/entity/merge.go b/entity/merge.go index 3ce8edac764f8f75241c969ed8bfc68b3e42fc41..7d1f3f432ebb67ef80bc9880cba6c3379d4e282c 100644 --- a/entity/merge.go +++ b/entity/merge.go @@ -8,14 +8,15 @@ import ( type MergeStatus int const ( - _ MergeStatus = iota - MergeStatusNew - MergeStatusInvalid - MergeStatusUpdated - MergeStatusNothing - MergeStatusError + _ MergeStatus = iota + MergeStatusNew // a new Entity was created locally + MergeStatusInvalid // the remote data is invalid + MergeStatusUpdated // a local Entity has been updated + MergeStatusNothing // no changes were made to a local Entity (already up to date) + MergeStatusError // a terminal error happened ) +// MergeResult hold the result of a merge operation on an Entity. type MergeResult struct { // Err is set when a terminal error occur in the process Err error @@ -55,6 +56,7 @@ func NewMergeError(err error, id Id) MergeResult { } } +// TODO: Interface --> *Entity ? func NewMergeStatus(status MergeStatus, id Id, entity Interface) MergeResult { return MergeResult{ Id: id, diff --git a/entity/operation_pack.go b/entity/operation_pack.go deleted file mode 100644 index 0a16dd61e9d044dfcbb063c16dd7af0b1893f3cd..0000000000000000000000000000000000000000 --- a/entity/operation_pack.go +++ /dev/null @@ -1,199 +0,0 @@ -package entity - -import ( - "encoding/json" - "fmt" - "strconv" - "strings" - - "github.com/pkg/errors" - - "github.com/MichaelMure/git-bug/repository" - "github.com/MichaelMure/git-bug/util/lamport" -) - -// TODO: extra data tree -const extraEntryName = "extra" - -const opsEntryName = "ops" -const versionEntryPrefix = "version-" -const createClockEntryPrefix = "create-clock-" -const editClockEntryPrefix = "edit-clock-" -const packClockEntryPrefix = "pack-clock-" - -type operationPack struct { - Operations []Operation - // Encode the entity's logical time of creation across all entities of the same type. - // Only exist on the root operationPack - CreateTime lamport.Time - // Encode the entity's logical time of last edition across all entities of the same type. - // Exist on all operationPack - EditTime lamport.Time - // Encode the operationPack's logical time of creation withing this entity. - // Exist on all operationPack - PackTime lamport.Time -} - -func (opp operationPack) write(def Definition, repo repository.RepoData) (repository.Hash, error) { - // For different reason, we store the clocks and format version directly in the git tree. - // Version has to be accessible before any attempt to decode to return early with a unique error. - // Clocks could possibly be stored in the git blob but it's nice to separate data and metadata, and - // we are storing something directly in the tree already so why not. - // - // To have a valid Tree, we point the "fake" entries to always the same value, the empty blob. - emptyBlobHash, err := repo.StoreData([]byte{}) - if err != nil { - return "", err - } - - // Write the Ops as a Git blob containing the serialized array - data, err := json.Marshal(struct { - Operations []Operation `json:"ops"` - }{ - Operations: opp.Operations, - }) - if err != nil { - return "", err - } - hash, err := repo.StoreData(data) - if err != nil { - return "", err - } - - // Make a Git tree referencing this blob and encoding the other values: - // - format version - // - clocks - tree := []repository.TreeEntry{ - {ObjectType: repository.Blob, Hash: emptyBlobHash, - Name: fmt.Sprintf(versionEntryPrefix+"%d", def.formatVersion)}, - {ObjectType: repository.Blob, Hash: hash, - Name: opsEntryName}, - {ObjectType: repository.Blob, Hash: emptyBlobHash, - Name: fmt.Sprintf(editClockEntryPrefix+"%d", opp.EditTime)}, - {ObjectType: repository.Blob, Hash: emptyBlobHash, - Name: fmt.Sprintf(packClockEntryPrefix+"%d", opp.PackTime)}, - } - if opp.CreateTime > 0 { - tree = append(tree, repository.TreeEntry{ - ObjectType: repository.Blob, - Hash: emptyBlobHash, - Name: fmt.Sprintf(createClockEntryPrefix+"%d", opp.CreateTime), - }) - } - - // Store the tree - return repo.StoreTree(tree) -} - -// readOperationPack read the operationPack encoded in git at the given Tree hash. -// -// Validity of the Lamport clocks is left for the caller to decide. -func readOperationPack(def Definition, repo repository.RepoData, treeHash repository.Hash) (*operationPack, error) { - entries, err := repo.ReadTree(treeHash) - if err != nil { - return nil, err - } - - // check the format version first, fail early instead of trying to read something - var version uint - for _, entry := range entries { - if strings.HasPrefix(entry.Name, versionEntryPrefix) { - v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, versionEntryPrefix), 10, 64) - if err != nil { - return nil, errors.Wrap(err, "can't read format version") - } - if v > 1<<12 { - return nil, fmt.Errorf("format version too big") - } - version = uint(v) - break - } - } - if version == 0 { - return nil, NewErrUnknowFormat(def.formatVersion) - } - if version != def.formatVersion { - return nil, NewErrInvalidFormat(version, def.formatVersion) - } - - var ops []Operation - var createTime lamport.Time - var editTime lamport.Time - var packTime lamport.Time - - for _, entry := range entries { - if entry.Name == opsEntryName { - data, err := repo.ReadData(entry.Hash) - if err != nil { - return nil, errors.Wrap(err, "failed to read git blob data") - } - - ops, err = unmarshallOperations(def, data) - if err != nil { - return nil, err - } - continue - } - - if strings.HasPrefix(entry.Name, createClockEntryPrefix) { - v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, createClockEntryPrefix), 10, 64) - if err != nil { - return nil, errors.Wrap(err, "can't read creation lamport time") - } - createTime = lamport.Time(v) - continue - } - - if strings.HasPrefix(entry.Name, editClockEntryPrefix) { - v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, editClockEntryPrefix), 10, 64) - if err != nil { - return nil, errors.Wrap(err, "can't read edit lamport time") - } - editTime = lamport.Time(v) - continue - } - - if strings.HasPrefix(entry.Name, packClockEntryPrefix) { - v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, packClockEntryPrefix), 10, 64) - if err != nil { - return nil, errors.Wrap(err, "can't read pack lamport time") - } - packTime = lamport.Time(v) - continue - } - } - - return &operationPack{ - Operations: ops, - CreateTime: createTime, - EditTime: editTime, - PackTime: packTime, - }, nil -} - -// unmarshallOperations delegate the unmarshalling of the Operation's JSON to the decoding -// function provided by the concrete entity. This gives access to the concrete type of each -// Operation. -func unmarshallOperations(def Definition, data []byte) ([]Operation, error) { - aux := struct { - Operations []json.RawMessage `json:"ops"` - }{} - - if err := json.Unmarshal(data, &aux); err != nil { - return nil, err - } - - ops := make([]Operation, 0, len(aux.Operations)) - - for _, raw := range aux.Operations { - // delegate to specialized unmarshal function - op, err := def.operationUnmarshaler(raw) - if err != nil { - return nil, err - } - - ops = append(ops, op) - } - - return ops, nil -} diff --git a/entity/refs.go b/entity/refs.go index f505dbf01ee99480d9ddc349b1113ff510cf74ed..070d4dbaab54a4f3f41320d66df8a5ae32765e8d 100644 --- a/entity/refs.go +++ b/entity/refs.go @@ -2,6 +2,7 @@ package entity import "strings" +// RefsToIds parse a slice of git references and return the corresponding Entity's Id. func RefsToIds(refs []string) []Id { ids := make([]Id, len(refs)) @@ -12,6 +13,7 @@ func RefsToIds(refs []string) []Id { return ids } +// RefsToIds parse a git reference and return the corresponding Entity's Id. func RefToId(ref string) Id { split := strings.Split(ref, "/") return Id(split[len(split)-1]) diff --git a/identity/identity.go b/identity/identity.go index ef488712ebcc1e99ff08fc8efce0feed08da5d8c..650190417a36a464a3d2c0c0613e94b3dcb79dc6 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -344,7 +344,7 @@ func (i *Identity) Commit(repo repository.ClockedRepo) error { var commitHash repository.Hash if lastCommit != "" { - commitHash, err = repo.StoreCommitWithParent(treeHash, lastCommit) + commitHash, err = repo.StoreCommit(treeHash, lastCommit) } else { commitHash, err = repo.StoreCommit(treeHash) } @@ -518,6 +518,15 @@ func (i *Identity) Keys() []*Key { return i.lastVersion().keys } +// SigningKey return the key that should be used to sign new messages. If no key is available, return nil. +func (i *Identity) SigningKey() *Key { + keys := i.Keys() + if len(keys) > 0 { + return keys[0] + } + return nil +} + // ValidKeysAtTime return the set of keys valid at a given lamport time func (i *Identity) ValidKeysAtTime(clockName string, time lamport.Time) []*Key { var result []*Key diff --git a/identity/identity_actions_test.go b/identity/identity_actions_test.go index 63f6aacd7347f68fa8ea4547359af94d20e8b66b..54cb2a4682c610cfdfcdfd33f2652883a163f27d 100644 --- a/identity/identity_actions_test.go +++ b/identity/identity_actions_test.go @@ -9,7 +9,7 @@ import ( ) func TestPushPull(t *testing.T) { - repoA, repoB, remote := repository.SetupReposAndRemote() + repoA, repoB, remote := repository.SetupGoGitReposAndRemote() defer repository.CleanupTestRepos(repoA, repoB, remote) identity1, err := NewIdentity(repoA, "name1", "email1") diff --git a/identity/identity_stub.go b/identity/identity_stub.go index fec9201086d76e83069373d7fc66531a70fcc64c..919453782cb62552eeab43e00096b167f811f957 100644 --- a/identity/identity_stub.go +++ b/identity/identity_stub.go @@ -71,6 +71,10 @@ func (IdentityStub) Keys() []*Key { panic("identities needs to be properly loaded with identity.ReadLocal()") } +func (i *IdentityStub) SigningKey() *Key { + panic("identities needs to be properly loaded with identity.ReadLocal()") +} + func (IdentityStub) ValidKeysAtTime(_ string, _ lamport.Time) []*Key { panic("identities needs to be properly loaded with identity.ReadLocal()") } diff --git a/identity/identity_test.go b/identity/identity_test.go index ad8317ce4fc3101c6168905a84e0f6238d6c5410..2cdb4b3653c38a620d1475bee8c972a9034148d4 100644 --- a/identity/identity_test.go +++ b/identity/identity_test.go @@ -36,18 +36,18 @@ func TestIdentityCommitLoad(t *testing.T) { // multiple versions - identity, err = NewIdentityFull(repo, "René Descartes", "rene.descartes@example.com", "", "", []*Key{{PubKey: "pubkeyA"}}) + identity, err = NewIdentityFull(repo, "René Descartes", "rene.descartes@example.com", "", "", []*Key{generatePublicKey()}) require.NoError(t, err) idBeforeCommit = identity.Id() err = identity.Mutate(repo, func(orig *Mutator) { - orig.Keys = []*Key{{PubKey: "pubkeyB"}} + orig.Keys = []*Key{generatePublicKey()} }) require.NoError(t, err) err = identity.Mutate(repo, func(orig *Mutator) { - orig.Keys = []*Key{{PubKey: "pubkeyC"}} + orig.Keys = []*Key{generatePublicKey()} }) require.NoError(t, err) @@ -70,13 +70,13 @@ func TestIdentityCommitLoad(t *testing.T) { err = identity.Mutate(repo, func(orig *Mutator) { orig.Email = "rene@descartes.com" - orig.Keys = []*Key{{PubKey: "pubkeyD"}} + orig.Keys = []*Key{generatePublicKey()} }) require.NoError(t, err) err = identity.Mutate(repo, func(orig *Mutator) { orig.Email = "rene@descartes.com" - orig.Keys = []*Key{{PubKey: "pubkeyD"}, {PubKey: "pubkeyE"}} + orig.Keys = []*Key{generatePublicKey(), generatePublicKey()} }) require.NoError(t, err) @@ -123,49 +123,45 @@ func commitsAreSet(t *testing.T, identity *Identity) { // Test that the correct crypto keys are returned for a given lamport time func TestIdentity_ValidKeysAtTime(t *testing.T) { + pubKeyA := generatePublicKey() + pubKeyB := generatePublicKey() + pubKeyC := generatePublicKey() + pubKeyD := generatePublicKey() + pubKeyE := generatePublicKey() + identity := Identity{ versions: []*version{ { times: map[string]lamport.Time{"foo": 100}, - keys: []*Key{ - {PubKey: "pubkeyA"}, - }, + keys: []*Key{pubKeyA}, }, { times: map[string]lamport.Time{"foo": 200}, - keys: []*Key{ - {PubKey: "pubkeyB"}, - }, + keys: []*Key{pubKeyB}, }, { times: map[string]lamport.Time{"foo": 201}, - keys: []*Key{ - {PubKey: "pubkeyC"}, - }, + keys: []*Key{pubKeyC}, }, { times: map[string]lamport.Time{"foo": 201}, - keys: []*Key{ - {PubKey: "pubkeyD"}, - }, + keys: []*Key{pubKeyD}, }, { times: map[string]lamport.Time{"foo": 300}, - keys: []*Key{ - {PubKey: "pubkeyE"}, - }, + keys: []*Key{pubKeyE}, }, }, } require.Nil(t, identity.ValidKeysAtTime("foo", 10)) - require.Equal(t, identity.ValidKeysAtTime("foo", 100), []*Key{{PubKey: "pubkeyA"}}) - require.Equal(t, identity.ValidKeysAtTime("foo", 140), []*Key{{PubKey: "pubkeyA"}}) - require.Equal(t, identity.ValidKeysAtTime("foo", 200), []*Key{{PubKey: "pubkeyB"}}) - require.Equal(t, identity.ValidKeysAtTime("foo", 201), []*Key{{PubKey: "pubkeyD"}}) - require.Equal(t, identity.ValidKeysAtTime("foo", 202), []*Key{{PubKey: "pubkeyD"}}) - require.Equal(t, identity.ValidKeysAtTime("foo", 300), []*Key{{PubKey: "pubkeyE"}}) - require.Equal(t, identity.ValidKeysAtTime("foo", 3000), []*Key{{PubKey: "pubkeyE"}}) + require.Equal(t, identity.ValidKeysAtTime("foo", 100), []*Key{pubKeyA}) + require.Equal(t, identity.ValidKeysAtTime("foo", 140), []*Key{pubKeyA}) + require.Equal(t, identity.ValidKeysAtTime("foo", 200), []*Key{pubKeyB}) + require.Equal(t, identity.ValidKeysAtTime("foo", 201), []*Key{pubKeyD}) + require.Equal(t, identity.ValidKeysAtTime("foo", 202), []*Key{pubKeyD}) + require.Equal(t, identity.ValidKeysAtTime("foo", 300), []*Key{pubKeyE}) + require.Equal(t, identity.ValidKeysAtTime("foo", 3000), []*Key{pubKeyE}) } // Test the immutable or mutable metadata search diff --git a/identity/interface.go b/identity/interface.go index 92a03c510982f3fa4cd71f411d5e2c9aa567b2c6..528cb067612caca62ce74399fffe2efdc493dc6e 100644 --- a/identity/interface.go +++ b/identity/interface.go @@ -36,6 +36,9 @@ type Interface interface { // Can be empty. Keys() []*Key + // SigningKey return the key that should be used to sign new messages. If no key is available, return nil. + SigningKey() *Key + // ValidKeysAtTime return the set of keys valid at a given lamport time for a given clock of another entity // Can be empty. ValidKeysAtTime(clockName string, time lamport.Time) []*Key diff --git a/identity/key.go b/identity/key.go index cc948394aeddc438e1eb7961036b53259459fd92..8dd5e8c12f51007ffc5cd3e3834ef9725b42021f 100644 --- a/identity/key.go +++ b/identity/key.go @@ -1,18 +1,193 @@ package identity +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "strings" + "time" + + "github.com/pkg/errors" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + "golang.org/x/crypto/openpgp/packet" + + "github.com/MichaelMure/git-bug/repository" +) + type Key struct { - // The GPG fingerprint of the key - Fingerprint string `json:"fingerprint"` - PubKey string `json:"pub_key"` + public *packet.PublicKey + private *packet.PrivateKey +} + +// GenerateKey generate a keypair (public+private) +func GenerateKey() *Key { + entity, err := openpgp.NewEntity("", "", "", &packet.Config{ + // The armored format doesn't include the creation time, which makes the round-trip data not being fully equal. + // We don't care about the creation time so we can set it to the zero value. + Time: func() time.Time { + return time.Time{} + }, + }) + if err != nil { + panic(err) + } + return &Key{ + public: entity.PrimaryKey, + private: entity.PrivateKey, + } +} + +// generatePublicKey generate only a public key (only useful for testing) +// See GenerateKey for the details. +func generatePublicKey() *Key { + k := GenerateKey() + k.private = nil + return k +} + +func (k *Key) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + w, err := armor.Encode(&buf, openpgp.PublicKeyType, nil) + if err != nil { + return nil, err + } + err = k.public.Serialize(w) + if err != nil { + return nil, err + } + err = w.Close() + if err != nil { + return nil, err + } + return json.Marshal(buf.String()) +} + +func (k *Key) UnmarshalJSON(data []byte) error { + var armored string + err := json.Unmarshal(data, &armored) + if err != nil { + return err + } + + block, err := armor.Decode(strings.NewReader(armored)) + if err == io.EOF { + return fmt.Errorf("no armored data found") + } + if err != nil { + return err + } + + if block.Type != openpgp.PublicKeyType { + return fmt.Errorf("invalid key type") + } + + reader := packet.NewReader(block.Body) + p, err := reader.Next() + if err != nil { + return errors.Wrap(err, "failed to read public key packet") + } + + public, ok := p.(*packet.PublicKey) + if !ok { + return errors.New("got no packet.publicKey") + } + + // The armored format doesn't include the creation time, which makes the round-trip data not being fully equal. + // We don't care about the creation time so we can set it to the zero value. + public.CreationTime = time.Time{} + + k.public = public + return nil } func (k *Key) Validate() error { - // Todo + if k.public == nil { + return fmt.Errorf("nil public key") + } + if !k.public.CanSign() { + return fmt.Errorf("public key can't sign") + } + + if k.private != nil { + if !k.private.CanSign() { + return fmt.Errorf("private key can't sign") + } + } return nil } func (k *Key) Clone() *Key { - clone := *k - return &clone + clone := &Key{} + + pub := *k.public + clone.public = &pub + + if k.private != nil { + priv := *k.private + clone.private = &priv + } + + return clone +} + +func (k *Key) EnsurePrivateKey(repo repository.RepoKeyring) error { + if k.private != nil { + return nil + } + + // item, err := repo.Keyring().Get(k.Fingerprint()) + // if err != nil { + // return fmt.Errorf("no private key found for %s", k.Fingerprint()) + // } + // + + panic("TODO") +} + +func (k *Key) Fingerprint() string { + return string(k.public.Fingerprint[:]) +} + +func (k *Key) PGPEntity() *openpgp.Entity { + return &openpgp.Entity{ + PrimaryKey: k.public, + PrivateKey: k.private, + } +} + +var _ openpgp.KeyRing = &PGPKeyring{} + +// PGPKeyring implement a openpgp.KeyRing from an slice of Key +type PGPKeyring []*Key + +func (pk PGPKeyring) KeysById(id uint64) []openpgp.Key { + var result []openpgp.Key + for _, key := range pk { + if key.public.KeyId == id { + result = append(result, openpgp.Key{ + PublicKey: key.public, + PrivateKey: key.private, + }) + } + } + return result +} + +func (pk PGPKeyring) KeysByIdUsage(id uint64, requiredUsage byte) []openpgp.Key { + // the only usage we care about is the ability to sign, which all keys should already be capable of + return pk.KeysById(id) +} + +func (pk PGPKeyring) DecryptionKeys() []openpgp.Key { + result := make([]openpgp.Key, len(pk)) + for i, key := range pk { + result[i] = openpgp.Key{ + PublicKey: key.public, + PrivateKey: key.private, + } + } + return result } diff --git a/identity/key_test.go b/identity/key_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3206c34eea8f7c87e3ca2652f171174b603d150e --- /dev/null +++ b/identity/key_test.go @@ -0,0 +1,21 @@ +package identity + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestKeyJSON(t *testing.T) { + k := generatePublicKey() + + data, err := json.Marshal(k) + require.NoError(t, err) + + var read Key + err = json.Unmarshal(data, &read) + require.NoError(t, err) + + require.Equal(t, k, &read) +} diff --git a/identity/version_test.go b/identity/version_test.go index 1efa0d03cb2ad00147008a7e3ab3f45b9cdac8ad..385ad4d7ca32d2dc355bb6e17559416c854434d4 100644 --- a/identity/version_test.go +++ b/identity/version_test.go @@ -18,29 +18,23 @@ func makeIdentityTestRepo(t *testing.T) repository.ClockedRepo { clock1, err := repo.GetOrCreateClock("foo") require.NoError(t, err) - err = clock1.Witness(42) // clock goes to 43 + err = clock1.Witness(42) require.NoError(t, err) clock2, err := repo.GetOrCreateClock("bar") require.NoError(t, err) - err = clock2.Witness(34) // clock goes to 35 + err = clock2.Witness(34) require.NoError(t, err) return repo } -func TestVersionSerialize(t *testing.T) { +func TestVersionJSON(t *testing.T) { repo := makeIdentityTestRepo(t) keys := []*Key{ - { - Fingerprint: "fingerprint1", - PubKey: "pubkey1", - }, - { - Fingerprint: "fingerprint2", - PubKey: "pubkey2", - }, + generatePublicKey(), + generatePublicKey(), } before, err := newVersion(repo, "name", "email", "login", "avatarUrl", keys) @@ -57,8 +51,8 @@ func TestVersionSerialize(t *testing.T) { avatarURL: "avatarUrl", unixTime: time.Now().Unix(), times: map[string]lamport.Time{ - "foo": 43, - "bar": 35, + "foo": 42, + "bar": 34, }, keys: keys, nonce: before.nonce, diff --git a/repository/common.go b/repository/common.go new file mode 100644 index 0000000000000000000000000000000000000000..7fd7ae19a98e2b586ac4c276a2a83bd6bd742ff1 --- /dev/null +++ b/repository/common.go @@ -0,0 +1,120 @@ +package repository + +import ( + "io" + + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + "golang.org/x/crypto/openpgp/errors" +) + +// nonNativeMerge is an implementation of a branch merge, for the case where +// the underlying git implementation doesn't support it natively. +func nonNativeMerge(repo RepoData, ref string, otherRef string, treeHashFn func() Hash) error { + commit, err := repo.ResolveRef(ref) + if err != nil { + return err + } + + otherCommit, err := repo.ResolveRef(otherRef) + if err != nil { + return err + } + + if commit == otherCommit { + // nothing to merge + return nil + } + + // fast-forward is possible if otherRef include ref + + otherCommits, err := repo.ListCommits(otherRef) + if err != nil { + return err + } + + fastForwardPossible := false + for _, hash := range otherCommits { + if hash == commit { + fastForwardPossible = true + break + } + } + + if fastForwardPossible { + return repo.UpdateRef(ref, otherCommit) + } + + // fast-forward is not possible, we need to create a merge commit + + // we need a Tree to make the commit, an empty Tree will do + emptyTreeHash, err := repo.StoreTree(nil) + if err != nil { + return err + } + + newHash, err := repo.StoreCommit(emptyTreeHash, commit, otherCommit) + if err != nil { + return err + } + + return repo.UpdateRef(ref, newHash) +} + +// nonNativeListCommits is an implementation for ListCommits, for the case where +// the underlying git implementation doesn't support if natively. +func nonNativeListCommits(repo RepoData, ref string) ([]Hash, error) { + var result []Hash + + stack := make([]Hash, 0, 32) + visited := make(map[Hash]struct{}) + + hash, err := repo.ResolveRef(ref) + if err != nil { + return nil, err + } + + stack = append(stack, hash) + + for len(stack) > 0 { + // pop + hash := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + if _, ok := visited[hash]; ok { + continue + } + + // mark as visited + visited[hash] = struct{}{} + result = append(result, hash) + + commit, err := repo.ReadCommit(hash) + if err != nil { + return nil, err + } + + for _, parent := range commit.Parents { + stack = append(stack, parent) + } + } + + // reverse + for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 { + result[i], result[j] = result[j], result[i] + } + + return result, nil +} + +// deArmorSignature convert an armored (text serialized) signature into raw binary +func deArmorSignature(armoredSig io.Reader) (io.Reader, error) { + block, err := armor.Decode(armoredSig) + if err != nil { + return nil, err + } + if block.Type != openpgp.SignatureType { + return nil, errors.InvalidArgumentError("expected '" + openpgp.SignatureType + "', got: " + block.Type) + } + return block.Body, nil +} diff --git a/repository/git.go b/repository/git.go deleted file mode 100644 index e89bae87a7e240ae2b06e92d9c03f542015baa5b..0000000000000000000000000000000000000000 --- a/repository/git.go +++ /dev/null @@ -1,570 +0,0 @@ -// Package repository contains helper methods for working with the Git repo. -package repository - -import ( - "bytes" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" - "sync" - - "github.com/blevesearch/bleve" - "github.com/go-git/go-billy/v5" - "github.com/go-git/go-billy/v5/osfs" - - "github.com/MichaelMure/git-bug/util/lamport" -) - -var _ ClockedRepo = &GitRepo{} -var _ TestedRepo = &GitRepo{} - -// GitRepo represents an instance of a (local) git repository. -type GitRepo struct { - gitCli - path string - - clocksMutex sync.Mutex - clocks map[string]lamport.Clock - - indexesMutex sync.Mutex - indexes map[string]bleve.Index - - keyring Keyring - localStorage billy.Filesystem -} - -func (repo *GitRepo) ReadCommit(hash Hash) (Commit, error) { - panic("implement me") -} - -func (repo *GitRepo) ResolveRef(ref string) (Hash, error) { - panic("implement me") -} - -// OpenGitRepo determines if the given working directory is inside of a git repository, -// and returns the corresponding GitRepo instance if it is. -func OpenGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) { - k, err := defaultKeyring() - if err != nil { - return nil, err - } - - repo := &GitRepo{ - gitCli: gitCli{path: path}, - path: path, - clocks: make(map[string]lamport.Clock), - indexes: make(map[string]bleve.Index), - keyring: k, - } - - // Check the repo and retrieve the root path - stdout, err := repo.runGitCommand("rev-parse", "--absolute-git-dir") - - // Now dir is fetched with "git rev-parse --git-dir". May be it can - // still return nothing in some cases. Then empty stdout check is - // kept. - if err != nil || stdout == "" { - return nil, ErrNotARepo - } - - // Fix the path to be sure we are at the root - repo.path = stdout - repo.gitCli.path = stdout - repo.localStorage = osfs.New(filepath.Join(path, "git-bug")) - - for _, loader := range clockLoaders { - allExist := true - for _, name := range loader.Clocks { - if _, err := repo.getClock(name); err != nil { - allExist = false - } - } - - if !allExist { - err = loader.Witnesser(repo) - if err != nil { - return nil, err - } - } - } - - return repo, nil -} - -// InitGitRepo create a new empty git repo at the given path -func InitGitRepo(path string) (*GitRepo, error) { - k, err := defaultKeyring() - if err != nil { - return nil, err - } - - repo := &GitRepo{ - gitCli: gitCli{path: path}, - path: filepath.Join(path, ".git"), - clocks: make(map[string]lamport.Clock), - indexes: make(map[string]bleve.Index), - keyring: k, - localStorage: osfs.New(filepath.Join(path, ".git", "git-bug")), - } - - _, err = repo.runGitCommand("init", path) - if err != nil { - return nil, err - } - - return repo, nil -} - -// InitBareGitRepo create a new --bare empty git repo at the given path -func InitBareGitRepo(path string) (*GitRepo, error) { - k, err := defaultKeyring() - if err != nil { - return nil, err - } - - repo := &GitRepo{ - gitCli: gitCli{path: path}, - path: path, - clocks: make(map[string]lamport.Clock), - indexes: make(map[string]bleve.Index), - keyring: k, - localStorage: osfs.New(filepath.Join(path, "git-bug")), - } - - _, err = repo.runGitCommand("init", "--bare", path) - if err != nil { - return nil, err - } - - return repo, nil -} - -func (repo *GitRepo) Close() error { - var firstErr error - for _, index := range repo.indexes { - err := index.Close() - if err != nil && firstErr == nil { - firstErr = err - } - } - return firstErr -} - -// LocalConfig give access to the repository scoped configuration -func (repo *GitRepo) LocalConfig() Config { - return newGitConfig(repo.gitCli, false) -} - -// GlobalConfig give access to the global scoped configuration -func (repo *GitRepo) GlobalConfig() Config { - return newGitConfig(repo.gitCli, true) -} - -// AnyConfig give access to a merged local/global configuration -func (repo *GitRepo) AnyConfig() ConfigRead { - return mergeConfig(repo.LocalConfig(), repo.GlobalConfig()) -} - -// Keyring give access to a user-wide storage for secrets -func (repo *GitRepo) Keyring() Keyring { - return repo.keyring -} - -// GetPath returns the path to the repo. -func (repo *GitRepo) GetPath() string { - return repo.path -} - -// GetUserName returns the name the the user has used to configure git -func (repo *GitRepo) GetUserName() (string, error) { - return repo.runGitCommand("config", "user.name") -} - -// GetUserEmail returns the email address that the user has used to configure git. -func (repo *GitRepo) GetUserEmail() (string, error) { - return repo.runGitCommand("config", "user.email") -} - -// GetCoreEditor returns the name of the editor that the user has used to configure git. -func (repo *GitRepo) GetCoreEditor() (string, error) { - return repo.runGitCommand("var", "GIT_EDITOR") -} - -// GetRemotes returns the configured remotes repositories. -func (repo *GitRepo) GetRemotes() (map[string]string, error) { - stdout, err := repo.runGitCommand("remote", "--verbose") - if err != nil { - return nil, err - } - - lines := strings.Split(stdout, "\n") - remotes := make(map[string]string, len(lines)) - - for _, line := range lines { - if strings.TrimSpace(line) == "" { - continue - } - elements := strings.Fields(line) - if len(elements) != 3 { - return nil, fmt.Errorf("git remote: unexpected output format: %s", line) - } - - remotes[elements[0]] = elements[1] - } - - return remotes, nil -} - -// LocalStorage return a billy.Filesystem giving access to $RepoPath/.git/git-bug -func (repo *GitRepo) LocalStorage() billy.Filesystem { - return repo.localStorage -} - -// GetBleveIndex return a bleve.Index that can be used to index documents -func (repo *GitRepo) GetBleveIndex(name string) (bleve.Index, error) { - repo.indexesMutex.Lock() - defer repo.indexesMutex.Unlock() - - if index, ok := repo.indexes[name]; ok { - return index, nil - } - - path := filepath.Join(repo.path, "indexes", name) - - index, err := bleve.Open(path) - if err == nil { - repo.indexes[name] = index - return index, nil - } - - err = os.MkdirAll(path, os.ModeDir) - if err != nil { - return nil, err - } - - mapping := bleve.NewIndexMapping() - mapping.DefaultAnalyzer = "en" - - index, err = bleve.New(path, mapping) - if err != nil { - return nil, err - } - - repo.indexes[name] = index - - return index, nil -} - -// ClearBleveIndex will wipe the given index -func (repo *GitRepo) ClearBleveIndex(name string) error { - repo.indexesMutex.Lock() - defer repo.indexesMutex.Unlock() - - path := filepath.Join(repo.path, "indexes", name) - - err := os.RemoveAll(path) - if err != nil { - return err - } - - delete(repo.indexes, name) - - return nil -} - -// FetchRefs fetch git refs from a remote -func (repo *GitRepo) FetchRefs(remote, refSpec string) (string, error) { - stdout, err := repo.runGitCommand("fetch", remote, refSpec) - - if err != nil { - return stdout, fmt.Errorf("failed to fetch from the remote '%s': %v", remote, err) - } - - return stdout, err -} - -// PushRefs push git refs to a remote -func (repo *GitRepo) PushRefs(remote string, refSpec string) (string, error) { - stdout, stderr, err := repo.runGitCommandRaw(nil, "push", remote, refSpec) - - if err != nil { - return stdout + stderr, fmt.Errorf("failed to push to the remote '%s': %v", remote, stderr) - } - return stdout + stderr, nil -} - -// StoreData will store arbitrary data and return the corresponding hash -func (repo *GitRepo) StoreData(data []byte) (Hash, error) { - var stdin = bytes.NewReader(data) - - stdout, err := repo.runGitCommandWithStdin(stdin, "hash-object", "--stdin", "-w") - - return Hash(stdout), err -} - -// ReadData will attempt to read arbitrary data from the given hash -func (repo *GitRepo) ReadData(hash Hash) ([]byte, error) { - var stdout bytes.Buffer - var stderr bytes.Buffer - - err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "cat-file", "-p", string(hash)) - - if err != nil { - return []byte{}, err - } - - return stdout.Bytes(), nil -} - -// StoreTree will store a mapping key-->Hash as a Git tree -func (repo *GitRepo) StoreTree(entries []TreeEntry) (Hash, error) { - buffer := prepareTreeEntries(entries) - - stdout, err := repo.runGitCommandWithStdin(&buffer, "mktree") - - if err != nil { - return "", err - } - - return Hash(stdout), nil -} - -// StoreCommit will store a Git commit with the given Git tree -func (repo *GitRepo) StoreCommit(treeHash Hash) (Hash, error) { - stdout, err := repo.runGitCommand("commit-tree", string(treeHash)) - - if err != nil { - return "", err - } - - return Hash(stdout), nil -} - -// StoreCommitWithParent will store a Git commit with the given Git tree -func (repo *GitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) { - stdout, err := repo.runGitCommand("commit-tree", string(treeHash), - "-p", string(parent)) - - if err != nil { - return "", err - } - - return Hash(stdout), nil -} - -// UpdateRef will create or update a Git reference -func (repo *GitRepo) UpdateRef(ref string, hash Hash) error { - _, err := repo.runGitCommand("update-ref", ref, string(hash)) - - return err -} - -// RemoveRef will remove a Git reference -func (repo *GitRepo) RemoveRef(ref string) error { - _, err := repo.runGitCommand("update-ref", "-d", ref) - - return err -} - -// ListRefs will return a list of Git ref matching the given refspec -func (repo *GitRepo) ListRefs(refPrefix string) ([]string, error) { - stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", refPrefix) - - if err != nil { - return nil, err - } - - split := strings.Split(stdout, "\n") - - if len(split) == 1 && split[0] == "" { - return []string{}, nil - } - - return split, nil -} - -// RefExist will check if a reference exist in Git -func (repo *GitRepo) RefExist(ref string) (bool, error) { - stdout, err := repo.runGitCommand("for-each-ref", ref) - - if err != nil { - return false, err - } - - return stdout != "", nil -} - -// CopyRef will create a new reference with the same value as another one -func (repo *GitRepo) CopyRef(source string, dest string) error { - _, err := repo.runGitCommand("update-ref", dest, source) - - return err -} - -// ListCommits will return the list of commit hashes of a ref, in chronological order -func (repo *GitRepo) ListCommits(ref string) ([]Hash, error) { - stdout, err := repo.runGitCommand("rev-list", "--first-parent", "--reverse", ref) - - if err != nil { - return nil, err - } - - split := strings.Split(stdout, "\n") - - casted := make([]Hash, len(split)) - for i, line := range split { - casted[i] = Hash(line) - } - - return casted, nil - -} - -// ReadTree will return the list of entries in a Git tree -func (repo *GitRepo) ReadTree(hash Hash) ([]TreeEntry, error) { - stdout, err := repo.runGitCommand("ls-tree", string(hash)) - - if err != nil { - return nil, err - } - - return readTreeEntries(stdout) -} - -// FindCommonAncestor will return the last common ancestor of two chain of commit -func (repo *GitRepo) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) { - stdout, err := repo.runGitCommand("merge-base", string(hash1), string(hash2)) - - if err != nil { - return "", err - } - - return Hash(stdout), nil -} - -// GetTreeHash return the git tree hash referenced in a commit -func (repo *GitRepo) GetTreeHash(commit Hash) (Hash, error) { - stdout, err := repo.runGitCommand("rev-parse", string(commit)+"^{tree}") - - if err != nil { - return "", err - } - - return Hash(stdout), nil -} - -func (repo *GitRepo) AllClocks() (map[string]lamport.Clock, error) { - repo.clocksMutex.Lock() - defer repo.clocksMutex.Unlock() - - result := make(map[string]lamport.Clock) - - files, err := ioutil.ReadDir(filepath.Join(repo.path, "git-bug", clockPath)) - if os.IsNotExist(err) { - return nil, nil - } - if err != nil { - return nil, err - } - - for _, file := range files { - name := file.Name() - if c, ok := repo.clocks[name]; ok { - result[name] = c - } else { - c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name)) - if err != nil { - return nil, err - } - repo.clocks[name] = c - result[name] = c - } - } - - return result, nil -} - -// GetOrCreateClock return a Lamport clock stored in the Repo. -// If the clock doesn't exist, it's created. -func (repo *GitRepo) GetOrCreateClock(name string) (lamport.Clock, error) { - repo.clocksMutex.Lock() - defer repo.clocksMutex.Unlock() - - c, err := repo.getClock(name) - if err == nil { - return c, nil - } - if err != ErrClockNotExist { - return nil, err - } - - c, err = lamport.NewPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name)) - if err != nil { - return nil, err - } - - repo.clocks[name] = c - return c, nil -} - -func (repo *GitRepo) getClock(name string) (lamport.Clock, error) { - if c, ok := repo.clocks[name]; ok { - return c, nil - } - - c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name)) - if err == nil { - repo.clocks[name] = c - return c, nil - } - if err == lamport.ErrClockNotExist { - return nil, ErrClockNotExist - } - return nil, err -} - -// Increment is equivalent to c = GetOrCreateClock(name) + c.Increment() -func (repo *GitRepo) Increment(name string) (lamport.Time, error) { - c, err := repo.GetOrCreateClock(name) - if err != nil { - return lamport.Time(0), err - } - return c.Increment() -} - -// Witness is equivalent to c = GetOrCreateClock(name) + c.Witness(time) -func (repo *GitRepo) Witness(name string, time lamport.Time) error { - c, err := repo.GetOrCreateClock(name) - if err != nil { - return err - } - return c.Witness(time) -} - -// AddRemote add a new remote to the repository -// Not in the interface because it's only used for testing -func (repo *GitRepo) AddRemote(name string, url string) error { - _, err := repo.runGitCommand("remote", "add", name, url) - - return err -} - -// GetLocalRemote return the URL to use to add this repo as a local remote -func (repo *GitRepo) GetLocalRemote() string { - return repo.path -} - -// EraseFromDisk delete this repository entirely from the disk -func (repo *GitRepo) EraseFromDisk() error { - err := repo.Close() - if err != nil { - return err - } - - path := filepath.Clean(strings.TrimSuffix(repo.path, string(filepath.Separator)+".git")) - - // fmt.Println("Cleaning repo:", path) - return os.RemoveAll(path) -} diff --git a/repository/git_cli.go b/repository/git_cli.go deleted file mode 100644 index 085b1cda2dd8575a6bb6c540f0a39ad1575aa888..0000000000000000000000000000000000000000 --- a/repository/git_cli.go +++ /dev/null @@ -1,56 +0,0 @@ -package repository - -import ( - "bytes" - "fmt" - "io" - "os/exec" - "strings" -) - -// gitCli is a helper to launch CLI git commands -type gitCli struct { - path string -} - -// Run the given git command with the given I/O reader/writers, returning an error if it fails. -func (cli gitCli) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error { - // make sure that the working directory for the command - // always exist, in particular when running "git init". - path := strings.TrimSuffix(cli.path, ".git") - - // fmt.Printf("[%s] Running git %s\n", path, strings.Join(args, " ")) - - cmd := exec.Command("git", args...) - cmd.Dir = path - cmd.Stdin = stdin - cmd.Stdout = stdout - cmd.Stderr = stderr - - return cmd.Run() -} - -// Run the given git command and return its stdout, or an error if the command fails. -func (cli gitCli) runGitCommandRaw(stdin io.Reader, args ...string) (string, string, error) { - var stdout bytes.Buffer - var stderr bytes.Buffer - err := cli.runGitCommandWithIO(stdin, &stdout, &stderr, args...) - return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err -} - -// Run the given git command and return its stdout, or an error if the command fails. -func (cli gitCli) runGitCommandWithStdin(stdin io.Reader, args ...string) (string, error) { - stdout, stderr, err := cli.runGitCommandRaw(stdin, args...) - if err != nil { - if stderr == "" { - stderr = "Error running git command: " + strings.Join(args, " ") - } - err = fmt.Errorf(stderr) - } - return stdout, err -} - -// Run the given git command and return its stdout, or an error if the command fails. -func (cli gitCli) runGitCommand(args ...string) (string, error) { - return cli.runGitCommandWithStdin(nil, args...) -} diff --git a/repository/git_config.go b/repository/git_config.go deleted file mode 100644 index b46cc69b2b00bf9feef9ba750326da3b7f6d2fc6..0000000000000000000000000000000000000000 --- a/repository/git_config.go +++ /dev/null @@ -1,221 +0,0 @@ -package repository - -import ( - "fmt" - "regexp" - "strconv" - "strings" - "time" - - "github.com/blang/semver" - "github.com/pkg/errors" -) - -var _ Config = &gitConfig{} - -type gitConfig struct { - cli gitCli - localityFlag string -} - -func newGitConfig(cli gitCli, global bool) *gitConfig { - localityFlag := "--local" - if global { - localityFlag = "--global" - } - return &gitConfig{ - cli: cli, - localityFlag: localityFlag, - } -} - -// StoreString store a single key/value pair in the config of the repo -func (gc *gitConfig) StoreString(key string, value string) error { - _, err := gc.cli.runGitCommand("config", gc.localityFlag, "--replace-all", key, value) - return err -} - -func (gc *gitConfig) StoreBool(key string, value bool) error { - return gc.StoreString(key, strconv.FormatBool(value)) -} - -func (gc *gitConfig) StoreTimestamp(key string, value time.Time) error { - return gc.StoreString(key, strconv.Itoa(int(value.Unix()))) -} - -// ReadAll read all key/value pair matching the key prefix -func (gc *gitConfig) ReadAll(keyPrefix string) (map[string]string, error) { - stdout, err := gc.cli.runGitCommand("config", gc.localityFlag, "--includes", "--get-regexp", keyPrefix) - - // / \ - // / ! \ - // ------- - // - // There can be a legitimate error here, but I see no portable way to - // distinguish them from the git error that say "no matching value exist" - if err != nil { - return nil, nil - } - - lines := strings.Split(stdout, "\n") - - result := make(map[string]string, len(lines)) - - for _, line := range lines { - if strings.TrimSpace(line) == "" { - continue - } - - parts := strings.SplitN(line, " ", 2) - result[parts[0]] = parts[1] - } - - return result, nil -} - -func (gc *gitConfig) ReadString(key string) (string, error) { - stdout, err := gc.cli.runGitCommand("config", gc.localityFlag, "--includes", "--get-all", key) - - // / \ - // / ! \ - // ------- - // - // There can be a legitimate error here, but I see no portable way to - // distinguish them from the git error that say "no matching value exist" - if err != nil { - return "", ErrNoConfigEntry - } - - lines := strings.Split(stdout, "\n") - - if len(lines) == 0 { - return "", ErrNoConfigEntry - } - if len(lines) > 1 { - return "", ErrMultipleConfigEntry - } - - return lines[0], nil -} - -func (gc *gitConfig) ReadBool(key string) (bool, error) { - val, err := gc.ReadString(key) - if err != nil { - return false, err - } - - return strconv.ParseBool(val) -} - -func (gc *gitConfig) ReadTimestamp(key string) (time.Time, error) { - value, err := gc.ReadString(key) - if err != nil { - return time.Time{}, err - } - return ParseTimestamp(value) -} - -func (gc *gitConfig) rmSection(keyPrefix string) error { - _, err := gc.cli.runGitCommand("config", gc.localityFlag, "--remove-section", keyPrefix) - return err -} - -func (gc *gitConfig) unsetAll(keyPrefix string) error { - _, err := gc.cli.runGitCommand("config", gc.localityFlag, "--unset-all", keyPrefix) - return err -} - -// return keyPrefix section -// example: sectionFromKey(a.b.c.d) return a.b.c -func sectionFromKey(keyPrefix string) string { - s := strings.Split(keyPrefix, ".") - if len(s) == 1 { - return keyPrefix - } - - return strings.Join(s[:len(s)-1], ".") -} - -// rmConfigs with git version lesser than 2.18 -func (gc *gitConfig) rmConfigsGitVersionLT218(keyPrefix string) error { - // try to remove key/value pair by key - err := gc.unsetAll(keyPrefix) - if err != nil { - return gc.rmSection(keyPrefix) - } - - m, err := gc.ReadAll(sectionFromKey(keyPrefix)) - if err != nil { - return err - } - - // if section doesn't have any left key/value remove the section - if len(m) == 0 { - return gc.rmSection(sectionFromKey(keyPrefix)) - } - - return nil -} - -// RmConfigs remove all key/value pair matching the key prefix -func (gc *gitConfig) RemoveAll(keyPrefix string) error { - // starting from git 2.18.0 sections are automatically deleted when the last existing - // key/value is removed. Before 2.18.0 we should remove the section - // see https://github.com/git/git/blob/master/Documentation/RelNotes/2.18.0.txt#L379 - lt218, err := gc.gitVersionLT218() - if err != nil { - return errors.Wrap(err, "getting git version") - } - - if lt218 { - return gc.rmConfigsGitVersionLT218(keyPrefix) - } - - err = gc.unsetAll(keyPrefix) - if err != nil { - return gc.rmSection(keyPrefix) - } - - return nil -} - -func (gc *gitConfig) gitVersion() (*semver.Version, error) { - versionOut, err := gc.cli.runGitCommand("version") - if err != nil { - return nil, err - } - return parseGitVersion(versionOut) -} - -func parseGitVersion(versionOut string) (*semver.Version, error) { - // extract the version and truncate potential bad parts - // ex: 2.23.0.rc1 instead of 2.23.0-rc1 - r := regexp.MustCompile(`(\d+\.){1,2}\d+`) - - extracted := r.FindString(versionOut) - if extracted == "" { - return nil, fmt.Errorf("unreadable git version %s", versionOut) - } - - version, err := semver.Make(extracted) - if err != nil { - return nil, err - } - - return &version, nil -} - -func (gc *gitConfig) gitVersionLT218() (bool, error) { - version, err := gc.gitVersion() - if err != nil { - return false, err - } - - version218string := "2.18.0" - gitVersion218, err := semver.Make(version218string) - if err != nil { - return false, err - } - - return version.LT(gitVersion218), nil -} diff --git a/repository/git_test.go b/repository/git_test.go deleted file mode 100644 index 6603e33907c7c52c68b8ce8b1632e2793d616dee..0000000000000000000000000000000000000000 --- a/repository/git_test.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package repository contains helper methods for working with the Git repo. -package repository - -// func TestGitRepo(t *testing.T) { -// RepoTest(t, CreateTestRepo, CleanupTestRepos) -// } diff --git a/repository/git_testing.go b/repository/git_testing.go deleted file mode 100644 index 2168d53e41df8e9cd838cda2960dd6d591611dee..0000000000000000000000000000000000000000 --- a/repository/git_testing.go +++ /dev/null @@ -1,72 +0,0 @@ -package repository - -import ( - "io/ioutil" - "log" - - "github.com/99designs/keyring" -) - -// This is intended for testing only - -func CreateTestRepo(bare bool) TestedRepo { - dir, err := ioutil.TempDir("", "") - if err != nil { - log.Fatal(err) - } - - var creator func(string) (*GitRepo, error) - - if bare { - creator = InitBareGitRepo - } else { - creator = InitGitRepo - } - - repo, err := creator(dir) - if err != nil { - log.Fatal(err) - } - - config := repo.LocalConfig() - if err := config.StoreString("user.name", "testuser"); err != nil { - log.Fatal("failed to set user.name for test repository: ", err) - } - if err := config.StoreString("user.email", "testuser@example.com"); err != nil { - log.Fatal("failed to set user.email for test repository: ", err) - } - - // make sure we use a mock keyring for testing to not interact with the global system - return &replaceKeyring{ - TestedRepo: repo, - keyring: keyring.NewArrayKeyring(nil), - } -} - -func SetupReposAndRemote() (repoA, repoB, remote TestedRepo) { - repoA = CreateGoGitTestRepo(false) - repoB = CreateGoGitTestRepo(false) - remote = CreateGoGitTestRepo(true) - - err := repoA.AddRemote("origin", remote.GetLocalRemote()) - if err != nil { - log.Fatal(err) - } - - err = repoB.AddRemote("origin", remote.GetLocalRemote()) - if err != nil { - log.Fatal(err) - } - - return repoA, repoB, remote -} - -// replaceKeyring allow to replace the Keyring of the underlying repo -type replaceKeyring struct { - TestedRepo - keyring Keyring -} - -func (rk replaceKeyring) Keyring() Keyring { - return rk.keyring -} diff --git a/repository/gogit.go b/repository/gogit.go index 64ccb773fb9bc49f6aa8003fcac897fe85fe6abd..d6eb8621ff7dc3e82fb8b9b772a6ff539c41f866 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -20,6 +20,7 @@ import ( "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/filemode" "github.com/go-git/go-git/v5/plumbing/object" + "golang.org/x/crypto/openpgp" "github.com/MichaelMure/git-bug/util/lamport" ) @@ -521,12 +522,13 @@ func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) { } // StoreCommit will store a Git commit with the given Git tree -func (repo *GoGitRepo) StoreCommit(treeHash Hash) (Hash, error) { - return repo.StoreCommitWithParent(treeHash, "") +func (repo *GoGitRepo) StoreCommit(treeHash Hash, parents ...Hash) (Hash, error) { + return repo.StoreSignedCommit(treeHash, nil, parents...) } -// StoreCommit will store a Git commit with the given Git tree -func (repo *GoGitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) { +// StoreCommit will store a Git commit with the given Git tree. If signKey is not nil, the commit +// will be signed accordingly. +func (repo *GoGitRepo) StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity, parents ...Hash) (Hash, error) { cfg, err := repo.r.Config() if err != nil { return "", err @@ -547,8 +549,28 @@ func (repo *GoGitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, TreeHash: plumbing.NewHash(treeHash.String()), } - if parent != "" { - commit.ParentHashes = []plumbing.Hash{plumbing.NewHash(parent.String())} + for _, parent := range parents { + commit.ParentHashes = append(commit.ParentHashes, plumbing.NewHash(parent.String())) + } + + // Compute the signature if needed + if signKey != nil { + // first get the serialized commit + encoded := &plumbing.MemoryObject{} + if err := commit.Encode(encoded); err != nil { + return "", err + } + r, err := encoded.Reader() + if err != nil { + return "", err + } + + // sign the data + var sig bytes.Buffer + if err := openpgp.ArmoredDetachSign(&sig, signKey, r, nil); err != nil { + return "", err + } + commit.PGPSignature = sig.String() } obj := repo.r.Storer.NewEncodedObject() @@ -608,6 +630,13 @@ func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error { return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(ref), plumbing.NewHash(hash.String()))) } +// MergeRef merge other into ref and update the reference +// If the update is not fast-forward, the callback treeHashFn will be called for the caller to generate +// the Tree to store in the merge commit. +func (repo *GoGitRepo) MergeRef(ref string, otherRef string, treeHashFn func() Hash) error { + return nonNativeMerge(repo, ref, otherRef, treeHashFn) +} + // RemoveRef will remove a Git reference func (repo *GoGitRepo) RemoveRef(ref string) error { return repo.r.Storer.RemoveReference(plumbing.ReferenceName(ref)) @@ -657,38 +686,16 @@ func (repo *GoGitRepo) CopyRef(source string, dest string) error { // ListCommits will return the list of tree hashes of a ref, in chronological order func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) { - r, err := repo.r.Reference(plumbing.ReferenceName(ref), false) - if err != nil { - return nil, err - } + return nonNativeListCommits(repo, ref) +} - commit, err := repo.r.CommitObject(r.Hash()) +func (repo *GoGitRepo) ReadCommit(hash Hash) (Commit, error) { + encoded, err := repo.r.Storer.EncodedObject(plumbing.CommitObject, plumbing.NewHash(hash.String())) if err != nil { - return nil, err - } - hashes := []Hash{Hash(commit.Hash.String())} - - for { - commit, err = commit.Parent(0) - if err == object.ErrParentNotFound { - break - } - if err != nil { - return nil, err - } - - if commit.NumParents() > 1 { - return nil, fmt.Errorf("multiple parents") - } - - hashes = append([]Hash{Hash(commit.Hash.String())}, hashes...) + return Commit{}, err } - return hashes, nil -} - -func (repo *GoGitRepo) ReadCommit(hash Hash) (Commit, error) { - commit, err := repo.r.CommitObject(plumbing.NewHash(hash.String())) + commit, err := object.DecodeCommit(repo.r.Storer, encoded) if err != nil { return Commit{}, err } @@ -698,12 +705,25 @@ func (repo *GoGitRepo) ReadCommit(hash Hash) (Commit, error) { parents[i] = Hash(parentHash.String()) } - return Commit{ + result := Commit{ Hash: hash, Parents: parents, TreeHash: Hash(commit.TreeHash.String()), - }, nil + } + + if commit.PGPSignature != "" { + result.SignedData, err = encoded.Reader() + if err != nil { + return Commit{}, err + } + result.Signature, err = deArmorSignature(strings.NewReader(commit.PGPSignature)) + if err != nil { + return Commit{}, err + } + } + + return result, nil } func (repo *GoGitRepo) AllClocks() (map[string]lamport.Clock, error) { diff --git a/repository/gogit_testing.go b/repository/gogit_testing.go index a8bff41ebd17f5d859175c415b558a02d325d905..cad776b383535e8d9f1972730e56952cb3c182f9 100644 --- a/repository/gogit_testing.go +++ b/repository/gogit_testing.go @@ -3,6 +3,8 @@ package repository import ( "io/ioutil" "log" + + "github.com/99designs/keyring" ) // This is intended for testing only @@ -34,7 +36,11 @@ func CreateGoGitTestRepo(bare bool) TestedRepo { log.Fatal("failed to set user.email for test repository: ", err) } - return repo + // make sure we use a mock keyring for testing to not interact with the global system + return &replaceKeyring{ + TestedRepo: repo, + keyring: keyring.NewArrayKeyring(nil), + } } func SetupGoGitReposAndRemote() (repoA, repoB, remote TestedRepo) { diff --git a/repository/keyring.go b/repository/keyring.go index 4cb3c9ff57079b171db4a4ad5f4ceba5fd1d3ac3..64365c39e980a2711006e753be153d104e3ec4b4 100644 --- a/repository/keyring.go +++ b/repository/keyring.go @@ -48,3 +48,13 @@ func defaultKeyring() (Keyring, error) { }, }) } + +// replaceKeyring allow to replace the Keyring of the underlying repo +type replaceKeyring struct { + TestedRepo + keyring Keyring +} + +func (rk replaceKeyring) Keyring() Keyring { + return rk.keyring +} diff --git a/repository/mock_repo.go b/repository/mock_repo.go index 227e0f2c678cab63ae098b4d29c1c9743bb8bdff..095ad61c05695a45ad7bd34a6e83112ff26b58a2 100644 --- a/repository/mock_repo.go +++ b/repository/mock_repo.go @@ -1,6 +1,7 @@ package repository import ( + "bytes" "crypto/sha1" "fmt" "strings" @@ -10,6 +11,7 @@ import ( "github.com/blevesearch/bleve" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" + "golang.org/x/crypto/openpgp" "github.com/MichaelMure/git-bug/util/lamport" ) @@ -180,6 +182,7 @@ var _ RepoData = &mockRepoData{} type commit struct { treeHash Hash parents []Hash + sig string } type mockRepoData struct { @@ -198,12 +201,12 @@ func NewMockRepoData() *mockRepoData { } } -// PushRefs push git refs to a remote -func (r *mockRepoData) PushRefs(remote string, refSpec string) (string, error) { +func (r *mockRepoData) FetchRefs(remote string, refSpec string) (string, error) { return "", nil } -func (r *mockRepoData) FetchRefs(remote string, refSpec string) (string, error) { +// PushRefs push git refs to a remote +func (r *mockRepoData) PushRefs(remote string, refSpec string) (string, error) { return "", nil } @@ -216,7 +219,6 @@ func (r *mockRepoData) StoreData(data []byte) (Hash, error) { func (r *mockRepoData) ReadData(hash Hash) ([]byte, error) { data, ok := r.blobs[hash] - if !ok { return nil, fmt.Errorf("unknown hash") } @@ -233,25 +235,86 @@ func (r *mockRepoData) StoreTree(entries []TreeEntry) (Hash, error) { return hash, nil } -func (r *mockRepoData) StoreCommit(treeHash Hash) (Hash, error) { - rawHash := sha1.Sum([]byte(treeHash)) - hash := Hash(fmt.Sprintf("%x", rawHash)) - r.commits[hash] = commit{ - treeHash: treeHash, +func (r *mockRepoData) ReadTree(hash Hash) ([]TreeEntry, error) { + var data string + + data, ok := r.trees[hash] + + if !ok { + // Git will understand a commit hash to reach a tree + commit, ok := r.commits[hash] + + if !ok { + return nil, fmt.Errorf("unknown hash") + } + + data, ok = r.trees[commit.treeHash] + + if !ok { + return nil, fmt.Errorf("unknown hash") + } } - return hash, nil + + return readTreeEntries(data) +} + +func (r *mockRepoData) StoreCommit(treeHash Hash, parents ...Hash) (Hash, error) { + return r.StoreSignedCommit(treeHash, nil, parents...) } -func (r *mockRepoData) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) { - rawHash := sha1.Sum([]byte(treeHash + parent)) +func (r *mockRepoData) StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity, parents ...Hash) (Hash, error) { + hasher := sha1.New() + hasher.Write([]byte(treeHash)) + for _, parent := range parents { + hasher.Write([]byte(parent)) + } + rawHash := hasher.Sum(nil) hash := Hash(fmt.Sprintf("%x", rawHash)) - r.commits[hash] = commit{ + c := commit{ treeHash: treeHash, - parents: []Hash{parent}, + parents: parents, + } + if signKey != nil { + // unlike go-git, we only sign the tree hash for simplicity instead of all the fields (parents ...) + var sig bytes.Buffer + if err := openpgp.DetachSign(&sig, signKey, strings.NewReader(string(treeHash)), nil); err != nil { + return "", err + } + c.sig = sig.String() } + r.commits[hash] = c return hash, nil } +func (r *mockRepoData) ReadCommit(hash Hash) (Commit, error) { + c, ok := r.commits[hash] + if !ok { + return Commit{}, fmt.Errorf("unknown commit") + } + + result := Commit{ + Hash: hash, + Parents: c.parents, + TreeHash: c.treeHash, + } + + if c.sig != "" { + result.SignedData = strings.NewReader(string(c.treeHash)) + result.Signature = strings.NewReader(c.sig) + } + + return result, nil +} + +func (r *mockRepoData) GetTreeHash(commit Hash) (Hash, error) { + c, ok := r.commits[commit] + if !ok { + return "", fmt.Errorf("unknown commit") + } + + return c.treeHash, nil +} + func (r *mockRepoData) ResolveRef(ref string) (Hash, error) { h, ok := r.refs[ref] if !ok { @@ -270,22 +333,6 @@ func (r *mockRepoData) RemoveRef(ref string) error { return nil } -func (r *mockRepoData) RefExist(ref string) (bool, error) { - _, exist := r.refs[ref] - return exist, nil -} - -func (r *mockRepoData) CopyRef(source string, dest string) error { - hash, exist := r.refs[source] - - if !exist { - return fmt.Errorf("Unknown ref") - } - - r.refs[dest] = hash - return nil -} - func (r *mockRepoData) ListRefs(refPrefix string) ([]string, error) { var keys []string @@ -298,63 +345,20 @@ func (r *mockRepoData) ListRefs(refPrefix string) ([]string, error) { return keys, nil } -func (r *mockRepoData) ListCommits(ref string) ([]Hash, error) { - var hashes []Hash - - hash := r.refs[ref] - - for { - commit, ok := r.commits[hash] - - if !ok { - break - } - - hashes = append([]Hash{hash}, hashes...) - - if len(commit.parents) == 0 { - break - } - hash = commit.parents[0] - } - - return hashes, nil -} - -func (r *mockRepoData) ReadCommit(hash Hash) (Commit, error) { - c, ok := r.commits[hash] - if !ok { - return Commit{}, fmt.Errorf("unknown commit") - } - - return Commit{ - Hash: hash, - Parents: c.parents, - TreeHash: c.treeHash, - }, nil +func (r *mockRepoData) RefExist(ref string) (bool, error) { + _, exist := r.refs[ref] + return exist, nil } -func (r *mockRepoData) ReadTree(hash Hash) ([]TreeEntry, error) { - var data string - - data, ok := r.trees[hash] - - if !ok { - // Git will understand a commit hash to reach a tree - commit, ok := r.commits[hash] - - if !ok { - return nil, fmt.Errorf("unknown hash") - } - - data, ok = r.trees[commit.treeHash] +func (r *mockRepoData) CopyRef(source string, dest string) error { + hash, exist := r.refs[source] - if !ok { - return nil, fmt.Errorf("unknown hash") - } + if !exist { + return fmt.Errorf("Unknown ref") } - return readTreeEntries(data) + r.refs[dest] = hash + return nil } func (r *mockRepoData) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) { @@ -392,13 +396,8 @@ func (r *mockRepoData) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) } } -func (r *mockRepoData) GetTreeHash(commit Hash) (Hash, error) { - c, ok := r.commits[commit] - if !ok { - return "", fmt.Errorf("unknown commit") - } - - return c.treeHash, nil +func (r *mockRepoData) ListCommits(ref string) ([]Hash, error) { + return nonNativeListCommits(r, ref) } var _ RepoClock = &mockRepoClock{} diff --git a/repository/mock_repo_test.go b/repository/mock_repo_test.go index dec09380d5ff5b26e9ba0e7c0c17deeeb860da1b..12851a80d21d8f271bec336bb809197934a20514 100644 --- a/repository/mock_repo_test.go +++ b/repository/mock_repo_test.go @@ -1,6 +1,8 @@ package repository -import "testing" +import ( + "testing" +) func TestMockRepo(t *testing.T) { creator := func(bare bool) TestedRepo { return NewMockRepo() } diff --git a/repository/repo.go b/repository/repo.go index afd8ff77765780812463d54377bcadfe733e8796..d7afa98343ba09400a79b98eca1fea88e91f6261 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -3,15 +3,17 @@ package repository import ( "errors" + "io" "github.com/blevesearch/bleve" "github.com/go-git/go-billy/v5" + "golang.org/x/crypto/openpgp" "github.com/MichaelMure/git-bug/util/lamport" ) var ( - // ErrNotARepo is the error returned when the git repo root wan't be found + // ErrNotARepo is the error returned when the git repo root can't be found ErrNotARepo = errors.New("not a git repository") // ErrClockNotExist is the error returned when a clock can't be found ErrClockNotExist = errors.New("clock doesn't exist") @@ -89,9 +91,11 @@ type RepoBleve interface { } type Commit struct { - Hash Hash - Parents []Hash - TreeHash Hash + Hash Hash + Parents []Hash // hashes of the parents, if any + TreeHash Hash // hash of the git Tree + SignedData io.Reader // if signed, reader for the signed data (likely, the serialized commit) + Signature io.Reader // if signed, reader for the (non-armored) signature } // RepoData give access to the git data storage @@ -116,21 +120,29 @@ type RepoData interface { ReadTree(hash Hash) ([]TreeEntry, error) // StoreCommit will store a Git commit with the given Git tree - StoreCommit(treeHash Hash) (Hash, error) + StoreCommit(treeHash Hash, parents ...Hash) (Hash, error) - // StoreCommit will store a Git commit with the given Git tree - StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) + // StoreCommit will store a Git commit with the given Git tree. If signKey is not nil, the commit + // will be signed accordingly. + StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity, parents ...Hash) (Hash, error) + // ReadCommit read a Git commit and returns some of its characteristic ReadCommit(hash Hash) (Commit, error) // GetTreeHash return the git tree hash referenced in a commit GetTreeHash(commit Hash) (Hash, error) + // ResolveRef returns the hash of the target commit of the given ref ResolveRef(ref string) (Hash, error) // UpdateRef will create or update a Git reference UpdateRef(ref string, hash Hash) error + // // MergeRef merge other into ref and update the reference + // // If the update is not fast-forward, the callback treeHashFn will be called for the caller to generate + // // the Tree to store in the merge commit. + // MergeRef(ref string, otherRef string, treeHashFn func() Hash) error + // RemoveRef will remove a Git reference RemoveRef(ref string) error @@ -148,7 +160,6 @@ type RepoData interface { FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error) // ListCommits will return the list of tree hashes of a ref, in chronological order - // Deprecated ListCommits(ref string) ([]Hash, error) } diff --git a/repository/repo_testing.go b/repository/repo_testing.go index 1d3a31557edf0c597e4426c9b4b8267d0661e744..4a5c48bbade43926c8646dc54c5c8b3f4b66ad2d 100644 --- a/repository/repo_testing.go +++ b/repository/repo_testing.go @@ -135,7 +135,8 @@ func RepoDataTest(t *testing.T, repo RepoData) { require.NoError(t, err) require.Equal(t, treeHash1, treeHash1Read) - commit2, err := repo.StoreCommitWithParent(treeHash2, commit1) + // commit with a parent + commit2, err := repo.StoreCommit(treeHash2, commit1) require.NoError(t, err) require.True(t, commit2.IsValid()) @@ -187,7 +188,7 @@ func RepoDataTest(t *testing.T, repo RepoData) { // Graph - commit3, err := repo.StoreCommitWithParent(treeHash1, commit1) + commit3, err := repo.StoreCommit(treeHash1, commit1) require.NoError(t, err) ancestorHash, err := repo.FindCommonAncestor(commit2, commit3) @@ -237,3 +238,22 @@ func randomData() []byte { } return b } + +func makeCommit(t *testing.T, repo RepoData, parents ...Hash) Hash { + blobHash, err := repo.StoreData(randomData()) + require.NoError(t, err) + + treeHash, err := repo.StoreTree([]TreeEntry{ + { + ObjectType: Blob, + Hash: blobHash, + Name: "foo", + }, + }) + require.NoError(t, err) + + commitHash, err := repo.StoreCommit(treeHash, parents...) + require.NoError(t, err) + + return commitHash +} diff --git a/util/lamport/clock_testing.go b/util/lamport/clock_testing.go index 4bf6d2bf8640700c08246a96a9f343fb4e41b6f2..de66c5c9076900a7b93a22061c61d5c0bcb4338a 100644 --- a/util/lamport/clock_testing.go +++ b/util/lamport/clock_testing.go @@ -14,11 +14,11 @@ func testClock(t *testing.T, c Clock) { assert.Equal(t, Time(2), val) assert.Equal(t, Time(2), c.Time()) - err = c.Witness(41) + err = c.Witness(42) assert.NoError(t, err) assert.Equal(t, Time(42), c.Time()) - err = c.Witness(41) + err = c.Witness(42) assert.NoError(t, err) assert.Equal(t, Time(42), c.Time()) diff --git a/util/lamport/mem_clock.go b/util/lamport/mem_clock.go index f113b5013a6f0a9149a76606347265d38f0a9595..d824d834478ab91a3ae508b6774a64eab8961284 100644 --- a/util/lamport/mem_clock.go +++ b/util/lamport/mem_clock.go @@ -25,6 +25,14 @@ */ +// Note: this code originally originate from Hashicorp's Serf but has been changed since to fit git-bug's need. + +// Note: this Lamport clock implementation is different than the algorithms you can find, notably Wikipedia or the +// original Serf implementation. The reason is lie to what constitute an event in this distributed system. +// Commonly, events happen when messages are sent or received, whereas in git-bug events happen when some data is +// written, but *not* when read. This is why Witness set the time to the max seen value instead of max seen value +1. +// See https://cs.stackexchange.com/a/133730/129795 + package lamport import ( @@ -72,12 +80,12 @@ WITNESS: // If the other value is old, we do not need to do anything cur := atomic.LoadUint64(&mc.counter) other := uint64(v) - if other < cur { + if other <= cur { return nil } // Ensure that our local clock is at least one ahead. - if !atomic.CompareAndSwapUint64(&mc.counter, cur, other+1) { + if !atomic.CompareAndSwapUint64(&mc.counter, cur, other) { // CAS: CompareAndSwap // The CAS failed, so we just retry. Eventually our CAS should // succeed or a future witness will pass us by and our witness From dc5059bc3372941e2908739831188768335ac50b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 24 Jan 2021 19:45:21 +0100 Subject: [PATCH 20/42] entity: more progress on merging and signing --- bug/bug_actions.go | 6 +- cache/repo_cache_bug.go | 5 +- entity/TODO | 1 + entity/dag/entity.go | 14 +-- entity/dag/entity_actions.go | 87 ++++++++++++------- entity/dag/operation.go | 8 +- entity/dag/operation_pack.go | 50 ++++++++++- entity/merge.go | 37 +++++--- identity/identity.go | 15 +++- identity/identity_actions.go | 6 +- identity/identity_stub.go | 3 +- identity/interface.go | 3 +- identity/key.go | 163 +++++++++++++++++++++-------------- identity/key_test.go | 45 +++++++++- repository/common.go | 53 ------------ repository/gogit.go | 7 -- repository/keyring.go | 2 +- repository/repo.go | 5 -- repository/repo_testing.go | 21 +---- 19 files changed, 303 insertions(+), 228 deletions(-) diff --git a/bug/bug_actions.go b/bug/bug_actions.go index aa82356d76c9cf9c34a9218f6a1e6435782e455a..40a2facba40a0de28bb39f6fced41c541d4831c3 100644 --- a/bug/bug_actions.go +++ b/bug/bug_actions.go @@ -112,7 +112,7 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeRes return } - out <- entity.NewMergeStatus(entity.MergeStatusNew, id, remoteBug) + out <- entity.NewMergeNewStatus(id, remoteBug) continue } @@ -131,9 +131,9 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeRes } if updated { - out <- entity.NewMergeStatus(entity.MergeStatusUpdated, id, localBug) + out <- entity.NewMergeUpdatedStatus(id, localBug) } else { - out <- entity.NewMergeStatus(entity.MergeStatusNothing, id, localBug) + out <- entity.NewMergeNothingStatus(id) } } }() diff --git a/cache/repo_cache_bug.go b/cache/repo_cache_bug.go index 90b9a892bf1e23308de82e47e3cd7599ad4743f4..9f011c0450d66c15ba2fe16df91dd15f80527304 100644 --- a/cache/repo_cache_bug.go +++ b/cache/repo_cache_bug.go @@ -16,10 +16,7 @@ import ( "github.com/blevesearch/bleve" ) -const ( - bugCacheFile = "bug-cache" - searchCacheDir = "search-cache" -) +const bugCacheFile = "bug-cache" var errBugNotInCache = errors.New("bug missing from cache") diff --git a/entity/TODO b/entity/TODO index fd3c97105ebac0ba2cc7b3b515d1933fa1a1da20..9f33dd09cae2672ca6e947d8118fdf751f1eebe0 100644 --- a/entity/TODO +++ b/entity/TODO @@ -1,6 +1,7 @@ - is the pack Lamport clock really useful vs only topological sort? - topological order is enforced on the clocks, so what's the point? - is EditTime equivalent to PackTime? no, avoid the gaps. Is it better? + --> PackTime is contained within a bug and might avoid extreme reordering? - how to do commit signature? - how to avoid id collision between Operations? - write tests for actions diff --git a/entity/dag/entity.go b/entity/dag/entity.go index 78347fa09473e9932b382ad69484e1d36a274f9c..63d7fc3b7c8dd0e5796064592c99aed56ac4cc3f 100644 --- a/entity/dag/entity.go +++ b/entity/dag/entity.go @@ -318,7 +318,6 @@ func (e *Entity) CommitAdNeeded(repo repository.ClockedRepo) error { } // Commit write the appended operations in the repository -// TODO: support commit signature func (e *Entity) Commit(repo repository.ClockedRepo) error { if !e.NeedCommit() { return fmt.Errorf("can't commit an entity with no pending operation") @@ -361,18 +360,13 @@ func (e *Entity) Commit(repo repository.ClockedRepo) error { // PackTime: packTime, } - treeHash, err := opp.Write(e.Definition, repo) - if err != nil { - return err - } - - // Write a Git commit referencing the tree, with the previous commit as parent var commitHash repository.Hash - if e.lastCommit != "" { - commitHash, err = repo.StoreCommit(treeHash, e.lastCommit) + if e.lastCommit == "" { + commitHash, err = opp.Write(e.Definition, repo) } else { - commitHash, err = repo.StoreCommit(treeHash) + commitHash, err = opp.Write(e.Definition, repo, e.lastCommit) } + if err != nil { return err } diff --git a/entity/dag/entity_actions.go b/entity/dag/entity_actions.go index 8dcf91e663b03faef02a1c9ef70a2372e1b22dd2..83ff7ddce04c7bc07d7dcc387902a8f719b21201 100644 --- a/entity/dag/entity_actions.go +++ b/entity/dag/entity_actions.go @@ -9,6 +9,7 @@ import ( "github.com/MichaelMure/git-bug/repository" ) +// ListLocalIds list all the available local Entity's Id func ListLocalIds(typename string, repo repository.RepoData) ([]entity.Id, error) { refs, err := repo.ListRefs(fmt.Sprintf("refs/%s/", typename)) if err != nil { @@ -56,6 +57,21 @@ func Pull(def Definition, repo repository.ClockedRepo, remote string) error { return nil } +// MergeAll will merge all the available remote Entity: +// +// Multiple scenario exist: +// 1. if the remote Entity doesn't exist locally, it's created +// --> emit entity.MergeStatusNew +// 2. if the remote and local Entity have the same state, nothing is changed +// --> emit entity.MergeStatusNothing +// 3. if the local Entity has new commits but the remote don't, nothing is changed +// --> emit entity.MergeStatusNothing +// 4. if the remote has new commit, the local bug is updated to match the same history +// (fast-forward update) +// --> emit entity.MergeStatusUpdated +// 5. if both local and remote Entity have new commits (that is, we have a concurrent edition), +// a merge commit with an empty operationPack is created to join both branch and form a DAG. +// --> emit entity.MergeStatusUpdated func MergeAll(def Definition, repo repository.ClockedRepo, remote string) <-chan entity.MergeResult { out := make(chan entity.MergeResult) @@ -81,6 +97,8 @@ func MergeAll(def Definition, repo repository.ClockedRepo, remote string) <-chan return out } +// merge perform a merge to make sure a local Entity is up to date. +// See MergeAll for more details. func merge(def Definition, repo repository.ClockedRepo, remoteRef string) entity.MergeResult { id := entity.RefToId(remoteRef) @@ -102,36 +120,24 @@ func merge(def Definition, repo repository.ClockedRepo, remoteRef string) entity localRef := fmt.Sprintf("refs/%s/%s", def.namespace, id.String()) + // SCENARIO 1 + // if the remote Entity doesn't exist locally, it's created + localExist, err := repo.RefExist(localRef) if err != nil { return entity.NewMergeError(err, id) } - // the bug is not local yet, simply create the reference if !localExist { + // the bug is not local yet, simply create the reference err := repo.CopyRef(remoteRef, localRef) if err != nil { return entity.NewMergeError(err, id) } - return entity.NewMergeStatus(entity.MergeStatusNew, id, remoteEntity) + return entity.NewMergeNewStatus(id, remoteEntity) } - // var updated bool - // err = repo.MergeRef(localRef, remoteRef, func() repository.Hash { - // updated = true - // - // }) - // if err != nil { - // return entity.NewMergeError(err, id) - // } - // - // if updated { - // return entity.NewMergeStatus(entity.MergeStatusUpdated, id, ) - // } else { - // return entity.NewMergeStatus(entity.MergeStatusNothing, id, ) - // } - localCommit, err := repo.ResolveRef(localRef) if err != nil { return entity.NewMergeError(err, id) @@ -142,18 +148,38 @@ func merge(def Definition, repo repository.ClockedRepo, remoteRef string) entity return entity.NewMergeError(err, id) } + // SCENARIO 2 + // if the remote and local Entity have the same state, nothing is changed + if localCommit == remoteCommit { // nothing to merge - return entity.NewMergeStatus(entity.MergeStatusNothing, id, remoteEntity) + return entity.NewMergeNothingStatus(id) } - // fast-forward is possible if otherRef include ref + // SCENARIO 3 + // if the local Entity has new commits but the remote don't, nothing is changed + + localCommits, err := repo.ListCommits(localRef) + if err != nil { + return entity.NewMergeError(err, id) + } + + for _, hash := range localCommits { + if hash == localCommit { + return entity.NewMergeNothingStatus(id) + } + } + + // SCENARIO 4 + // if the remote has new commit, the local bug is updated to match the same history + // (fast-forward update) remoteCommits, err := repo.ListCommits(remoteRef) if err != nil { return entity.NewMergeError(err, id) } + // fast-forward is possible if otherRef include ref fastForwardPossible := false for _, hash := range remoteCommits { if hash == localCommit { @@ -167,9 +193,13 @@ func merge(def Definition, repo repository.ClockedRepo, remoteRef string) entity if err != nil { return entity.NewMergeError(err, id) } - return entity.NewMergeStatus(entity.MergeStatusUpdated, id, remoteEntity) + return entity.NewMergeUpdatedStatus(id, remoteEntity) } + // SCENARIO 5 + // if both local and remote Entity have new commits (that is, we have a concurrent edition), + // a merge commit with an empty operationPack is created to join both branch and form a DAG. + // fast-forward is not possible, we need to create a merge commit // For simplicity when reading and to have clocks that record this change, we store // an empty operationPack. @@ -180,6 +210,7 @@ func merge(def Definition, repo repository.ClockedRepo, remoteRef string) entity return entity.NewMergeError(err, id) } + // TODO: pack clock // err = localEntity.packClock.Witness(remoteEntity.packClock.Time()) // if err != nil { // return entity.NewMergeError(err, id) @@ -199,27 +230,25 @@ func merge(def Definition, repo repository.ClockedRepo, remoteRef string) entity Operations: nil, CreateTime: 0, EditTime: editTime, + // TODO: pack clock // PackTime: packTime, } - treeHash, err := opp.Write(def, repo) - if err != nil { - return entity.NewMergeError(err, id) - } - - // Create the merge commit with two parents - newHash, err := repo.StoreCommit(treeHash, localCommit, remoteCommit) + commitHash, err := opp.Write(def, repo, localCommit, remoteCommit) if err != nil { return entity.NewMergeError(err, id) } // finally update the ref - err = repo.UpdateRef(localRef, newHash) + err = repo.UpdateRef(localRef, commitHash) if err != nil { return entity.NewMergeError(err, id) } - return entity.NewMergeStatus(entity.MergeStatusUpdated, id, localEntity) + // Note: we don't need to update localEntity state (lastCommit, operations...) as we + // discard it entirely anyway. + + return entity.NewMergeUpdatedStatus(id, localEntity) } func Remove() error { diff --git a/entity/dag/operation.go b/entity/dag/operation.go index 9fcc055be607c7a0c064a95e3d3d9affdc160b84..86e2f7d7df6a35fb7049417919670a83387db77d 100644 --- a/entity/dag/operation.go +++ b/entity/dag/operation.go @@ -12,17 +12,19 @@ type Operation interface { // Id return the Operation identifier // Some care need to be taken to define a correct Id derivation and enough entropy in the data used to avoid // collisions. Notably: - // - the Id of the first Operation will be used as the Id of the Entity. Collision need to be avoided across Entities. + // - the Id of the first Operation will be used as the Id of the Entity. Collision need to be avoided across entities of the same type + // (example: no collision within the "bug" namespace). // - collisions can also happen within the set of Operations of an Entity. Simple Operation might not have enough - // entropy to yield unique Ids. + // entropy to yield unique Ids (example: two "close" operation within the same second, same author). // A common way to derive an Id will be to use the DeriveId function on the serialized operation data. Id() entity.Id // Validate check if the Operation data is valid Validate() error - + // Author returns the author of this operation Author() identity.Interface } +// TODO: remove? type operationBase struct { author identity.Interface diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go index 7cf4ee580a7d97796cf3d74a1dc59f6f102e7a6c..ebacdbd98343df4b0e5a304b0c248a6749d04518 100644 --- a/entity/dag/operation_pack.go +++ b/entity/dag/operation_pack.go @@ -86,7 +86,10 @@ func (opp *operationPack) Validate() error { return nil } -func (opp *operationPack) Write(def Definition, repo repository.RepoData, parentCommit ...repository.Hash) (repository.Hash, error) { +// Write write the OperationPack in git, with zero, one or more parent commits. +// If the repository has a keypair able to sign (that is, with a private key), the resulting commit is signed with that key. +// Return the hash of the created commit. +func (opp *operationPack) Write(def Definition, repo repository.Repo, parentCommit ...repository.Hash) (repository.Hash, error) { if err := opp.Validate(); err != nil { return "", err } @@ -148,8 +151,13 @@ func (opp *operationPack) Write(def Definition, repo repository.RepoData, parent var commitHash repository.Hash // Sign the commit if we have a key - if opp.Author.SigningKey() != nil { - commitHash, err = repo.StoreSignedCommit(treeHash, opp.Author.SigningKey().PGPEntity(), parentCommit...) + signingKey, err := opp.Author.SigningKey(repo) + if err != nil { + return "", err + } + + if signingKey != nil { + commitHash, err = repo.StoreSignedCommit(treeHash, signingKey.PGPEntity(), parentCommit...) } else { commitHash, err = repo.StoreCommit(treeHash, parentCommit...) } @@ -240,7 +248,7 @@ func readOperationPack(def Definition, repo repository.RepoData, commit reposito // Verify signature if we expect one keys := author.ValidKeysAtTime(fmt.Sprintf(editClockPattern, def.namespace), editTime) if len(keys) > 0 { - keyring := identity.PGPKeyring(keys) + keyring := PGPKeyring(keys) _, err = openpgp.CheckDetachedSignature(keyring, commit.SignedData, commit.Signature) if err != nil { return nil, fmt.Errorf("signature failure: %v", err) @@ -292,3 +300,37 @@ func unmarshallPack(def Definition, data []byte) ([]Operation, identity.Interfac return ops, author, nil } + +var _ openpgp.KeyRing = &PGPKeyring{} + +// PGPKeyring implement a openpgp.KeyRing from an slice of Key +type PGPKeyring []*identity.Key + +func (pk PGPKeyring) KeysById(id uint64) []openpgp.Key { + var result []openpgp.Key + for _, key := range pk { + if key.Public().KeyId == id { + result = append(result, openpgp.Key{ + PublicKey: key.Public(), + PrivateKey: key.Private(), + }) + } + } + return result +} + +func (pk PGPKeyring) KeysByIdUsage(id uint64, requiredUsage byte) []openpgp.Key { + // the only usage we care about is the ability to sign, which all keys should already be capable of + return pk.KeysById(id) +} + +func (pk PGPKeyring) DecryptionKeys() []openpgp.Key { + result := make([]openpgp.Key, len(pk)) + for i, key := range pk { + result[i] = openpgp.Key{ + PublicKey: key.Public(), + PrivateKey: key.Private(), + } + } + return result +} diff --git a/entity/merge.go b/entity/merge.go index 7d1f3f432ebb67ef80bc9880cba6c3379d4e282c..1b68b4de284dc8a1952c46e946861a87fc6bfdc7 100644 --- a/entity/merge.go +++ b/entity/merge.go @@ -24,10 +24,10 @@ type MergeResult struct { Id Id Status MergeStatus - // Only set for invalid status + // Only set for Invalid status Reason string - // Not set for invalid status + // Only set for New or Updated status Entity Interface } @@ -48,29 +48,42 @@ func (mr MergeResult) String() string { } } -func NewMergeError(err error, id Id) MergeResult { +// TODO: Interface --> *Entity ? +func NewMergeNewStatus(id Id, entity Interface) MergeResult { return MergeResult{ - Err: err, Id: id, - Status: MergeStatusError, + Status: MergeStatusNew, + Entity: entity, } } -// TODO: Interface --> *Entity ? -func NewMergeStatus(status MergeStatus, id Id, entity Interface) MergeResult { +func NewMergeInvalidStatus(id Id, reason string) MergeResult { return MergeResult{ Id: id, - Status: status, + Status: MergeStatusInvalid, + Reason: reason, + } +} - // Entity is not set for an invalid merge result +func NewMergeUpdatedStatus(id Id, entity Interface) MergeResult { + return MergeResult{ + Id: id, + Status: MergeStatusUpdated, Entity: entity, } } -func NewMergeInvalidStatus(id Id, reason string) MergeResult { +func NewMergeNothingStatus(id Id) MergeResult { return MergeResult{ Id: id, - Status: MergeStatusInvalid, - Reason: reason, + Status: MergeStatusNothing, + } +} + +func NewMergeError(err error, id Id) MergeResult { + return MergeResult{ + Id: id, + Status: MergeStatusError, + Err: err, } } diff --git a/identity/identity.go b/identity/identity.go index 650190417a36a464a3d2c0c0613e94b3dcb79dc6..ad5f1efd6386a603a036cbca80f9b7446e20f96c 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -519,12 +519,19 @@ func (i *Identity) Keys() []*Key { } // SigningKey return the key that should be used to sign new messages. If no key is available, return nil. -func (i *Identity) SigningKey() *Key { +func (i *Identity) SigningKey(repo repository.RepoKeyring) (*Key, error) { keys := i.Keys() - if len(keys) > 0 { - return keys[0] + for _, key := range keys { + err := key.ensurePrivateKey(repo) + if err == errNoPrivateKey { + continue + } + if err != nil { + return nil, err + } + return key, nil } - return nil + return nil, nil } // ValidKeysAtTime return the set of keys valid at a given lamport time diff --git a/identity/identity_actions.go b/identity/identity_actions.go index 2e804533cae00f6770e1ad9310fff02e1a2de49d..21ce3fa642e61c61e65ed34ffce844bdfda8e87b 100644 --- a/identity/identity_actions.go +++ b/identity/identity_actions.go @@ -102,7 +102,7 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeRes return } - out <- entity.NewMergeStatus(entity.MergeStatusNew, id, remoteIdentity) + out <- entity.NewMergeNewStatus(id, remoteIdentity) continue } @@ -121,9 +121,9 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeRes } if updated { - out <- entity.NewMergeStatus(entity.MergeStatusUpdated, id, localIdentity) + out <- entity.NewMergeUpdatedStatus(id, localIdentity) } else { - out <- entity.NewMergeStatus(entity.MergeStatusNothing, id, localIdentity) + out <- entity.NewMergeNothingStatus(id) } } }() diff --git a/identity/identity_stub.go b/identity/identity_stub.go index 919453782cb62552eeab43e00096b167f811f957..fb5c90a5c8684356be2b7801f0fa73d184ee31b8 100644 --- a/identity/identity_stub.go +++ b/identity/identity_stub.go @@ -4,6 +4,7 @@ import ( "encoding/json" "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/lamport" "github.com/MichaelMure/git-bug/util/timestamp" ) @@ -71,7 +72,7 @@ func (IdentityStub) Keys() []*Key { panic("identities needs to be properly loaded with identity.ReadLocal()") } -func (i *IdentityStub) SigningKey() *Key { +func (i *IdentityStub) SigningKey(repo repository.RepoKeyring) (*Key, error) { panic("identities needs to be properly loaded with identity.ReadLocal()") } diff --git a/identity/interface.go b/identity/interface.go index 528cb067612caca62ce74399fffe2efdc493dc6e..5b14295b15d13c038a079df0925d0a5318b3275f 100644 --- a/identity/interface.go +++ b/identity/interface.go @@ -2,6 +2,7 @@ package identity import ( "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/lamport" "github.com/MichaelMure/git-bug/util/timestamp" ) @@ -37,7 +38,7 @@ type Interface interface { Keys() []*Key // SigningKey return the key that should be used to sign new messages. If no key is available, return nil. - SigningKey() *Key + SigningKey(repo repository.RepoKeyring) (*Key, error) // ValidKeysAtTime return the set of keys valid at a given lamport time for a given clock of another entity // Can be empty. diff --git a/identity/key.go b/identity/key.go index 8dd5e8c12f51007ffc5cd3e3834ef9725b42021f..daa66b0e2242ab64df6be291a49a9f3cc84f2b40 100644 --- a/identity/key.go +++ b/identity/key.go @@ -16,12 +16,15 @@ import ( "github.com/MichaelMure/git-bug/repository" ) +var errNoPrivateKey = fmt.Errorf("no private key") + type Key struct { public *packet.PublicKey private *packet.PrivateKey } // GenerateKey generate a keypair (public+private) +// The type and configuration of the key is determined by the default value in go's OpenPGP. func GenerateKey() *Key { entity, err := openpgp.NewEntity("", "", "", &packet.Config{ // The armored format doesn't include the creation time, which makes the round-trip data not being fully equal. @@ -47,12 +50,53 @@ func generatePublicKey() *Key { return k } +func (k *Key) Public() *packet.PublicKey { + return k.public +} + +func (k *Key) Private() *packet.PrivateKey { + return k.private +} + +func (k *Key) Validate() error { + if k.public == nil { + return fmt.Errorf("nil public key") + } + if !k.public.CanSign() { + return fmt.Errorf("public key can't sign") + } + + if k.private != nil { + if !k.private.CanSign() { + return fmt.Errorf("private key can't sign") + } + } + + return nil +} + +func (k *Key) Clone() *Key { + clone := &Key{} + + pub := *k.public + clone.public = &pub + + if k.private != nil { + priv := *k.private + clone.private = &priv + } + + return clone +} + func (k *Key) MarshalJSON() ([]byte, error) { + // Serialize only the public key, in the armored format. var buf bytes.Buffer w, err := armor.Encode(&buf, openpgp.PublicKeyType, nil) if err != nil { return nil, err } + err = k.public.Serialize(w) if err != nil { return nil, err @@ -65,6 +109,7 @@ func (k *Key) MarshalJSON() ([]byte, error) { } func (k *Key) UnmarshalJSON(data []byte) error { + // De-serialize only the public key, in the armored format. var armored string err := json.Unmarshal(data, &armored) if err != nil { @@ -83,8 +128,7 @@ func (k *Key) UnmarshalJSON(data []byte) error { return fmt.Errorf("invalid key type") } - reader := packet.NewReader(block.Body) - p, err := reader.Next() + p, err := packet.Read(block.Body) if err != nil { return errors.Wrap(err, "failed to read public key packet") } @@ -102,53 +146,74 @@ func (k *Key) UnmarshalJSON(data []byte) error { return nil } -func (k *Key) Validate() error { - if k.public == nil { - return fmt.Errorf("nil public key") +func (k *Key) loadPrivate(repo repository.RepoKeyring) error { + item, err := repo.Keyring().Get(k.public.KeyIdString()) + if err == repository.ErrKeyringKeyNotFound { + return errNoPrivateKey } - if !k.public.CanSign() { - return fmt.Errorf("public key can't sign") + if err != nil { + return err } - if k.private != nil { - if !k.private.CanSign() { - return fmt.Errorf("private key can't sign") - } + block, err := armor.Decode(bytes.NewReader(item.Data)) + if err == io.EOF { + return fmt.Errorf("no armored data found") + } + if err != nil { + return err } - return nil -} - -func (k *Key) Clone() *Key { - clone := &Key{} + if block.Type != openpgp.PrivateKeyType { + return fmt.Errorf("invalid key type") + } - pub := *k.public - clone.public = &pub + p, err := packet.Read(block.Body) + if err != nil { + return errors.Wrap(err, "failed to read private key packet") + } - if k.private != nil { - priv := *k.private - clone.private = &priv + private, ok := p.(*packet.PrivateKey) + if !ok { + return errors.New("got no packet.privateKey") } - return clone + // The armored format doesn't include the creation time, which makes the round-trip data not being fully equal. + // We don't care about the creation time so we can set it to the zero value. + private.CreationTime = time.Time{} + + k.private = private + return nil } -func (k *Key) EnsurePrivateKey(repo repository.RepoKeyring) error { +// ensurePrivateKey attempt to load the corresponding private key if it is not loaded already. +// If no private key is found, returns errNoPrivateKey +func (k *Key) ensurePrivateKey(repo repository.RepoKeyring) error { if k.private != nil { return nil } - // item, err := repo.Keyring().Get(k.Fingerprint()) - // if err != nil { - // return fmt.Errorf("no private key found for %s", k.Fingerprint()) - // } - // - - panic("TODO") + return k.loadPrivate(repo) } -func (k *Key) Fingerprint() string { - return string(k.public.Fingerprint[:]) +func (k *Key) storePrivate(repo repository.RepoKeyring) error { + var buf bytes.Buffer + w, err := armor.Encode(&buf, openpgp.PrivateKeyType, nil) + if err != nil { + return err + } + err = k.private.Serialize(w) + if err != nil { + return err + } + err = w.Close() + if err != nil { + return err + } + + return repo.Keyring().Set(repository.Item{ + Key: k.public.KeyIdString(), + Data: buf.Bytes(), + }) } func (k *Key) PGPEntity() *openpgp.Entity { @@ -157,37 +222,3 @@ func (k *Key) PGPEntity() *openpgp.Entity { PrivateKey: k.private, } } - -var _ openpgp.KeyRing = &PGPKeyring{} - -// PGPKeyring implement a openpgp.KeyRing from an slice of Key -type PGPKeyring []*Key - -func (pk PGPKeyring) KeysById(id uint64) []openpgp.Key { - var result []openpgp.Key - for _, key := range pk { - if key.public.KeyId == id { - result = append(result, openpgp.Key{ - PublicKey: key.public, - PrivateKey: key.private, - }) - } - } - return result -} - -func (pk PGPKeyring) KeysByIdUsage(id uint64, requiredUsage byte) []openpgp.Key { - // the only usage we care about is the ability to sign, which all keys should already be capable of - return pk.KeysById(id) -} - -func (pk PGPKeyring) DecryptionKeys() []openpgp.Key { - result := make([]openpgp.Key, len(pk)) - for i, key := range pk { - result[i] = openpgp.Key{ - PublicKey: key.public, - PrivateKey: key.private, - } - } - return result -} diff --git a/identity/key_test.go b/identity/key_test.go index 3206c34eea8f7c87e3ca2652f171174b603d150e..6e320dc2056dc35dcc0e4f8de2806a1d277520e9 100644 --- a/identity/key_test.go +++ b/identity/key_test.go @@ -1,21 +1,60 @@ package identity import ( + "crypto/rsa" "encoding/json" "testing" "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/repository" ) -func TestKeyJSON(t *testing.T) { +func TestPublicKeyJSON(t *testing.T) { k := generatePublicKey() - data, err := json.Marshal(k) + dataJSON, err := json.Marshal(k) require.NoError(t, err) var read Key - err = json.Unmarshal(data, &read) + err = json.Unmarshal(dataJSON, &read) require.NoError(t, err) require.Equal(t, k, &read) } + +func TestStoreLoad(t *testing.T) { + repo := repository.NewMockRepoKeyring() + + // public + private + k := GenerateKey() + + // Store + + dataJSON, err := json.Marshal(k) + require.NoError(t, err) + + err = k.storePrivate(repo) + require.NoError(t, err) + + // Load + + var read Key + err = json.Unmarshal(dataJSON, &read) + require.NoError(t, err) + + err = read.ensurePrivateKey(repo) + require.NoError(t, err) + + require.Equal(t, k.public, read.public) + + require.IsType(t, (*rsa.PrivateKey)(nil), k.private.PrivateKey) + + // See https://github.com/golang/crypto/pull/175 + rsaPriv := read.private.PrivateKey.(*rsa.PrivateKey) + back := rsaPriv.Primes[0] + rsaPriv.Primes[0] = rsaPriv.Primes[1] + rsaPriv.Primes[1] = back + + require.True(t, k.private.PrivateKey.(*rsa.PrivateKey).Equal(read.private.PrivateKey)) +} diff --git a/repository/common.go b/repository/common.go index 7fd7ae19a98e2b586ac4c276a2a83bd6bd742ff1..4cefbd9e82c6031b16963fb448192094af172e79 100644 --- a/repository/common.go +++ b/repository/common.go @@ -8,59 +8,6 @@ import ( "golang.org/x/crypto/openpgp/errors" ) -// nonNativeMerge is an implementation of a branch merge, for the case where -// the underlying git implementation doesn't support it natively. -func nonNativeMerge(repo RepoData, ref string, otherRef string, treeHashFn func() Hash) error { - commit, err := repo.ResolveRef(ref) - if err != nil { - return err - } - - otherCommit, err := repo.ResolveRef(otherRef) - if err != nil { - return err - } - - if commit == otherCommit { - // nothing to merge - return nil - } - - // fast-forward is possible if otherRef include ref - - otherCommits, err := repo.ListCommits(otherRef) - if err != nil { - return err - } - - fastForwardPossible := false - for _, hash := range otherCommits { - if hash == commit { - fastForwardPossible = true - break - } - } - - if fastForwardPossible { - return repo.UpdateRef(ref, otherCommit) - } - - // fast-forward is not possible, we need to create a merge commit - - // we need a Tree to make the commit, an empty Tree will do - emptyTreeHash, err := repo.StoreTree(nil) - if err != nil { - return err - } - - newHash, err := repo.StoreCommit(emptyTreeHash, commit, otherCommit) - if err != nil { - return err - } - - return repo.UpdateRef(ref, newHash) -} - // nonNativeListCommits is an implementation for ListCommits, for the case where // the underlying git implementation doesn't support if natively. func nonNativeListCommits(repo RepoData, ref string) ([]Hash, error) { diff --git a/repository/gogit.go b/repository/gogit.go index d6eb8621ff7dc3e82fb8b9b772a6ff539c41f866..fe434d88afabc2ce102dea7ac7602800bc3e3025 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -630,13 +630,6 @@ func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error { return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(ref), plumbing.NewHash(hash.String()))) } -// MergeRef merge other into ref and update the reference -// If the update is not fast-forward, the callback treeHashFn will be called for the caller to generate -// the Tree to store in the merge commit. -func (repo *GoGitRepo) MergeRef(ref string, otherRef string, treeHashFn func() Hash) error { - return nonNativeMerge(repo, ref, otherRef, treeHashFn) -} - // RemoveRef will remove a Git reference func (repo *GoGitRepo) RemoveRef(ref string) error { return repo.r.Storer.RemoveReference(plumbing.ReferenceName(ref)) diff --git a/repository/keyring.go b/repository/keyring.go index 64365c39e980a2711006e753be153d104e3ec4b4..6cba303e4da77a0b80af7f604f51e887b9b48dac 100644 --- a/repository/keyring.go +++ b/repository/keyring.go @@ -15,7 +15,7 @@ var ErrKeyringKeyNotFound = keyring.ErrKeyNotFound type Keyring interface { // Returns an Item matching the key or ErrKeyringKeyNotFound Get(key string) (Item, error) - // Stores an Item on the keyring + // Stores an Item on the keyring. Set is idempotent. Set(item Item) error // Removes the item with matching key Remove(key string) error diff --git a/repository/repo.go b/repository/repo.go index d7afa98343ba09400a79b98eca1fea88e91f6261..8742633365a1b6eb687123d03b2eb78f2b7f7938 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -138,11 +138,6 @@ type RepoData interface { // UpdateRef will create or update a Git reference UpdateRef(ref string, hash Hash) error - // // MergeRef merge other into ref and update the reference - // // If the update is not fast-forward, the callback treeHashFn will be called for the caller to generate - // // the Tree to store in the merge commit. - // MergeRef(ref string, otherRef string, treeHashFn func() Hash) error - // RemoveRef will remove a Git reference RemoveRef(ref string) error diff --git a/repository/repo_testing.go b/repository/repo_testing.go index 4a5c48bbade43926c8646dc54c5c8b3f4b66ad2d..92aa16911203de03ded3226265d79c686c6de42a 100644 --- a/repository/repo_testing.go +++ b/repository/repo_testing.go @@ -197,6 +197,8 @@ func RepoDataTest(t *testing.T, repo RepoData) { err = repo.RemoveRef("refs/bugs/ref1") require.NoError(t, err) + + // TODO: testing for commit's signature } // helper to test a RepoClock @@ -238,22 +240,3 @@ func randomData() []byte { } return b } - -func makeCommit(t *testing.T, repo RepoData, parents ...Hash) Hash { - blobHash, err := repo.StoreData(randomData()) - require.NoError(t, err) - - treeHash, err := repo.StoreTree([]TreeEntry{ - { - ObjectType: Blob, - Hash: blobHash, - Name: "foo", - }, - }) - require.NoError(t, err) - - commitHash, err := repo.StoreCommit(treeHash, parents...) - require.NoError(t, err) - - return commitHash -} From fe4237df3c62bd6dfd1f385893295f93072d0e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Mon, 25 Jan 2021 12:39:34 +0100 Subject: [PATCH 21/42] entity: readAll and more testing --- bug/bug_actions.go | 3 +- bug/bug_actions_test.go | 8 +-- cache/repo_cache_test.go | 2 +- entity/dag/common_test.go | 38 +++++++++-- entity/dag/entity.go | 48 ++++++++++++- entity/dag/entity_actions.go | 8 +-- entity/dag/entity_actions_test.go | 110 ++++++++++++++++++++++++++++++ repository/mock_repo.go | 4 +- repository/repo.go | 1 + repository/repo_testing.go | 3 + 10 files changed, 203 insertions(+), 22 deletions(-) create mode 100644 entity/dag/entity_actions_test.go diff --git a/bug/bug_actions.go b/bug/bug_actions.go index 40a2facba40a0de28bb39f6fced41c541d4831c3..0e24071155abe6715498208a056602c03f39fc80 100644 --- a/bug/bug_actions.go +++ b/bug/bug_actions.go @@ -4,10 +4,11 @@ import ( "fmt" "strings" + "github.com/pkg/errors" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" - "github.com/pkg/errors" ) // Fetch retrieve updates from a remote diff --git a/bug/bug_actions_test.go b/bug/bug_actions_test.go index 7a9673d6f420c22caa3ee521da7bf1c0fe1a223e..fc67106347134f76e430cc46c0ce974547783cea 100644 --- a/bug/bug_actions_test.go +++ b/bug/bug_actions_test.go @@ -12,7 +12,7 @@ import ( ) func TestPushPull(t *testing.T) { - repoA, repoB, remote := repository.SetupReposAndRemote() + repoA, repoB, remote := repository.SetupGoGitReposAndRemote() defer repository.CleanupTestRepos(repoA, repoB, remote) reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") @@ -90,7 +90,7 @@ func BenchmarkRebaseTheirs(b *testing.B) { } func _RebaseTheirs(t testing.TB) { - repoA, repoB, remote := repository.SetupReposAndRemote() + repoA, repoB, remote := repository.SetupGoGitReposAndRemote() defer repository.CleanupTestRepos(repoA, repoB, remote) reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") @@ -171,7 +171,7 @@ func BenchmarkRebaseOurs(b *testing.B) { } func _RebaseOurs(t testing.TB) { - repoA, repoB, remote := repository.SetupReposAndRemote() + repoA, repoB, remote := repository.SetupGoGitReposAndRemote() defer repository.CleanupTestRepos(repoA, repoB, remote) reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") @@ -263,7 +263,7 @@ func BenchmarkRebaseConflict(b *testing.B) { } func _RebaseConflict(t testing.TB) { - repoA, repoB, remote := repository.SetupReposAndRemote() + repoA, repoB, remote := repository.SetupGoGitReposAndRemote() defer repository.CleanupTestRepos(repoA, repoB, remote) reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") diff --git a/cache/repo_cache_test.go b/cache/repo_cache_test.go index bd06e84db518c4ec7fc476fb7398d255d62189d5..9cdd584df6161c16053011f2b9055a4497aab76b 100644 --- a/cache/repo_cache_test.go +++ b/cache/repo_cache_test.go @@ -109,7 +109,7 @@ func TestCache(t *testing.T) { } func TestPushPull(t *testing.T) { - repoA, repoB, remote := repository.SetupReposAndRemote() + repoA, repoB, remote := repository.SetupGoGitReposAndRemote() defer repository.CleanupTestRepos(repoA, repoB, remote) cacheA, err := NewRepoCache(repoA) diff --git a/entity/dag/common_test.go b/entity/dag/common_test.go index 29f1279e680eb45de85dd3ec312bfb0336becd5d..b822fc79bcc572a5f014241316990ea47544bd7d 100644 --- a/entity/dag/common_test.go +++ b/entity/dag/common_test.go @@ -26,16 +26,16 @@ func newOp1(author identity.Interface, field1 string) *op1 { return &op1{author: author, OperationType: 1, Field1: field1} } -func (o op1) Id() entity.Id { +func (o *op1) Id() entity.Id { data, _ := json.Marshal(o) return entity.DeriveId(data) } -func (o op1) Author() identity.Interface { +func (o *op1) Author() identity.Interface { return o.author } -func (o op1) Validate() error { return nil } +func (o *op1) Validate() error { return nil } type op2 struct { author identity.Interface @@ -48,16 +48,16 @@ func newOp2(author identity.Interface, field2 string) *op2 { return &op2{author: author, OperationType: 2, Field2: field2} } -func (o op2) Id() entity.Id { +func (o *op2) Id() entity.Id { data, _ := json.Marshal(o) return entity.DeriveId(data) } -func (o op2) Author() identity.Interface { +func (o *op2) Author() identity.Interface { return o.author } -func (o op2) Validate() error { return nil } +func (o *op2) Validate() error { return nil } func unmarshaler(author identity.Interface, raw json.RawMessage) (Operation, error) { var t struct { @@ -90,7 +90,31 @@ func unmarshaler(author identity.Interface, raw json.RawMessage) (Operation, err func makeTestContext() (repository.ClockedRepo, identity.Interface, identity.Interface, Definition) { repo := repository.NewMockRepo() + id1, id2, def := makeTestContextInternal(repo) + return repo, id1, id2, def +} + +func makeTestContextRemote() (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) + } + + id1, id2, def := makeTestContextInternal(repoA) + + return repoA, repoB, remote, id1, id2, def +} + +func makeTestContextInternal(repo repository.ClockedRepo) (identity.Interface, identity.Interface, Definition) { id1, err := identity.NewIdentity(repo, "name1", "email1") if err != nil { panic(err) @@ -127,7 +151,7 @@ func makeTestContext() (repository.ClockedRepo, identity.Interface, identity.Int formatVersion: 1, } - return repo, id1, id2, def + return id1, id2, def } type identityResolverFunc func(id entity.Id) (identity.Interface, error) diff --git a/entity/dag/entity.go b/entity/dag/entity.go index 63d7fc3b7c8dd0e5796064592c99aed56ac4cc3f..d3f5b482dec1097c91b590dc8bd010b38bab1e08 100644 --- a/entity/dag/entity.go +++ b/entity/dag/entity.go @@ -58,7 +58,7 @@ func New(definition Definition) *Entity { } } -// Read will read and decode a stored Entity from a repository +// Read will read and decode a stored local Entity from a repository func Read(def Definition, repo repository.ClockedRepo, id entity.Id) (*Entity, error) { if err := id.Validate(); err != nil { return nil, errors.Wrap(err, "invalid id") @@ -69,6 +69,17 @@ func Read(def Definition, repo repository.ClockedRepo, id entity.Id) (*Entity, e return read(def, repo, ref) } +// readRemote will read and decode a stored remote Entity from a repository +func readRemote(def Definition, repo repository.ClockedRepo, remote string, id entity.Id) (*Entity, error) { + if err := id.Validate(); err != nil { + return nil, errors.Wrap(err, "invalid id") + } + + ref := fmt.Sprintf("refs/remotes/%s/%s/%s", def.namespace, remote, id.String()) + + return read(def, repo, ref) +} + // read fetch from git and decode an Entity at an arbitrary git reference. func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, error) { rootHash, err := repo.ResolveRef(ref) @@ -232,6 +243,41 @@ func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, err }, nil } +type StreamedEntity struct { + Entity *Entity + Err error +} + +// ReadAll read and parse all local Entity +func ReadAll(def Definition, repo repository.ClockedRepo) <-chan StreamedEntity { + out := make(chan StreamedEntity) + + go func() { + defer close(out) + + refPrefix := fmt.Sprintf("refs/%s/", def.namespace) + + refs, err := repo.ListRefs(refPrefix) + if err != nil { + out <- StreamedEntity{Err: err} + return + } + + for _, ref := range refs { + e, err := read(def, repo, ref) + + if err != nil { + out <- StreamedEntity{Err: err} + return + } + + out <- StreamedEntity{Entity: e} + } + }() + + return out +} + // Id return the Entity identifier func (e *Entity) Id() entity.Id { // id is the id of the first operation diff --git a/entity/dag/entity_actions.go b/entity/dag/entity_actions.go index 83ff7ddce04c7bc07d7dcc387902a8f719b21201..db3a545c2fb38cf0abf333952ee6f94f226ab232 100644 --- a/entity/dag/entity_actions.go +++ b/entity/dag/entity_actions.go @@ -10,8 +10,8 @@ import ( ) // ListLocalIds list all the available local Entity's Id -func ListLocalIds(typename string, repo repository.RepoData) ([]entity.Id, error) { - refs, err := repo.ListRefs(fmt.Sprintf("refs/%s/", typename)) +func ListLocalIds(def Definition, repo repository.RepoData) ([]entity.Id, error) { + refs, err := repo.ListRefs(fmt.Sprintf("refs/%s/", def.namespace)) if err != nil { return nil, err } @@ -75,10 +75,6 @@ func Pull(def Definition, repo repository.ClockedRepo, remote string) error { func MergeAll(def Definition, repo repository.ClockedRepo, remote string) <-chan entity.MergeResult { out := make(chan entity.MergeResult) - // no caching for the merge, we load everything from git even if that means multiple - // copy of the same entity in memory. The cache layer will intercept the results to - // invalidate entities if necessary. - go func() { defer close(out) diff --git a/entity/dag/entity_actions_test.go b/entity/dag/entity_actions_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6cc544b669cf612f042df5c19228260057dab7c1 --- /dev/null +++ b/entity/dag/entity_actions_test.go @@ -0,0 +1,110 @@ +package dag + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/repository" +) + +func allEntities(t testing.TB, bugs <-chan StreamedEntity) []*Entity { + var result []*Entity + for streamed := range bugs { + if streamed.Err != nil { + t.Fatal(streamed.Err) + } + result = append(result, streamed.Entity) + } + return result +} + +func TestPushPull(t *testing.T) { + repoA, repoB, remote, id1, id2, def := makeTestContextRemote() + 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) + require.NoError(t, err) + + _, err = Push(def, repoA, "origin") + require.NoError(t, err) + + err = Pull(def, repoB, "origin") + 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")) + + err = entity.Commit(repoB) + require.NoError(t, err) + + _, err = Push(def, repoB, "origin") + require.NoError(t, err) + + err = Pull(def, repoA, "origin") + require.NoError(t, err) + + entities = allEntities(t, ReadAll(def, repoB)) + require.Len(t, entities, 2) +} + +func TestListLocalIds(t *testing.T) { + repoA, repoB, remote, id1, id2, def := makeTestContextRemote() + 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) + require.NoError(t, err) + + entity = New(def) + entity.Append(newOp2(id2, "bar")) + err = entity.Commit(repoA) + require.NoError(t, err) + + listLocalIds(t, def, repoA, 2) + listLocalIds(t, def, repoB, 0) + + _, err = Push(def, repoA, "origin") + require.NoError(t, err) + + _, err = Fetch(def, repoB, "origin") + require.NoError(t, err) + + listLocalIds(t, def, repoA, 2) + listLocalIds(t, def, repoB, 0) + + err = Pull(def, repoB, "origin") + require.NoError(t, err) + + listLocalIds(t, def, repoA, 2) + listLocalIds(t, def, repoB, 2) +} + +func listLocalIds(t *testing.T, def Definition, repo repository.RepoData, expectedCount int) { + ids, err := ListLocalIds(def, repo) + require.NoError(t, err) + require.Len(t, ids, expectedCount) +} diff --git a/repository/mock_repo.go b/repository/mock_repo.go index 095ad61c05695a45ad7bd34a6e83112ff26b58a2..adbceec2bec03888a226c27d567eafe4f3ed7573 100644 --- a/repository/mock_repo.go +++ b/repository/mock_repo.go @@ -202,12 +202,12 @@ func NewMockRepoData() *mockRepoData { } func (r *mockRepoData) FetchRefs(remote string, refSpec string) (string, error) { - return "", nil + panic("implement me") } // PushRefs push git refs to a remote func (r *mockRepoData) PushRefs(remote string, refSpec string) (string, error) { - return "", nil + panic("implement me") } func (r *mockRepoData) StoreData(data []byte) (Hash, error) { diff --git a/repository/repo.go b/repository/repo.go index 8742633365a1b6eb687123d03b2eb78f2b7f7938..c31afa2209d5c338c200a9f8864d07c018cceabc 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -130,6 +130,7 @@ type RepoData interface { ReadCommit(hash Hash) (Commit, error) // GetTreeHash return the git tree hash referenced in a commit + // Deprecated GetTreeHash(commit Hash) (Hash, error) // ResolveRef returns the hash of the target commit of the given ref diff --git a/repository/repo_testing.go b/repository/repo_testing.go index 92aa16911203de03ded3226265d79c686c6de42a..8fc109bb8d1f5fbbe76d0e43331aa2a8dce4881d 100644 --- a/repository/repo_testing.go +++ b/repository/repo_testing.go @@ -10,6 +10,9 @@ import ( "github.com/MichaelMure/git-bug/util/lamport" ) +// TODO: add tests for RepoBleve +// TODO: add tests for RepoStorage + func CleanupTestRepos(repos ...Repo) { var firstErr error for _, repo := range repos { From e35c7c4d170d1b682992c95f1c14772158501015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Fri, 5 Feb 2021 11:18:38 +0100 Subject: [PATCH 22/42] entity: more testing and bug fixing --- 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(-) diff --git a/bug/bug_actions.go b/bug/bug_actions.go index 0e24071155abe6715498208a056602c03f39fc80..bf894ef8a55f50659fd5d2265eca709337bf3bbf 100644 --- a/bug/bug_actions.go +++ b/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/>/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 diff --git a/entity/dag/common_test.go b/entity/dag/common_test.go index b822fc79bcc572a5f014241316990ea47544bd7d..05d8589807ba3e4103bd2c3621491a9b08c2ec81 100644 --- a/entity/dag/common_test.go +++ b/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 } diff --git a/entity/dag/entity_actions.go b/entity/dag/entity_actions.go index db3a545c2fb38cf0abf333952ee6f94f226ab232..edc47d52abca0705dec4b02b688f72ad5b6af536 100644 --- a/entity/dag/entity_actions.go +++ b/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//*:refs/remotes///*" - 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//*:refs//*" - 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 diff --git a/entity/dag/entity_actions_test.go b/entity/dag/entity_actions_test.go index 6cc544b669cf612f042df5c19228260057dab7c1..d7717056bacf61ed313f3e72e47a9e922e8eb435 100644 --- a/entity/dag/entity_actions_test.go +++ b/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) +} diff --git a/identity/identity_actions.go b/identity/identity_actions.go index 21ce3fa642e61c61e65ed34ffce844bdfda8e87b..b58bb2d9df2f7f2f23a58fb5a91c140888e243d5 100644 --- a/identity/identity_actions.go +++ b/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//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 diff --git a/repository/gogit.go b/repository/gogit.go index fe434d88afabc2ce102dea7ac7602800bc3e3025..c26fde9fe2d3018efe3f4735a5fedbad6047fe44 100644 --- a/repository/gogit.go +++ b/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//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//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//... 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 { diff --git a/repository/mock_repo.go b/repository/mock_repo.go index adbceec2bec03888a226c27d567eafe4f3ed7573..12cf375b4c045d7c1e3a154f0220daa4ef6e4845 100644 --- a/repository/mock_repo.go +++ b/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") } diff --git a/repository/repo.go b/repository/repo.go index c31afa2209d5c338c200a9f8864d07c018cceabc..8d162c6d9dbb1249c9ad53f94998d3acba140dd0 100644 --- a/repository/repo.go +++ b/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//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//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) From 32c55a4985cf897774e508b13c3e63b1935d1470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 7 Feb 2021 13:52:04 +0100 Subject: [PATCH 23/42] entity: use BFS instead of DFS to get the proper topological order --- entity/dag/entity.go | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/entity/dag/entity.go b/entity/dag/entity.go index d3f5b482dec1097c91b590dc8bd010b38bab1e08..273e6ad12e29890f98808fe14e74f09d83931c07 100644 --- a/entity/dag/entity.go +++ b/entity/dag/entity.go @@ -87,36 +87,34 @@ func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, err return nil, err } - // Perform a depth-first search to get a topological order of the DAG where we discover the + // Perform a breadth-first search to get a topological order of the DAG where we discover the // parents commit and go back in time up to the chronological root - stack := make([]repository.Hash, 0, 32) + queue := make([]repository.Hash, 0, 32) visited := make(map[repository.Hash]struct{}) - DFSOrder := make([]repository.Commit, 0, 32) + BFSOrder := make([]repository.Commit, 0, 32) - stack = append(stack, rootHash) + queue = append(queue, rootHash) + visited[rootHash] = struct{}{} - for len(stack) > 0 { + for len(queue) > 0 { // pop - hash := stack[len(stack)-1] - stack = stack[:len(stack)-1] - - if _, ok := visited[hash]; ok { - continue - } - - // mark as visited - visited[hash] = struct{}{} + hash := queue[0] + queue = queue[1:] commit, err := repo.ReadCommit(hash) if err != nil { return nil, err } - DFSOrder = append(DFSOrder, commit) + BFSOrder = append(BFSOrder, commit) for _, parent := range commit.Parents { - stack = append(stack, parent) + if _, ok := visited[parent]; !ok { + queue = append(queue, parent) + // mark as visited + visited[parent] = struct{}{} + } } } @@ -131,9 +129,9 @@ func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, err var opsCount int // var packClock = lamport.NewMemClock() - for i := len(DFSOrder) - 1; i >= 0; i-- { - commit := DFSOrder[i] - isFirstCommit := i == len(DFSOrder)-1 + for i := len(BFSOrder) - 1; i >= 0; i-- { + commit := BFSOrder[i] + isFirstCommit := i == len(BFSOrder)-1 isMerge := len(commit.Parents) > 1 // Verify DAG structure: single chronological root, so only the root From 26a4b0332e0f0a52026ac6e333e0bbd78a588171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 7 Feb 2021 13:54:03 +0100 Subject: [PATCH 24/42] entity: test all merge scenario --- entity/dag/entity_actions.go | 14 +- entity/dag/entity_actions_test.go | 214 +++++++++++++++++++++++++++--- 2 files changed, 200 insertions(+), 28 deletions(-) diff --git a/entity/dag/entity_actions.go b/entity/dag/entity_actions.go index edc47d52abca0705dec4b02b688f72ad5b6af536..fe9125570c1c21bc4ce66602b7bae9a78a782322 100644 --- a/entity/dag/entity_actions.go +++ b/entity/dag/entity_actions.go @@ -6,6 +6,7 @@ import ( "github.com/pkg/errors" "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" ) @@ -31,13 +32,13 @@ func Push(def Definition, repo repository.Repo, remote string) (string, error) { // Pull will do a Fetch + MergeAll // Contrary to MergeAll, this function will return an error if a merge fail. -func Pull(def Definition, repo repository.ClockedRepo, remote string) error { +func Pull(def Definition, repo repository.ClockedRepo, remote string, author identity.Interface) error { _, err := Fetch(def, repo, remote) if err != nil { return err } - for merge := range MergeAll(def, repo, remote) { + for merge := range MergeAll(def, repo, remote, author) { if merge.Err != nil { return merge.Err } @@ -64,7 +65,7 @@ func Pull(def Definition, repo repository.ClockedRepo, remote string) error { // 5. if both local and remote Entity have new commits (that is, we have a concurrent edition), // a merge commit with an empty operationPack is created to join both branch and form a DAG. // --> emit entity.MergeStatusUpdated -func MergeAll(def Definition, repo repository.ClockedRepo, remote string) <-chan entity.MergeResult { +func MergeAll(def Definition, repo repository.ClockedRepo, remote string, author identity.Interface) <-chan entity.MergeResult { out := make(chan entity.MergeResult) go func() { @@ -78,7 +79,7 @@ func MergeAll(def Definition, repo repository.ClockedRepo, remote string) <-chan } for _, remoteRef := range remoteRefs { - out <- merge(def, repo, remoteRef) + out <- merge(def, repo, remoteRef, author) } }() @@ -87,7 +88,7 @@ func MergeAll(def Definition, repo repository.ClockedRepo, remote string) <-chan // merge perform a merge to make sure a local Entity is up to date. // See MergeAll for more details. -func merge(def Definition, repo repository.ClockedRepo, remoteRef string) entity.MergeResult { +func merge(def Definition, repo repository.ClockedRepo, remoteRef string, author identity.Interface) entity.MergeResult { id := entity.RefToId(remoteRef) if err := id.Validate(); err != nil { @@ -153,7 +154,7 @@ func merge(def Definition, repo repository.ClockedRepo, remoteRef string) entity } for _, hash := range localCommits { - if hash == localCommit { + if hash == remoteCommit { return entity.NewMergeNothingStatus(id) } } @@ -215,6 +216,7 @@ func merge(def Definition, repo repository.ClockedRepo, remoteRef string) entity } opp := &operationPack{ + Author: author, Operations: nil, CreateTime: 0, EditTime: editTime, diff --git a/entity/dag/entity_actions_test.go b/entity/dag/entity_actions_test.go index d7717056bacf61ed313f3e72e47a9e922e8eb435..78baf41fee70697f144b30a64579b6bc7d2917a0 100644 --- a/entity/dag/entity_actions_test.go +++ b/entity/dag/entity_actions_test.go @@ -2,6 +2,7 @@ package dag import ( "sort" + "strings" "testing" "github.com/stretchr/testify/require" @@ -36,7 +37,7 @@ func TestPushPull(t *testing.T) { _, err = Push(def, repoA, "remote") require.NoError(t, err) - err = Pull(def, repoB, "remote") + err = Pull(def, repoB, "remote", id1) require.NoError(t, err) entities := allEntities(t, ReadAll(def, repoB)) @@ -52,7 +53,7 @@ func TestPushPull(t *testing.T) { _, err = Push(def, repoB, "remote") require.NoError(t, err) - err = Pull(def, repoA, "remote") + err = Pull(def, repoA, "remote", id1) require.NoError(t, err) entities = allEntities(t, ReadAll(def, repoB)) @@ -86,7 +87,7 @@ func TestListLocalIds(t *testing.T) { listLocalIds(t, def, repoA, 2) listLocalIds(t, def, repoB, 0) - err = Pull(def, repoB, "remote") + err = Pull(def, repoB, "remote", id1) require.NoError(t, err) listLocalIds(t, def, repoA, 2) @@ -132,6 +133,78 @@ func assertMergeResults(t *testing.T, expected []entity.MergeResult, results <-c } } +func assertEqualRefs(t *testing.T, repoA, repoB repository.RepoData, prefix string) { + t.Helper() + + refsA, err := repoA.ListRefs("") + require.NoError(t, err) + + var refsAFiltered []string + for _, ref := range refsA { + if strings.HasPrefix(ref, prefix) { + refsAFiltered = append(refsAFiltered, ref) + } + } + + refsB, err := repoB.ListRefs("") + require.NoError(t, err) + + var refsBFiltered []string + for _, ref := range refsB { + if strings.HasPrefix(ref, prefix) { + refsBFiltered = append(refsBFiltered, ref) + } + } + + require.NotEmpty(t, refsAFiltered) + require.Equal(t, refsAFiltered, refsBFiltered) + + for _, ref := range refsAFiltered { + commitA, err := repoA.ResolveRef(ref) + require.NoError(t, err) + commitB, err := repoB.ResolveRef(ref) + require.NoError(t, err) + + require.Equal(t, commitA, commitB) + } +} + +func assertNotEqualRefs(t *testing.T, repoA, repoB repository.RepoData, prefix string) { + t.Helper() + + refsA, err := repoA.ListRefs("") + require.NoError(t, err) + + var refsAFiltered []string + for _, ref := range refsA { + if strings.HasPrefix(ref, prefix) { + refsAFiltered = append(refsAFiltered, ref) + } + } + + refsB, err := repoB.ListRefs("") + require.NoError(t, err) + + var refsBFiltered []string + for _, ref := range refsB { + if strings.HasPrefix(ref, prefix) { + refsBFiltered = append(refsBFiltered, ref) + } + } + + require.NotEmpty(t, refsAFiltered) + require.Equal(t, refsAFiltered, refsBFiltered) + + for _, ref := range refsAFiltered { + commitA, err := repoA.ResolveRef(ref) + require.NoError(t, err) + commitB, err := repoB.ResolveRef(ref) + require.NoError(t, err) + + require.NotEqual(t, commitA, commitB) + } +} + func TestMerge(t *testing.T) { repoA, repoB, remote, id1, id2, def := makeTestContextRemote(t) defer repository.CleanupTestRepos(repoA, repoB, remote) @@ -140,14 +213,14 @@ func TestMerge(t *testing.T) { // 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) + e1A := New(def) + e1A.Append(newOp1(id1, "foo")) + err := e1A.Commit(repoA) require.NoError(t, err) - e2 := New(def) - e2.Append(newOp2(id2, "bar")) - err = e2.Commit(repoA) + e2A := New(def) + e2A.Append(newOp2(id2, "bar")) + err = e2A.Commit(repoA) require.NoError(t, err) _, err = Push(def, repoA, "remote") @@ -158,60 +231,157 @@ func TestMerge(t *testing.T) { _, err = Fetch(def, repoB, "remote") require.NoError(t, err) - results := MergeAll(def, repoB, "remote") + results := MergeAll(def, repoB, "remote", id1) assertMergeResults(t, []entity.MergeResult{ { - Id: e1.Id(), + Id: e1A.Id(), Status: entity.MergeStatusNew, }, { - Id: e2.Id(), + Id: e2A.Id(), Status: entity.MergeStatusNew, }, }, results) + assertEqualRefs(t, repoA, repoB, "refs/"+def.namespace) + // SCENARIO 2 // if the remote and local Entity have the same state, nothing is changed - results = MergeAll(def, repoB, "remote") + results = MergeAll(def, repoB, "remote", id1) assertMergeResults(t, []entity.MergeResult{ { - Id: e1.Id(), + Id: e1A.Id(), Status: entity.MergeStatusNothing, }, { - Id: e2.Id(), + Id: e2A.Id(), Status: entity.MergeStatusNothing, }, }, results) + assertEqualRefs(t, repoA, repoB, "refs/"+def.namespace) + // 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) + e1A.Append(newOp1(id1, "barbar")) + err = e1A.Commit(repoA) require.NoError(t, err) - e2.Append(newOp2(id2, "barbarbar")) - err = e2.Commit(repoA) + e2A.Append(newOp2(id2, "barbarbar")) + err = e2A.Commit(repoA) require.NoError(t, err) - results = MergeAll(def, repoA, "remote") + results = MergeAll(def, repoA, "remote", id1) assertMergeResults(t, []entity.MergeResult{ { - Id: e1.Id(), + Id: e1A.Id(), Status: entity.MergeStatusNothing, }, { - Id: e2.Id(), + Id: e2A.Id(), Status: entity.MergeStatusNothing, }, }, results) + assertNotEqualRefs(t, repoA, repoB, "refs/"+def.namespace) + // SCENARIO 4 // if the remote has new commit, the local bug is updated to match the same history // (fast-forward update) + + _, err = Push(def, repoA, "remote") + require.NoError(t, err) + + _, err = Fetch(def, repoB, "remote") + require.NoError(t, err) + + results = MergeAll(def, repoB, "remote", id1) + + assertMergeResults(t, []entity.MergeResult{ + { + Id: e1A.Id(), + Status: entity.MergeStatusUpdated, + }, + { + Id: e2A.Id(), + Status: entity.MergeStatusUpdated, + }, + }, results) + + assertEqualRefs(t, repoA, repoB, "refs/"+def.namespace) + + // SCENARIO 5 + // if both local and remote Entity have new commits (that is, we have a concurrent edition), + // a merge commit with an empty operationPack is created to join both branch and form a DAG. + + e1A.Append(newOp1(id1, "barbarfoo")) + err = e1A.Commit(repoA) + require.NoError(t, err) + + e2A.Append(newOp2(id2, "barbarbarfoo")) + err = e2A.Commit(repoA) + require.NoError(t, err) + + e1B, err := Read(def, repoB, e1A.Id()) + require.NoError(t, err) + + e2B, err := Read(def, repoB, e2A.Id()) + require.NoError(t, err) + + e1B.Append(newOp1(id1, "barbarfoofoo")) + err = e1B.Commit(repoB) + require.NoError(t, err) + + e2B.Append(newOp2(id2, "barbarbarfoofoo")) + err = e2B.Commit(repoB) + require.NoError(t, err) + + _, err = Push(def, repoA, "remote") + require.NoError(t, err) + + _, err = Fetch(def, repoB, "remote") + require.NoError(t, err) + + results = MergeAll(def, repoB, "remote", id1) + + assertMergeResults(t, []entity.MergeResult{ + { + Id: e1A.Id(), + Status: entity.MergeStatusUpdated, + }, + { + Id: e2A.Id(), + Status: entity.MergeStatusUpdated, + }, + }, results) + + assertNotEqualRefs(t, repoA, repoB, "refs/"+def.namespace) + + _, err = Push(def, repoB, "remote") + require.NoError(t, err) + + _, err = Fetch(def, repoA, "remote") + require.NoError(t, err) + + results = MergeAll(def, repoA, "remote", id1) + + assertMergeResults(t, []entity.MergeResult{ + { + Id: e1A.Id(), + Status: entity.MergeStatusUpdated, + }, + { + Id: e2A.Id(), + Status: entity.MergeStatusUpdated, + }, + }, results) + + // make sure that the graphs become stable over multiple repo, due to the + // fast-forward + assertEqualRefs(t, repoA, repoB, "refs/"+def.namespace) } From 2bdb1b60ff83de157f1a0d9ed42555d96b945fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 9 Feb 2021 10:46:33 +0100 Subject: [PATCH 25/42] entity: working commit signatures --- entity/dag/operation_pack_test.go | 44 +++++++++++++++++++++++++++ repository/gogit.go | 16 ++++++---- repository/mock_repo.go | 2 ++ repository/repo_testing.go | 50 ++++++++++++++++++++++++++++++- 4 files changed, 105 insertions(+), 7 deletions(-) diff --git a/entity/dag/operation_pack_test.go b/entity/dag/operation_pack_test.go index ad2a985967c91d76d6b7f5743c50986ae81ed472..ac9797765a1fb442bea45ccdf79db7ce7dcd71a2 100644 --- a/entity/dag/operation_pack_test.go +++ b/entity/dag/operation_pack_test.go @@ -4,6 +4,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/identity" ) func TestOperationPackReadWrite(t *testing.T) { @@ -42,3 +44,45 @@ func TestOperationPackReadWrite(t *testing.T) { } require.Equal(t, opp.Id(), opp3.Id()) } + +func TestOperationPackSignedReadWrite(t *testing.T) { + repo, id1, _, def := makeTestContext() + + err := id1.(*identity.Identity).Mutate(repo, func(orig *identity.Mutator) { + orig.Keys = append(orig.Keys, identity.GenerateKey()) + }) + require.NoError(t, err) + + opp := &operationPack{ + Author: id1, + Operations: []Operation{ + newOp1(id1, "foo"), + newOp2(id1, "bar"), + }, + CreateTime: 123, + EditTime: 456, + } + + commitHash, err := opp.Write(def, repo) + require.NoError(t, err) + + commit, err := repo.ReadCommit(commitHash) + require.NoError(t, err) + + opp2, err := readOperationPack(def, repo, commit) + require.NoError(t, err) + + require.Equal(t, opp, opp2) + + // make sure we get the same Id with the same data + opp3 := &operationPack{ + Author: id1, + Operations: []Operation{ + newOp1(id1, "foo"), + newOp2(id1, "bar"), + }, + CreateTime: 123, + EditTime: 456, + } + require.Equal(t, opp.Id(), opp3.Id()) +} diff --git a/repository/gogit.go b/repository/gogit.go index c26fde9fe2d3018efe3f4735a5fedbad6047fe44..736729333b450bb5bf54d5038c7ae6b1333411b0 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -715,12 +715,7 @@ func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) { } func (repo *GoGitRepo) ReadCommit(hash Hash) (Commit, error) { - encoded, err := repo.r.Storer.EncodedObject(plumbing.CommitObject, plumbing.NewHash(hash.String())) - if err != nil { - return Commit{}, err - } - - commit, err := object.DecodeCommit(repo.r.Storer, encoded) + commit, err := repo.r.CommitObject(plumbing.NewHash(hash.String())) if err != nil { return Commit{}, err } @@ -737,6 +732,15 @@ func (repo *GoGitRepo) ReadCommit(hash Hash) (Commit, error) { } if commit.PGPSignature != "" { + // I can't find a way to just remove the signature when reading the encoded commit so we need to + // re-encode the commit without signature. + + encoded := &plumbing.MemoryObject{} + err := commit.EncodeWithoutSignature(encoded) + if err != nil { + return Commit{}, err + } + result.SignedData, err = encoded.Reader() if err != nil { return Commit{}, err diff --git a/repository/mock_repo.go b/repository/mock_repo.go index 12cf375b4c045d7c1e3a154f0220daa4ef6e4845..2749bfbdae3fc820191ca42036cea3193dc6b4dc 100644 --- a/repository/mock_repo.go +++ b/repository/mock_repo.go @@ -299,6 +299,8 @@ func (r *mockRepoData) ReadCommit(hash Hash) (Commit, error) { } if c.sig != "" { + // Note: this is actually incorrect as the signed data should be the full commit (+comment, +date ...) + // but only the tree hash work for our purpose here. result.SignedData = strings.NewReader(string(c.treeHash)) result.Signature = strings.NewReader(c.sig) } diff --git a/repository/repo_testing.go b/repository/repo_testing.go index 8fc109bb8d1f5fbbe76d0e43331aa2a8dce4881d..cdcb9008067b41bd1070c0502e97612cb70201a9 100644 --- a/repository/repo_testing.go +++ b/repository/repo_testing.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "golang.org/x/crypto/openpgp" "github.com/MichaelMure/git-bug/util/lamport" ) @@ -47,6 +48,7 @@ func RepoTest(t *testing.T, creator RepoCreator, cleaner RepoCleaner) { t.Run("Data", func(t *testing.T) { RepoDataTest(t, repo) + RepoDataSignatureTest(t, repo) }) t.Run("Config", func(t *testing.T) { @@ -200,8 +202,54 @@ func RepoDataTest(t *testing.T, repo RepoData) { err = repo.RemoveRef("refs/bugs/ref1") require.NoError(t, err) +} + +func RepoDataSignatureTest(t *testing.T, repo RepoData) { + data := randomData() + + blobHash, err := repo.StoreData(data) + require.NoError(t, err) + + treeHash, err := repo.StoreTree([]TreeEntry{ + { + ObjectType: Blob, + Hash: blobHash, + Name: "blob", + }, + }) + require.NoError(t, err) + + pgpEntity1, err := openpgp.NewEntity("", "", "", nil) + require.NoError(t, err) + keyring1 := openpgp.EntityList{pgpEntity1} + + pgpEntity2, err := openpgp.NewEntity("", "", "", nil) + require.NoError(t, err) + keyring2 := openpgp.EntityList{pgpEntity2} + + commitHash1, err := repo.StoreSignedCommit(treeHash, pgpEntity1) + require.NoError(t, err) + + commit1, err := repo.ReadCommit(commitHash1) + require.NoError(t, err) + + _, err = openpgp.CheckDetachedSignature(keyring1, commit1.SignedData, commit1.Signature) + require.NoError(t, err) + + _, err = openpgp.CheckDetachedSignature(keyring2, commit1.SignedData, commit1.Signature) + require.Error(t, err) + + commitHash2, err := repo.StoreSignedCommit(treeHash, pgpEntity1, commitHash1) + require.NoError(t, err) + + commit2, err := repo.ReadCommit(commitHash2) + require.NoError(t, err) + + _, err = openpgp.CheckDetachedSignature(keyring1, commit2.SignedData, commit2.Signature) + require.NoError(t, err) - // TODO: testing for commit's signature + _, err = openpgp.CheckDetachedSignature(keyring2, commit2.SignedData, commit2.Signature) + require.Error(t, err) } // helper to test a RepoClock From f5b92dcd53c744dc12883a35ef2e8e564a8b69de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 9 Feb 2021 10:51:14 +0100 Subject: [PATCH 26/42] require go 1.15 --- .github/workflows/go.yml | 2 +- go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 3ce22f3a0f0ef9cc926c5fdb764aa6cc4bd94145..8200a84a47a18c023ca4e59681e67a565e9ef169 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - go-version: [1.13.x, 1.14.x, 1.15.x] + go-version: [1.15.x] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} diff --git a/go.mod b/go.mod index 69e62bcc3cfd7a60fe888b813bb4a4d5b72eb6f1..fa75caaf8189c7834c317762d0b9bcbd6a4d9e4c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/MichaelMure/git-bug -go 1.13 +go 1.15 require ( github.com/99designs/gqlgen v0.10.3-0.20200209012558-b7a58a1c0e4b From f74166914c344329f08823770982f12966c79a77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 9 Feb 2021 15:47:07 +0100 Subject: [PATCH 27/42] entity: remove the pack lamport time that doesn't bring anything actually --- entity/dag/entity.go | 32 +++---------------- entity/dag/entity_actions.go | 13 -------- entity/dag/entity_test.go | 60 ------------------------------------ entity/dag/operation_pack.go | 17 ---------- 4 files changed, 4 insertions(+), 118 deletions(-) diff --git a/entity/dag/entity.go b/entity/dag/entity.go index 273e6ad12e29890f98808fe14e74f09d83931c07..3f4dfcb4b1893972ae524c428274c4afacb6a831 100644 --- a/entity/dag/entity.go +++ b/entity/dag/entity.go @@ -44,9 +44,6 @@ type Entity struct { // TODO: add here createTime and editTime - // // TODO: doesn't seems to actually be useful over the topological sort ? Timestamp can be generated from graph depth - // // TODO: maybe EditTime is better because it could spread ops in consecutive groups on the logical timeline --> avoid interleaving - // packClock lamport.Clock lastCommit repository.Hash } @@ -54,7 +51,6 @@ type Entity struct { func New(definition Definition) *Entity { return &Entity{ Definition: definition, - // packClock: lamport.NewMemClock(), } } @@ -127,7 +123,6 @@ func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, err oppMap := make(map[repository.Hash]*operationPack) var opsCount int - // var packClock = lamport.NewMemClock() for i := len(BFSOrder) - 1; i >= 0; i-- { commit := BFSOrder[i] @@ -174,8 +169,6 @@ func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, err if !isMerge && opp.EditTime-parentPack.EditTime > 1_000_000 { return nil, fmt.Errorf("lamport clock jumping too far in the future, likely an attack") } - - // TODO: PackTime is not checked } oppMap[commit.Hash] = opp @@ -192,10 +185,6 @@ func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, err if err != nil { return nil, err } - // err = packClock.Witness(opp.PackTime) - // if err != nil { - // return nil, err - // } } // Now that we know that the topological order and clocks are fine, we order the operationPacks @@ -206,19 +195,13 @@ func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, err oppSlice = append(oppSlice, pack) } sort.Slice(oppSlice, func(i, j int) bool { - // Primary ordering with the dedicated "pack" Lamport time that encode causality - // within the entity - // if oppSlice[i].PackTime != oppSlice[j].PackTime { - // return oppSlice[i].PackTime < oppSlice[i].PackTime - // } - // We have equal PackTime, which means we had a concurrent edition. We can't tell which exactly - // came first. As a secondary arbitrary ordering, we can use the EditTime. It's unlikely to be - // enough but it can give us an edge to approach what really happened. + // Primary ordering with the EditTime. if oppSlice[i].EditTime != oppSlice[j].EditTime { return oppSlice[i].EditTime < oppSlice[j].EditTime } - // Well, what now? We still need a total ordering and the most stable possible. - // As a last resort, we can order based on a hash of the serialized Operations in the + // We have equal EditTime, which means we have concurrent edition over different machines and we + // can't tell which one came first. So, what now? We still need a total ordering and the most stable possible. + // As a secondary ordering, we can order based on a hash of the serialized Operations in the // operationPack. It doesn't carry much meaning but it's unbiased and hard to abuse. // This is a lexicographic ordering on the stringified ID. return oppSlice[i].Id() < oppSlice[j].Id() @@ -236,7 +219,6 @@ func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, err return &Entity{ Definition: def, ops: ops, - // packClock: packClock, lastCommit: rootHash, }, nil } @@ -379,11 +361,6 @@ func (e *Entity) Commit(repo repository.ClockedRepo) error { author = op.Author() } - // increment the various clocks for this new operationPack - // packTime, err := e.packClock.Increment() - // if err != nil { - // return err - // } editTime, err := repo.Increment(fmt.Sprintf(editClockPattern, e.namespace)) if err != nil { return err @@ -401,7 +378,6 @@ func (e *Entity) Commit(repo repository.ClockedRepo) error { Operations: e.staging, CreateTime: creationTime, EditTime: editTime, - // PackTime: packTime, } var commitHash repository.Hash diff --git a/entity/dag/entity_actions.go b/entity/dag/entity_actions.go index fe9125570c1c21bc4ce66602b7bae9a78a782322..6f6fe45ce7ffcef40274f80ee11bf9ae83c1e850 100644 --- a/entity/dag/entity_actions.go +++ b/entity/dag/entity_actions.go @@ -199,17 +199,6 @@ func merge(def Definition, repo repository.ClockedRepo, remoteRef string, author return entity.NewMergeError(err, id) } - // TODO: pack clock - // err = localEntity.packClock.Witness(remoteEntity.packClock.Time()) - // if err != nil { - // return entity.NewMergeError(err, id) - // } - // - // packTime, err := localEntity.packClock.Increment() - // if err != nil { - // return entity.NewMergeError(err, id) - // } - editTime, err := repo.Increment(fmt.Sprintf(editClockPattern, def.namespace)) if err != nil { return entity.NewMergeError(err, id) @@ -220,8 +209,6 @@ func merge(def Definition, repo repository.ClockedRepo, remoteRef string, author Operations: nil, CreateTime: 0, EditTime: editTime, - // TODO: pack clock - // PackTime: packTime, } commitHash, err := opp.Write(def, repo, localCommit, remoteCommit) diff --git a/entity/dag/entity_test.go b/entity/dag/entity_test.go index c5c835670abd9140c3824e616051a7b059b1538d..012c87aaf72e3f691e8588292b72be34e9bd477d 100644 --- a/entity/dag/entity_test.go +++ b/entity/dag/entity_test.go @@ -55,63 +55,3 @@ func assertEqualEntities(t *testing.T, a, b *Entity) { require.Equal(t, a, b) } - -// // Merge -// -// merge1 := makeCommit(t, repo) -// merge1 = makeCommit(t, repo, merge1) -// err = repo.UpdateRef("merge1", merge1) -// require.NoError(t, err) -// -// err = repo.UpdateRef("merge2", merge1) -// require.NoError(t, err) -// -// // identical merge -// err = repo.MergeRef("merge1", "merge2") -// require.NoError(t, err) -// -// refMerge1, err := repo.ResolveRef("merge1") -// require.NoError(t, err) -// require.Equal(t, merge1, refMerge1) -// refMerge2, err := repo.ResolveRef("merge2") -// require.NoError(t, err) -// require.Equal(t, merge1, refMerge2) -// -// // fast-forward merge -// merge2 := makeCommit(t, repo, merge1) -// merge2 = makeCommit(t, repo, merge2) -// -// err = repo.UpdateRef("merge2", merge2) -// require.NoError(t, err) -// -// err = repo.MergeRef("merge1", "merge2") -// require.NoError(t, err) -// -// refMerge1, err = repo.ResolveRef("merge1") -// require.NoError(t, err) -// require.Equal(t, merge2, refMerge1) -// refMerge2, err = repo.ResolveRef("merge2") -// require.NoError(t, err) -// require.Equal(t, merge2, refMerge2) -// -// // merge commit -// merge1 = makeCommit(t, repo, merge1) -// err = repo.UpdateRef("merge1", merge1) -// require.NoError(t, err) -// -// merge2 = makeCommit(t, repo, merge2) -// err = repo.UpdateRef("merge2", merge2) -// require.NoError(t, err) -// -// err = repo.MergeRef("merge1", "merge2") -// require.NoError(t, err) -// -// refMerge1, err = repo.ResolveRef("merge1") -// require.NoError(t, err) -// require.NotEqual(t, merge1, refMerge1) -// commitRefMerge1, err := repo.ReadCommit(refMerge1) -// require.NoError(t, err) -// require.ElementsMatch(t, commitRefMerge1.Parents, []Hash{merge1, merge2}) -// refMerge2, err = repo.ResolveRef("merge2") -// require.NoError(t, err) -// require.Equal(t, merge2, refMerge2) diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go index ebacdbd98343df4b0e5a304b0c248a6749d04518..959b1ae05ef27a63cafc0282ebe42401e22ca805 100644 --- a/entity/dag/operation_pack.go +++ b/entity/dag/operation_pack.go @@ -22,7 +22,6 @@ const opsEntryName = "ops" const versionEntryPrefix = "version-" const createClockEntryPrefix = "create-clock-" const editClockEntryPrefix = "edit-clock-" -const packClockEntryPrefix = "pack-clock-" // operationPack is a wrapper structure to store multiple operations in a single git blob. // Additionally, it holds and store the metadata for those operations. @@ -40,9 +39,6 @@ type operationPack struct { // Encode the entity's logical time of last edition across all entities of the same type. // Exist on all operationPack EditTime lamport.Time - // // Encode the operationPack's logical time of creation withing this entity. - // // Exist on all operationPack - // PackTime lamport.Time } func (opp *operationPack) Id() entity.Id { @@ -129,8 +125,6 @@ func (opp *operationPack) Write(def Definition, repo repository.Repo, parentComm Name: opsEntryName}, {ObjectType: repository.Blob, Hash: emptyBlobHash, Name: fmt.Sprintf(editClockEntryPrefix+"%d", opp.EditTime)}, - // {ObjectType: repository.Blob, Hash: emptyBlobHash, - // Name: fmt.Sprintf(packClockEntryPrefix+"%d", opp.PackTime)}, } if opp.CreateTime > 0 { tree = append(tree, repository.TreeEntry{ @@ -205,7 +199,6 @@ func readOperationPack(def Definition, repo repository.RepoData, commit reposito var ops []Operation var createTime lamport.Time var editTime lamport.Time - // var packTime lamport.Time for _, entry := range entries { switch { @@ -233,15 +226,6 @@ func readOperationPack(def Definition, repo repository.RepoData, commit reposito return nil, errors.Wrap(err, "can't read edit lamport time") } editTime = lamport.Time(v) - - // case strings.HasPrefix(entry.Name, packClockEntryPrefix): - // found &= 1 << 3 - // - // v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, packClockEntryPrefix), 10, 64) - // if err != nil { - // return nil, errors.Wrap(err, "can't read pack lamport time") - // } - // packTime = lamport.Time(v) } } @@ -261,7 +245,6 @@ func readOperationPack(def Definition, repo repository.RepoData, commit reposito Operations: ops, CreateTime: createTime, EditTime: editTime, - // PackTime: packTime, }, nil } From ef05c15f87468e0f4f1c688b0b9359cee2181c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 10 Feb 2021 18:22:21 +0100 Subject: [PATCH 28/42] entity: implement remove --- entity/dag/entity_actions.go | 28 ++++++++++++++++++++++++++-- entity/dag/entity_actions_test.go | 25 +++++++++++++++++++++++++ repository/repo.go | 1 + repository/repo_testing.go | 4 ++++ 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/entity/dag/entity_actions.go b/entity/dag/entity_actions.go index 6f6fe45ce7ffcef40274f80ee11bf9ae83c1e850..fa50473cde906df035a3d8aa1080b76d55762369 100644 --- a/entity/dag/entity_actions.go +++ b/entity/dag/entity_actions.go @@ -228,6 +228,30 @@ func merge(def Definition, repo repository.ClockedRepo, remoteRef string, author return entity.NewMergeUpdatedStatus(id, localEntity) } -func Remove() error { - panic("") +// Remove delete an Entity. +// Remove is idempotent. +func Remove(def Definition, repo repository.ClockedRepo, id entity.Id) error { + var matches []string + + ref := fmt.Sprintf("refs/%s/%s", def.namespace, id.String()) + matches = append(matches, ref) + + remotes, err := repo.GetRemotes() + if err != nil { + return err + } + + for remote := range remotes { + ref = fmt.Sprintf("refs/remotes/%s/%s/%s", remote, def.namespace, id.String()) + matches = append(matches, ref) + } + + for _, ref = range matches { + err = repo.RemoveRef(ref) + if err != nil { + return err + } + } + + return nil } diff --git a/entity/dag/entity_actions_test.go b/entity/dag/entity_actions_test.go index 78baf41fee70697f144b30a64579b6bc7d2917a0..79afe525c07e755b57420a5cfc554d27bc10cc56 100644 --- a/entity/dag/entity_actions_test.go +++ b/entity/dag/entity_actions_test.go @@ -385,3 +385,28 @@ func TestMerge(t *testing.T) { // fast-forward assertEqualRefs(t, repoA, repoB, "refs/"+def.namespace) } + +func TestRemove(t *testing.T) { + repoA, repoB, remote, id1, _, def := makeTestContextRemote(t) + defer repository.CleanupTestRepos(repoA, repoB, remote) + + e := New(def) + e.Append(newOp1(id1, "foo")) + require.NoError(t, e.Commit(repoA)) + + _, err := Push(def, repoA, "remote") + require.NoError(t, err) + + err = Remove(def, repoA, e.Id()) + require.NoError(t, err) + + _, err = Read(def, repoA, e.Id()) + require.Error(t, err) + + _, err = readRemote(def, repoA, "remote", e.Id()) + require.Error(t, err) + + // Remove is idempotent + err = Remove(def, repoA, e.Id()) + require.NoError(t, err) +} diff --git a/repository/repo.go b/repository/repo.go index 8d162c6d9dbb1249c9ad53f94998d3acba140dd0..80bb7ce7e5fc65ad35036d2e9de10bddc13998ac 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -147,6 +147,7 @@ type RepoData interface { UpdateRef(ref string, hash Hash) error // RemoveRef will remove a Git reference + // RemoveRef is idempotent. RemoveRef(ref string) error // ListRefs will return a list of Git ref matching the given refspec diff --git a/repository/repo_testing.go b/repository/repo_testing.go index cdcb9008067b41bd1070c0502e97612cb70201a9..1f80d89871a1dae9419fa7cd278f14522454a1db 100644 --- a/repository/repo_testing.go +++ b/repository/repo_testing.go @@ -202,6 +202,10 @@ func RepoDataTest(t *testing.T, repo RepoData) { err = repo.RemoveRef("refs/bugs/ref1") require.NoError(t, err) + + // RemoveRef is idempotent + err = repo.RemoveRef("refs/bugs/ref1") + require.NoError(t, err) } func RepoDataSignatureTest(t *testing.T, repo RepoData) { From 59e9981161acea461a3ef9d386f20e23e78d8433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 11 Feb 2021 09:51:32 +0100 Subject: [PATCH 29/42] entity: expose create and edit lamport clocks --- entity/dag/entity.go | 51 +++++++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/entity/dag/entity.go b/entity/dag/entity.go index 3f4dfcb4b1893972ae524c428274c4afacb6a831..d92b386e117d7d3b9528e3105d7649918ce79419 100644 --- a/entity/dag/entity.go +++ b/entity/dag/entity.go @@ -35,6 +35,12 @@ type Definition struct { // Entity is a data structure stored in a chain of git objects, supporting actions like Push, Pull and Merge. type Entity struct { + // A Lamport clock is a logical clock that allow to order event + // inside a distributed system. + // It must be the first field in this struct due to https://github.com/golang/go/issues/36606 + createTime lamport.Time + editTime lamport.Time + Definition // operations that are already stored in the repository @@ -42,8 +48,6 @@ type Entity struct { // operations not yet stored in the repository staging []Operation - // TODO: add here createTime and editTime - lastCommit repository.Hash } @@ -210,16 +214,26 @@ func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, err // Now that we ordered the operationPacks, we have the order of the Operations ops := make([]Operation, 0, opsCount) + var createTime lamport.Time + var editTime lamport.Time for _, pack := range oppSlice { for _, operation := range pack.Operations { ops = append(ops, operation) } + if pack.CreateTime > createTime { + createTime = pack.CreateTime + } + if pack.EditTime > editTime { + editTime = pack.EditTime + } } return &Entity{ Definition: def, ops: ops, lastCommit: rootHash, + createTime: createTime, + editTime: editTime, }, nil } @@ -349,7 +363,8 @@ func (e *Entity) Commit(repo repository.ClockedRepo) error { return fmt.Errorf("can't commit an entity with no pending operation") } - if err := e.Validate(); err != nil { + err := e.Validate() + if err != nil { return errors.Wrapf(err, "can't commit a %s with invalid data", e.Definition.typename) } @@ -361,23 +376,23 @@ func (e *Entity) Commit(repo repository.ClockedRepo) error { author = op.Author() } - editTime, err := repo.Increment(fmt.Sprintf(editClockPattern, e.namespace)) + e.editTime, err = repo.Increment(fmt.Sprintf(editClockPattern, e.namespace)) if err != nil { return err } - var creationTime lamport.Time - if e.lastCommit == "" { - creationTime, err = repo.Increment(fmt.Sprintf(creationClockPattern, e.namespace)) - if err != nil { - return err - } - } opp := &operationPack{ Author: author, Operations: e.staging, - CreateTime: creationTime, - EditTime: editTime, + EditTime: e.editTime, + } + + if e.lastCommit == "" { + e.createTime, err = repo.Increment(fmt.Sprintf(creationClockPattern, e.namespace)) + if err != nil { + return err + } + opp.CreateTime = e.createTime } var commitHash repository.Hash @@ -401,3 +416,13 @@ func (e *Entity) Commit(repo repository.ClockedRepo) error { ref := fmt.Sprintf(refsPattern, e.namespace, e.Id().String()) return repo.UpdateRef(ref, commitHash) } + +// CreateLamportTime return the Lamport time of creation +func (e *Entity) CreateLamportTime() lamport.Time { + return e.createTime +} + +// EditLamportTime return the Lamport time of the last edition +func (e *Entity) EditLamportTime() lamport.Time { + return e.editTime +} From 71e22d9f6e49ce0c3bc3b177323b17652a1c45a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 11 Feb 2021 09:52:09 +0100 Subject: [PATCH 30/42] entity: clock loader --- entity/dag/clock.go | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 entity/dag/clock.go diff --git a/entity/dag/clock.go b/entity/dag/clock.go new file mode 100644 index 0000000000000000000000000000000000000000..fa944b33ea0c82d6df635d476d88523bcf85a9a0 --- /dev/null +++ b/entity/dag/clock.go @@ -0,0 +1,41 @@ +package dag + +import ( + "fmt" + + "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/repository" +) + +// ClockLoader is the repository.ClockLoader for Entity +func ClockLoader(defs ...Definition) repository.ClockLoader { + clocks := make([]string, len(defs)*2) + for _, def := range defs { + clocks = append(clocks, fmt.Sprintf(creationClockPattern, def.namespace)) + clocks = append(clocks, fmt.Sprintf(editClockPattern, def.namespace)) + } + + return repository.ClockLoader{ + Clocks: clocks, + Witnesser: func(repo repository.ClockedRepo) error { + // We don't care about the actual identity so an IdentityStub will do + resolver := identity.NewStubResolver() + + for _, def := range defs { + // override the resolver + def := def + def.identityResolver = resolver + + // we actually just need to read all entities, + // as that will create and update the clocks + // TODO: concurrent loading to be faster? + for b := range ReadAll(def, repo) { + if b.Err != nil { + return b.Err + } + } + } + return nil + }, + } +} From 94f06cd54defa73f5e8b79345597279e454c78e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 14 Feb 2021 10:02:01 +0100 Subject: [PATCH 31/42] entity: pass the identity resolver instead of defining it once Having the resolver in Definition doesn't actually work well as the resolver is very situational. --- entity/dag/clock.go | 6 +----- entity/dag/common_test.go | 17 +++++++-------- entity/dag/entity.go | 18 +++++++--------- entity/dag/entity_actions.go | 17 +++++++++------ entity/dag/entity_actions_test.go | 36 +++++++++++++++---------------- entity/dag/operation_pack.go | 8 +++---- entity/dag/operation_pack_test.go | 8 +++---- 7 files changed, 53 insertions(+), 57 deletions(-) diff --git a/entity/dag/clock.go b/entity/dag/clock.go index fa944b33ea0c82d6df635d476d88523bcf85a9a0..c9d2b94b0ea4648f800855b233d8360197fe6a08 100644 --- a/entity/dag/clock.go +++ b/entity/dag/clock.go @@ -22,14 +22,10 @@ func ClockLoader(defs ...Definition) repository.ClockLoader { resolver := identity.NewStubResolver() for _, def := range defs { - // override the resolver - def := def - def.identityResolver = resolver - // we actually just need to read all entities, // as that will create and update the clocks // TODO: concurrent loading to be faster? - for b := range ReadAll(def, repo) { + for b := range ReadAll(def, repo, resolver) { if b.Err != nil { return b.Err } diff --git a/entity/dag/common_test.go b/entity/dag/common_test.go index 05d8589807ba3e4103bd2c3621491a9b08c2ec81..0ddbca478e79de2ff6ab7ecb67eb3d350e1a07f4 100644 --- a/entity/dag/common_test.go +++ b/entity/dag/common_test.go @@ -91,13 +91,13 @@ func unmarshaler(author identity.Interface, raw json.RawMessage) (Operation, err Identities + repo + definition */ -func makeTestContext() (repository.ClockedRepo, identity.Interface, identity.Interface, Definition) { +func makeTestContext() (repository.ClockedRepo, identity.Interface, identity.Interface, identity.Resolver, Definition) { repo := repository.NewMockRepo() - id1, id2, def := makeTestContextInternal(repo) - return repo, id1, id2, def + id1, id2, resolver, def := makeTestContextInternal(repo) + return repo, id1, id2, resolver, def } -func makeTestContextRemote(t *testing.T) (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, identity.Resolver, Definition) { repoA := repository.CreateGoGitTestRepo(false) repoB := repository.CreateGoGitTestRepo(false) remote := repository.CreateGoGitTestRepo(true) @@ -111,7 +111,7 @@ func makeTestContextRemote(t *testing.T) (repository.ClockedRepo, repository.Clo err = repoB.AddRemote("repoA", repoA.GetLocalRemote()) require.NoError(t, err) - id1, id2, def := makeTestContextInternal(repoA) + id1, id2, resolver, def := makeTestContextInternal(repoA) // distribute the identities _, err = identity.Push(repoA, "remote") @@ -119,10 +119,10 @@ func makeTestContextRemote(t *testing.T) (repository.ClockedRepo, repository.Clo err = identity.Pull(repoB, "remote") require.NoError(t, err) - return repoA, repoB, remote, id1, id2, def + return repoA, repoB, remote, id1, id2, resolver, def } -func makeTestContextInternal(repo repository.ClockedRepo) (identity.Interface, identity.Interface, Definition) { +func makeTestContextInternal(repo repository.ClockedRepo) (identity.Interface, identity.Interface, identity.Resolver, Definition) { id1, err := identity.NewIdentity(repo, "name1", "email1") if err != nil { panic(err) @@ -155,11 +155,10 @@ func makeTestContextInternal(repo repository.ClockedRepo) (identity.Interface, i typename: "foo", namespace: "foos", operationUnmarshaler: unmarshaler, - identityResolver: resolver, formatVersion: 1, } - return id1, id2, def + return id1, id2, resolver, def } type identityResolverFunc func(id entity.Id) (identity.Interface, error) diff --git a/entity/dag/entity.go b/entity/dag/entity.go index d92b386e117d7d3b9528e3105d7649918ce79419..09576d28e1eb1e0e241fb7a898c1ba1a773c8303 100644 --- a/entity/dag/entity.go +++ b/entity/dag/entity.go @@ -27,8 +27,6 @@ type Definition struct { namespace string // a function decoding a JSON message into an Operation operationUnmarshaler func(author identity.Interface, raw json.RawMessage) (Operation, error) - // a function loading an identity.Identity from its Id - identityResolver identity.Resolver // the expected format version number, that can be used for data migration/upgrade formatVersion uint } @@ -59,29 +57,29 @@ func New(definition Definition) *Entity { } // Read will read and decode a stored local Entity from a repository -func Read(def Definition, repo repository.ClockedRepo, id entity.Id) (*Entity, error) { +func Read(def Definition, repo repository.ClockedRepo, resolver identity.Resolver, id entity.Id) (*Entity, error) { if err := id.Validate(); err != nil { return nil, errors.Wrap(err, "invalid id") } ref := fmt.Sprintf("refs/%s/%s", def.namespace, id.String()) - return read(def, repo, ref) + return read(def, repo, resolver, ref) } // readRemote will read and decode a stored remote Entity from a repository -func readRemote(def Definition, repo repository.ClockedRepo, remote string, id entity.Id) (*Entity, error) { +func readRemote(def Definition, repo repository.ClockedRepo, resolver identity.Resolver, remote string, id entity.Id) (*Entity, error) { if err := id.Validate(); err != nil { return nil, errors.Wrap(err, "invalid id") } ref := fmt.Sprintf("refs/remotes/%s/%s/%s", def.namespace, remote, id.String()) - return read(def, repo, ref) + return read(def, repo, resolver, ref) } // read fetch from git and decode an Entity at an arbitrary git reference. -func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, error) { +func read(def Definition, repo repository.ClockedRepo, resolver identity.Resolver, ref string) (*Entity, error) { rootHash, err := repo.ResolveRef(ref) if err != nil { return nil, err @@ -140,7 +138,7 @@ func read(def Definition, repo repository.ClockedRepo, ref string) (*Entity, err return nil, fmt.Errorf("multiple leafs in the entity DAG") } - opp, err := readOperationPack(def, repo, commit) + opp, err := readOperationPack(def, repo, resolver, commit) if err != nil { return nil, err } @@ -243,7 +241,7 @@ type StreamedEntity struct { } // ReadAll read and parse all local Entity -func ReadAll(def Definition, repo repository.ClockedRepo) <-chan StreamedEntity { +func ReadAll(def Definition, repo repository.ClockedRepo, resolver identity.Resolver) <-chan StreamedEntity { out := make(chan StreamedEntity) go func() { @@ -258,7 +256,7 @@ func ReadAll(def Definition, repo repository.ClockedRepo) <-chan StreamedEntity } for _, ref := range refs { - e, err := read(def, repo, ref) + e, err := read(def, repo, resolver, ref) if err != nil { out <- StreamedEntity{Err: err} diff --git a/entity/dag/entity_actions.go b/entity/dag/entity_actions.go index fa50473cde906df035a3d8aa1080b76d55762369..707c93aa7107ab8c97e73ff9bac2f61818fba273 100644 --- a/entity/dag/entity_actions.go +++ b/entity/dag/entity_actions.go @@ -32,13 +32,13 @@ func Push(def Definition, repo repository.Repo, remote string) (string, error) { // Pull will do a Fetch + MergeAll // Contrary to MergeAll, this function will return an error if a merge fail. -func Pull(def Definition, repo repository.ClockedRepo, remote string, author identity.Interface) error { +func Pull(def Definition, repo repository.ClockedRepo, resolver identity.Resolver, remote string, author identity.Interface) error { _, err := Fetch(def, repo, remote) if err != nil { return err } - for merge := range MergeAll(def, repo, remote, author) { + for merge := range MergeAll(def, repo, resolver, remote, author) { if merge.Err != nil { return merge.Err } @@ -65,7 +65,10 @@ func Pull(def Definition, repo repository.ClockedRepo, remote string, author ide // 5. if both local and remote Entity have new commits (that is, we have a concurrent edition), // a merge commit with an empty operationPack is created to join both branch and form a DAG. // --> emit entity.MergeStatusUpdated -func MergeAll(def Definition, repo repository.ClockedRepo, remote string, author identity.Interface) <-chan entity.MergeResult { +// +// Note: an author is necessary for the case where a merge commit is created, as this commit will +// have an author and may be signed if a signing key is available. +func MergeAll(def Definition, repo repository.ClockedRepo, resolver identity.Resolver, remote string, author identity.Interface) <-chan entity.MergeResult { out := make(chan entity.MergeResult) go func() { @@ -79,7 +82,7 @@ func MergeAll(def Definition, repo repository.ClockedRepo, remote string, author } for _, remoteRef := range remoteRefs { - out <- merge(def, repo, remoteRef, author) + out <- merge(def, repo, resolver, remoteRef, author) } }() @@ -88,14 +91,14 @@ func MergeAll(def Definition, repo repository.ClockedRepo, remote string, author // merge perform a merge to make sure a local Entity is up to date. // See MergeAll for more details. -func merge(def Definition, repo repository.ClockedRepo, remoteRef string, author identity.Interface) entity.MergeResult { +func merge(def Definition, repo repository.ClockedRepo, resolver identity.Resolver, remoteRef string, author identity.Interface) entity.MergeResult { id := entity.RefToId(remoteRef) if err := id.Validate(); err != nil { return entity.NewMergeInvalidStatus(id, errors.Wrap(err, "invalid ref").Error()) } - remoteEntity, err := read(def, repo, remoteRef) + remoteEntity, err := read(def, repo, resolver, remoteRef) if err != nil { return entity.NewMergeInvalidStatus(id, errors.Wrapf(err, "remote %s is not readable", def.typename).Error()) @@ -194,7 +197,7 @@ func merge(def Definition, repo repository.ClockedRepo, remoteRef string, author // an empty operationPack. // First step is to collect those clocks. - localEntity, err := read(def, repo, localRef) + localEntity, err := read(def, repo, resolver, localRef) if err != nil { return entity.NewMergeError(err, id) } diff --git a/entity/dag/entity_actions_test.go b/entity/dag/entity_actions_test.go index 79afe525c07e755b57420a5cfc554d27bc10cc56..848d64688f9ea33148e756fbf1ead7415c4cf92d 100644 --- a/entity/dag/entity_actions_test.go +++ b/entity/dag/entity_actions_test.go @@ -24,7 +24,7 @@ func allEntities(t testing.TB, bugs <-chan StreamedEntity) []*Entity { } func TestPushPull(t *testing.T) { - repoA, repoB, remote, id1, id2, def := makeTestContextRemote(t) + repoA, repoB, remote, id1, id2, resolver, def := makeTestContextRemote(t) defer repository.CleanupTestRepos(repoA, repoB, remote) // A --> remote --> B @@ -37,10 +37,10 @@ func TestPushPull(t *testing.T) { _, err = Push(def, repoA, "remote") require.NoError(t, err) - err = Pull(def, repoB, "remote", id1) + err = Pull(def, repoB, resolver, "remote", id1) require.NoError(t, err) - entities := allEntities(t, ReadAll(def, repoB)) + entities := allEntities(t, ReadAll(def, repoB, resolver)) require.Len(t, entities, 1) // B --> remote --> A @@ -53,15 +53,15 @@ func TestPushPull(t *testing.T) { _, err = Push(def, repoB, "remote") require.NoError(t, err) - err = Pull(def, repoA, "remote", id1) + err = Pull(def, repoA, resolver, "remote", id1) require.NoError(t, err) - entities = allEntities(t, ReadAll(def, repoB)) + entities = allEntities(t, ReadAll(def, repoB, resolver)) require.Len(t, entities, 2) } func TestListLocalIds(t *testing.T) { - repoA, repoB, remote, id1, id2, def := makeTestContextRemote(t) + repoA, repoB, remote, id1, id2, resolver, def := makeTestContextRemote(t) defer repository.CleanupTestRepos(repoA, repoB, remote) // A --> remote --> B @@ -87,7 +87,7 @@ func TestListLocalIds(t *testing.T) { listLocalIds(t, def, repoA, 2) listLocalIds(t, def, repoB, 0) - err = Pull(def, repoB, "remote", id1) + err = Pull(def, repoB, resolver, "remote", id1) require.NoError(t, err) listLocalIds(t, def, repoA, 2) @@ -206,7 +206,7 @@ func assertNotEqualRefs(t *testing.T, repoA, repoB repository.RepoData, prefix s } func TestMerge(t *testing.T) { - repoA, repoB, remote, id1, id2, def := makeTestContextRemote(t) + repoA, repoB, remote, id1, id2, resolver, def := makeTestContextRemote(t) defer repository.CleanupTestRepos(repoA, repoB, remote) // SCENARIO 1 @@ -231,7 +231,7 @@ func TestMerge(t *testing.T) { _, err = Fetch(def, repoB, "remote") require.NoError(t, err) - results := MergeAll(def, repoB, "remote", id1) + results := MergeAll(def, repoB, resolver, "remote", id1) assertMergeResults(t, []entity.MergeResult{ { @@ -249,7 +249,7 @@ func TestMerge(t *testing.T) { // SCENARIO 2 // if the remote and local Entity have the same state, nothing is changed - results = MergeAll(def, repoB, "remote", id1) + results = MergeAll(def, repoB, resolver, "remote", id1) assertMergeResults(t, []entity.MergeResult{ { @@ -275,7 +275,7 @@ func TestMerge(t *testing.T) { err = e2A.Commit(repoA) require.NoError(t, err) - results = MergeAll(def, repoA, "remote", id1) + results = MergeAll(def, repoA, resolver, "remote", id1) assertMergeResults(t, []entity.MergeResult{ { @@ -300,7 +300,7 @@ func TestMerge(t *testing.T) { _, err = Fetch(def, repoB, "remote") require.NoError(t, err) - results = MergeAll(def, repoB, "remote", id1) + results = MergeAll(def, repoB, resolver, "remote", id1) assertMergeResults(t, []entity.MergeResult{ { @@ -327,10 +327,10 @@ func TestMerge(t *testing.T) { err = e2A.Commit(repoA) require.NoError(t, err) - e1B, err := Read(def, repoB, e1A.Id()) + e1B, err := Read(def, repoB, resolver, e1A.Id()) require.NoError(t, err) - e2B, err := Read(def, repoB, e2A.Id()) + e2B, err := Read(def, repoB, resolver, e2A.Id()) require.NoError(t, err) e1B.Append(newOp1(id1, "barbarfoofoo")) @@ -347,7 +347,7 @@ func TestMerge(t *testing.T) { _, err = Fetch(def, repoB, "remote") require.NoError(t, err) - results = MergeAll(def, repoB, "remote", id1) + results = MergeAll(def, repoB, resolver, "remote", id1) assertMergeResults(t, []entity.MergeResult{ { @@ -387,7 +387,7 @@ func TestMerge(t *testing.T) { } func TestRemove(t *testing.T) { - repoA, repoB, remote, id1, _, def := makeTestContextRemote(t) + repoA, repoB, remote, id1, _, resolver, def := makeTestContextRemote(t) defer repository.CleanupTestRepos(repoA, repoB, remote) e := New(def) @@ -400,10 +400,10 @@ func TestRemove(t *testing.T) { err = Remove(def, repoA, e.Id()) require.NoError(t, err) - _, err = Read(def, repoA, e.Id()) + _, err = Read(def, repoA, resolver, e.Id()) require.Error(t, err) - _, err = readRemote(def, repoA, "remote", e.Id()) + _, err = readRemote(def, repoA, resolver, "remote", e.Id()) require.Error(t, err) // Remove is idempotent diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go index 959b1ae05ef27a63cafc0282ebe42401e22ca805..d6bce9f2a5bd6b9e404308575dc466135dcbeca1 100644 --- a/entity/dag/operation_pack.go +++ b/entity/dag/operation_pack.go @@ -166,7 +166,7 @@ func (opp *operationPack) Write(def Definition, repo repository.Repo, parentComm // readOperationPack read the operationPack encoded in git at the given Tree hash. // // Validity of the Lamport clocks is left for the caller to decide. -func readOperationPack(def Definition, repo repository.RepoData, commit repository.Commit) (*operationPack, error) { +func readOperationPack(def Definition, repo repository.RepoData, resolver identity.Resolver, commit repository.Commit) (*operationPack, error) { entries, err := repo.ReadTree(commit.TreeHash) if err != nil { return nil, err @@ -207,7 +207,7 @@ func readOperationPack(def Definition, repo repository.RepoData, commit reposito if err != nil { return nil, errors.Wrap(err, "failed to read git blob data") } - ops, author, err = unmarshallPack(def, data) + ops, author, err = unmarshallPack(def, resolver, data) if err != nil { return nil, err } @@ -251,7 +251,7 @@ func readOperationPack(def Definition, repo repository.RepoData, commit reposito // unmarshallPack delegate the unmarshalling of the Operation's JSON to the decoding // function provided by the concrete entity. This gives access to the concrete type of each // Operation. -func unmarshallPack(def Definition, data []byte) ([]Operation, identity.Interface, error) { +func unmarshallPack(def Definition, resolver identity.Resolver, data []byte) ([]Operation, identity.Interface, error) { aux := struct { Author identity.IdentityStub `json:"author"` Operations []json.RawMessage `json:"ops"` @@ -265,7 +265,7 @@ func unmarshallPack(def Definition, data []byte) ([]Operation, identity.Interfac return nil, nil, fmt.Errorf("missing author") } - author, err := def.identityResolver.ResolveIdentity(aux.Author.Id()) + author, err := resolver.ResolveIdentity(aux.Author.Id()) if err != nil { return nil, nil, err } diff --git a/entity/dag/operation_pack_test.go b/entity/dag/operation_pack_test.go index ac9797765a1fb442bea45ccdf79db7ce7dcd71a2..a12382afe021adda78affc9d53139d76ead70b25 100644 --- a/entity/dag/operation_pack_test.go +++ b/entity/dag/operation_pack_test.go @@ -9,7 +9,7 @@ import ( ) func TestOperationPackReadWrite(t *testing.T) { - repo, id1, _, def := makeTestContext() + repo, id1, _, resolver, def := makeTestContext() opp := &operationPack{ Author: id1, @@ -27,7 +27,7 @@ func TestOperationPackReadWrite(t *testing.T) { commit, err := repo.ReadCommit(commitHash) require.NoError(t, err) - opp2, err := readOperationPack(def, repo, commit) + opp2, err := readOperationPack(def, repo, resolver, commit) require.NoError(t, err) require.Equal(t, opp, opp2) @@ -46,7 +46,7 @@ func TestOperationPackReadWrite(t *testing.T) { } func TestOperationPackSignedReadWrite(t *testing.T) { - repo, id1, _, def := makeTestContext() + repo, id1, _, resolver, def := makeTestContext() err := id1.(*identity.Identity).Mutate(repo, func(orig *identity.Mutator) { orig.Keys = append(orig.Keys, identity.GenerateKey()) @@ -69,7 +69,7 @@ func TestOperationPackSignedReadWrite(t *testing.T) { commit, err := repo.ReadCommit(commitHash) require.NoError(t, err) - opp2, err := readOperationPack(def, repo, commit) + opp2, err := readOperationPack(def, repo, resolver, commit) require.NoError(t, err) require.Equal(t, opp, opp2) From 99b9dd84cb4b0cfd3eb1fd50b07c8b826eb52d19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 14 Feb 2021 10:06:16 +0100 Subject: [PATCH 32/42] entity: support different author in staging operations --- entity/dag/clock.go | 4 +- entity/dag/common_test.go | 8 +-- entity/dag/entity.go | 99 +++++++++++++++++-------------- entity/dag/entity_actions.go | 20 +++---- entity/dag/entity_actions_test.go | 14 ++--- entity/dag/entity_test.go | 47 +++++++++------ entity/dag/operation.go | 8 --- entity/dag/operation_pack.go | 14 ++--- 8 files changed, 113 insertions(+), 101 deletions(-) diff --git a/entity/dag/clock.go b/entity/dag/clock.go index c9d2b94b0ea4648f800855b233d8360197fe6a08..dc9bb72dab386180cb94b67e0893df68acf2d74b 100644 --- a/entity/dag/clock.go +++ b/entity/dag/clock.go @@ -11,8 +11,8 @@ import ( func ClockLoader(defs ...Definition) repository.ClockLoader { clocks := make([]string, len(defs)*2) for _, def := range defs { - clocks = append(clocks, fmt.Sprintf(creationClockPattern, def.namespace)) - clocks = append(clocks, fmt.Sprintf(editClockPattern, def.namespace)) + clocks = append(clocks, fmt.Sprintf(creationClockPattern, def.Namespace)) + clocks = append(clocks, fmt.Sprintf(editClockPattern, def.Namespace)) } return repository.ClockLoader{ diff --git a/entity/dag/common_test.go b/entity/dag/common_test.go index 0ddbca478e79de2ff6ab7ecb67eb3d350e1a07f4..fa15cd1f41bd81a8e22ff6fa22db06f2fa5fdac8 100644 --- a/entity/dag/common_test.go +++ b/entity/dag/common_test.go @@ -152,10 +152,10 @@ func makeTestContextInternal(repo repository.ClockedRepo) (identity.Interface, i }) def := Definition{ - typename: "foo", - namespace: "foos", - operationUnmarshaler: unmarshaler, - formatVersion: 1, + Typename: "foo", + Namespace: "foos", + OperationUnmarshaler: unmarshaler, + FormatVersion: 1, } return id1, id2, resolver, def diff --git a/entity/dag/entity.go b/entity/dag/entity.go index 09576d28e1eb1e0e241fb7a898c1ba1a773c8303..196280a8b287c57da6defcd247246166e6ce7b8a 100644 --- a/entity/dag/entity.go +++ b/entity/dag/entity.go @@ -22,13 +22,13 @@ const editClockPattern = "%s-edit" // Definition hold the details defining one specialization of an Entity. type Definition struct { // the name of the entity (bug, pull-request, ...) - typename string - // the namespace in git (bugs, prs, ...) - namespace string + Typename string + // the Namespace in git (bugs, prs, ...) + Namespace string // a function decoding a JSON message into an Operation - operationUnmarshaler func(author identity.Interface, raw json.RawMessage) (Operation, error) + OperationUnmarshaler func(author identity.Interface, raw json.RawMessage) (Operation, error) // the expected format version number, that can be used for data migration/upgrade - formatVersion uint + FormatVersion uint } // Entity is a data structure stored in a chain of git objects, supporting actions like Push, Pull and Merge. @@ -62,7 +62,7 @@ func Read(def Definition, repo repository.ClockedRepo, resolver identity.Resolve return nil, errors.Wrap(err, "invalid id") } - ref := fmt.Sprintf("refs/%s/%s", def.namespace, id.String()) + ref := fmt.Sprintf("refs/%s/%s", def.Namespace, id.String()) return read(def, repo, resolver, ref) } @@ -73,7 +73,7 @@ func readRemote(def Definition, repo repository.ClockedRepo, resolver identity.R return nil, errors.Wrap(err, "invalid id") } - ref := fmt.Sprintf("refs/remotes/%s/%s/%s", def.namespace, remote, id.String()) + ref := fmt.Sprintf("refs/remotes/%s/%s/%s", def.Namespace, remote, id.String()) return read(def, repo, resolver, ref) } @@ -179,11 +179,11 @@ func read(def Definition, repo repository.ClockedRepo, resolver identity.Resolve // The clocks are fine, we witness them for _, opp := range oppMap { - err = repo.Witness(fmt.Sprintf(creationClockPattern, def.namespace), opp.CreateTime) + err = repo.Witness(fmt.Sprintf(creationClockPattern, def.Namespace), opp.CreateTime) if err != nil { return nil, err } - err = repo.Witness(fmt.Sprintf(editClockPattern, def.namespace), opp.EditTime) + err = repo.Witness(fmt.Sprintf(editClockPattern, def.Namespace), opp.EditTime) if err != nil { return nil, err } @@ -247,7 +247,7 @@ func ReadAll(def Definition, repo repository.ClockedRepo, resolver identity.Reso go func() { defer close(out) - refPrefix := fmt.Sprintf("refs/%s/", def.namespace) + refPrefix := fmt.Sprintf("refs/%s/", def.Namespace) refs, err := repo.ListRefs(refPrefix) if err != nil { @@ -346,9 +346,9 @@ func (e *Entity) NeedCommit() bool { return len(e.staging) > 0 } -// CommitAdNeeded execute a Commit only if necessary. This function is useful to avoid getting an error if the Entity +// CommitAsNeeded execute a Commit only if necessary. This function is useful to avoid getting an error if the Entity // is already in sync with the repository. -func (e *Entity) CommitAdNeeded(repo repository.ClockedRepo) error { +func (e *Entity) CommitAsNeeded(repo repository.ClockedRepo) error { if e.NeedCommit() { return e.Commit(repo) } @@ -363,56 +363,65 @@ func (e *Entity) Commit(repo repository.ClockedRepo) error { err := e.Validate() if err != nil { - return errors.Wrapf(err, "can't commit a %s with invalid data", e.Definition.typename) + return errors.Wrapf(err, "can't commit a %s with invalid data", e.Definition.Typename) } - var author identity.Interface - for _, op := range e.staging { - if author != nil && op.Author() != author { - return fmt.Errorf("operations with different author") + for len(e.staging) > 0 { + var author identity.Interface + var toCommit []Operation + + // Split into chunks with the same author + for len(e.staging) > 0 { + op := e.staging[0] + if author != nil && op.Author().Id() != author.Id() { + break + } + author = e.staging[0].Author() + toCommit = append(toCommit, op) + e.staging = e.staging[1:] } - author = op.Author() - } - e.editTime, err = repo.Increment(fmt.Sprintf(editClockPattern, e.namespace)) - if err != nil { - return err - } + e.editTime, err = repo.Increment(fmt.Sprintf(editClockPattern, e.Namespace)) + if err != nil { + return err + } - opp := &operationPack{ - Author: author, - Operations: e.staging, - EditTime: e.editTime, - } + opp := &operationPack{ + Author: author, + Operations: toCommit, + EditTime: e.editTime, + } - if e.lastCommit == "" { - e.createTime, err = repo.Increment(fmt.Sprintf(creationClockPattern, e.namespace)) + if e.lastCommit == "" { + e.createTime, err = repo.Increment(fmt.Sprintf(creationClockPattern, e.Namespace)) + if err != nil { + return err + } + opp.CreateTime = e.createTime + } + + var parentCommit []repository.Hash + if e.lastCommit != "" { + parentCommit = []repository.Hash{e.lastCommit} + } + + commitHash, err := opp.Write(e.Definition, repo, parentCommit...) if err != nil { return err } - opp.CreateTime = e.createTime - } - var commitHash repository.Hash - if e.lastCommit == "" { - commitHash, err = opp.Write(e.Definition, repo) - } else { - commitHash, err = opp.Write(e.Definition, repo, e.lastCommit) - } - - if err != nil { - return err + e.lastCommit = commitHash + e.ops = append(e.ops, toCommit...) } - e.lastCommit = commitHash - e.ops = append(e.ops, e.staging...) + // not strictly necessary but make equality testing easier in tests e.staging = nil // Create or update the Git reference for this entity // When pushing later, the remote will ensure that this ref update // is fast-forward, that is no data has been overwritten. - ref := fmt.Sprintf(refsPattern, e.namespace, e.Id().String()) - return repo.UpdateRef(ref, commitHash) + ref := fmt.Sprintf(refsPattern, e.Namespace, e.Id().String()) + return repo.UpdateRef(ref, e.lastCommit) } // CreateLamportTime return the Lamport time of creation diff --git a/entity/dag/entity_actions.go b/entity/dag/entity_actions.go index 707c93aa7107ab8c97e73ff9bac2f61818fba273..2926e992b2d6ade978bfe35b14071846f999c787 100644 --- a/entity/dag/entity_actions.go +++ b/entity/dag/entity_actions.go @@ -12,7 +12,7 @@ import ( // ListLocalIds list all the available local Entity's Id func ListLocalIds(def Definition, repo repository.RepoData) ([]entity.Id, error) { - refs, err := repo.ListRefs(fmt.Sprintf("refs/%s/", def.namespace)) + refs, err := repo.ListRefs(fmt.Sprintf("refs/%s/", def.Namespace)) if err != nil { return nil, err } @@ -22,12 +22,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) { - return repo.FetchRefs(remote, def.namespace) + 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) { - return repo.PushRefs(remote, def.namespace) + return repo.PushRefs(remote, def.Namespace) } // Pull will do a Fetch + MergeAll @@ -74,7 +74,7 @@ func MergeAll(def Definition, repo repository.ClockedRepo, resolver identity.Res go func() { defer close(out) - remoteRefSpec := fmt.Sprintf("refs/remotes/%s/%s/", remote, def.namespace) + remoteRefSpec := fmt.Sprintf("refs/remotes/%s/%s/", remote, def.Namespace) remoteRefs, err := repo.ListRefs(remoteRefSpec) if err != nil { out <- entity.MergeResult{Err: err} @@ -101,16 +101,16 @@ func merge(def Definition, repo repository.ClockedRepo, resolver identity.Resolv remoteEntity, err := read(def, repo, resolver, remoteRef) if err != nil { return entity.NewMergeInvalidStatus(id, - errors.Wrapf(err, "remote %s is not readable", def.typename).Error()) + errors.Wrapf(err, "remote %s is not readable", def.Typename).Error()) } // Check for error in remote data if err := remoteEntity.Validate(); err != nil { return entity.NewMergeInvalidStatus(id, - errors.Wrapf(err, "remote %s data is invalid", def.typename).Error()) + errors.Wrapf(err, "remote %s data is invalid", def.Typename).Error()) } - localRef := fmt.Sprintf("refs/%s/%s", def.namespace, id.String()) + localRef := fmt.Sprintf("refs/%s/%s", def.Namespace, id.String()) // SCENARIO 1 // if the remote Entity doesn't exist locally, it's created @@ -202,7 +202,7 @@ func merge(def Definition, repo repository.ClockedRepo, resolver identity.Resolv return entity.NewMergeError(err, id) } - editTime, err := repo.Increment(fmt.Sprintf(editClockPattern, def.namespace)) + editTime, err := repo.Increment(fmt.Sprintf(editClockPattern, def.Namespace)) if err != nil { return entity.NewMergeError(err, id) } @@ -236,7 +236,7 @@ func merge(def Definition, repo repository.ClockedRepo, resolver identity.Resolv func Remove(def Definition, repo repository.ClockedRepo, id entity.Id) error { var matches []string - ref := fmt.Sprintf("refs/%s/%s", def.namespace, id.String()) + ref := fmt.Sprintf("refs/%s/%s", def.Namespace, id.String()) matches = append(matches, ref) remotes, err := repo.GetRemotes() @@ -245,7 +245,7 @@ func Remove(def Definition, repo repository.ClockedRepo, id entity.Id) error { } for remote := range remotes { - ref = fmt.Sprintf("refs/remotes/%s/%s/%s", remote, def.namespace, id.String()) + ref = fmt.Sprintf("refs/remotes/%s/%s/%s", remote, def.Namespace, id.String()) matches = append(matches, ref) } diff --git a/entity/dag/entity_actions_test.go b/entity/dag/entity_actions_test.go index 848d64688f9ea33148e756fbf1ead7415c4cf92d..402f459cd14ce8ac5d15d71223e7d6a01997838b 100644 --- a/entity/dag/entity_actions_test.go +++ b/entity/dag/entity_actions_test.go @@ -244,7 +244,7 @@ func TestMerge(t *testing.T) { }, }, results) - assertEqualRefs(t, repoA, repoB, "refs/"+def.namespace) + assertEqualRefs(t, repoA, repoB, "refs/"+def.Namespace) // SCENARIO 2 // if the remote and local Entity have the same state, nothing is changed @@ -262,7 +262,7 @@ func TestMerge(t *testing.T) { }, }, results) - assertEqualRefs(t, repoA, repoB, "refs/"+def.namespace) + assertEqualRefs(t, repoA, repoB, "refs/"+def.Namespace) // SCENARIO 3 // if the local Entity has new commits but the remote don't, nothing is changed @@ -288,7 +288,7 @@ func TestMerge(t *testing.T) { }, }, results) - assertNotEqualRefs(t, repoA, repoB, "refs/"+def.namespace) + assertNotEqualRefs(t, repoA, repoB, "refs/"+def.Namespace) // SCENARIO 4 // if the remote has new commit, the local bug is updated to match the same history @@ -313,7 +313,7 @@ func TestMerge(t *testing.T) { }, }, results) - assertEqualRefs(t, repoA, repoB, "refs/"+def.namespace) + assertEqualRefs(t, repoA, repoB, "refs/"+def.Namespace) // SCENARIO 5 // if both local and remote Entity have new commits (that is, we have a concurrent edition), @@ -360,7 +360,7 @@ func TestMerge(t *testing.T) { }, }, results) - assertNotEqualRefs(t, repoA, repoB, "refs/"+def.namespace) + assertNotEqualRefs(t, repoA, repoB, "refs/"+def.Namespace) _, err = Push(def, repoB, "remote") require.NoError(t, err) @@ -368,7 +368,7 @@ func TestMerge(t *testing.T) { _, err = Fetch(def, repoA, "remote") require.NoError(t, err) - results = MergeAll(def, repoA, "remote", id1) + results = MergeAll(def, repoA, resolver, "remote", id1) assertMergeResults(t, []entity.MergeResult{ { @@ -383,7 +383,7 @@ func TestMerge(t *testing.T) { // make sure that the graphs become stable over multiple repo, due to the // fast-forward - assertEqualRefs(t, repoA, repoB, "refs/"+def.namespace) + assertEqualRefs(t, repoA, repoB, "refs/"+def.Namespace) } func TestRemove(t *testing.T) { diff --git a/entity/dag/entity_test.go b/entity/dag/entity_test.go index 012c87aaf72e3f691e8588292b72be34e9bd477d..6d621bbeb2c418a8996f71e973e2e65c75a6c79b 100644 --- a/entity/dag/entity_test.go +++ b/entity/dag/entity_test.go @@ -7,7 +7,7 @@ import ( ) func TestWriteRead(t *testing.T) { - repo, id1, id2, def := makeTestContext() + repo, id1, id2, resolver, def := makeTestContext() entity := New(def) require.False(t, entity.NeedCommit()) @@ -16,15 +16,34 @@ func TestWriteRead(t *testing.T) { entity.Append(newOp2(id1, "bar")) require.True(t, entity.NeedCommit()) - require.NoError(t, entity.CommitAdNeeded(repo)) + require.NoError(t, entity.CommitAsNeeded(repo)) require.False(t, entity.NeedCommit()) entity.Append(newOp2(id2, "foobar")) require.True(t, entity.NeedCommit()) - require.NoError(t, entity.CommitAdNeeded(repo)) + require.NoError(t, entity.CommitAsNeeded(repo)) require.False(t, entity.NeedCommit()) - read, err := Read(def, repo, entity.Id()) + read, err := Read(def, repo, resolver, entity.Id()) + require.NoError(t, err) + + assertEqualEntities(t, entity, read) +} + +func TestWriteReadMultipleAuthor(t *testing.T) { + repo, id1, id2, resolver, def := makeTestContext() + + entity := New(def) + + entity.Append(newOp1(id1, "foo")) + entity.Append(newOp2(id2, "bar")) + + require.NoError(t, entity.CommitAsNeeded(repo)) + + entity.Append(newOp2(id1, "foobar")) + require.NoError(t, entity.CommitAsNeeded(repo)) + + read, err := Read(def, repo, resolver, entity.Id()) require.NoError(t, err) assertEqualEntities(t, entity, read) @@ -34,23 +53,15 @@ func assertEqualEntities(t *testing.T, a, b *Entity) { // testify doesn't support comparing functions and systematically fail if they are not nil // so we have to set them to nil temporarily - backOpUnA := a.Definition.operationUnmarshaler - backOpUnB := b.Definition.operationUnmarshaler - - a.Definition.operationUnmarshaler = nil - b.Definition.operationUnmarshaler = nil - - backIdResA := a.Definition.identityResolver - backIdResB := b.Definition.identityResolver + backOpUnA := a.Definition.OperationUnmarshaler + backOpUnB := b.Definition.OperationUnmarshaler - a.Definition.identityResolver = nil - b.Definition.identityResolver = nil + a.Definition.OperationUnmarshaler = nil + b.Definition.OperationUnmarshaler = nil defer func() { - a.Definition.operationUnmarshaler = backOpUnA - b.Definition.operationUnmarshaler = backOpUnB - a.Definition.identityResolver = backIdResA - b.Definition.identityResolver = backIdResB + a.Definition.OperationUnmarshaler = backOpUnA + b.Definition.OperationUnmarshaler = backOpUnB }() require.Equal(t, a, b) diff --git a/entity/dag/operation.go b/entity/dag/operation.go index 86e2f7d7df6a35fb7049417919670a83387db77d..b0a78de6f7f6a438d447459db4c0d8d4b97fc8ca 100644 --- a/entity/dag/operation.go +++ b/entity/dag/operation.go @@ -23,11 +23,3 @@ type Operation interface { // Author returns the author of this operation Author() identity.Interface } - -// TODO: remove? -type operationBase struct { - author identity.Interface - - // Not serialized. Store the op's id in memory. - id entity.Id -} diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go index d6bce9f2a5bd6b9e404308575dc466135dcbeca1..00cf2557878609cfc9ed9e7b6779bdfa6267e034 100644 --- a/entity/dag/operation_pack.go +++ b/entity/dag/operation_pack.go @@ -72,7 +72,7 @@ func (opp *operationPack) Validate() error { return fmt.Errorf("missing author") } for _, op := range opp.Operations { - if op.Author() != opp.Author { + if op.Author().Id() != opp.Author.Id() { return fmt.Errorf("operation has different author than the operationPack's") } } @@ -120,7 +120,7 @@ func (opp *operationPack) Write(def Definition, repo repository.Repo, parentComm // - clocks tree := []repository.TreeEntry{ {ObjectType: repository.Blob, Hash: emptyBlobHash, - Name: fmt.Sprintf(versionEntryPrefix+"%d", def.formatVersion)}, + Name: fmt.Sprintf(versionEntryPrefix+"%d", def.FormatVersion)}, {ObjectType: repository.Blob, Hash: hash, Name: opsEntryName}, {ObjectType: repository.Blob, Hash: emptyBlobHash, @@ -188,10 +188,10 @@ func readOperationPack(def Definition, repo repository.RepoData, resolver identi } } if version == 0 { - return nil, entity.NewErrUnknowFormat(def.formatVersion) + return nil, entity.NewErrUnknowFormat(def.FormatVersion) } - if version != def.formatVersion { - return nil, entity.NewErrInvalidFormat(version, def.formatVersion) + if version != def.FormatVersion { + return nil, entity.NewErrInvalidFormat(version, def.FormatVersion) } var id entity.Id @@ -230,7 +230,7 @@ func readOperationPack(def Definition, repo repository.RepoData, resolver identi } // Verify signature if we expect one - keys := author.ValidKeysAtTime(fmt.Sprintf(editClockPattern, def.namespace), editTime) + keys := author.ValidKeysAtTime(fmt.Sprintf(editClockPattern, def.Namespace), editTime) if len(keys) > 0 { keyring := PGPKeyring(keys) _, err = openpgp.CheckDetachedSignature(keyring, commit.SignedData, commit.Signature) @@ -274,7 +274,7 @@ func unmarshallPack(def Definition, resolver identity.Resolver, data []byte) ([] for _, raw := range aux.Operations { // delegate to specialized unmarshal function - op, err := def.operationUnmarshaler(author, raw) + op, err := def.OperationUnmarshaler(author, raw) if err != nil { return nil, nil, err } From 3f6ef50883492f77995a7e27872d0b5ae17b9d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 14 Feb 2021 11:36:32 +0100 Subject: [PATCH 33/42] bug: migrate to the DAG entity structure! --- api/graphql/resolvers/operations.go | 12 +- api/graphql/resolvers/query.go | 13 - bridge/github/export.go | 2 +- bridge/github/import_test.go | 27 +- bridge/gitlab/export.go | 2 +- bridge/gitlab/import_test.go | 27 +- bridge/jira/export.go | 2 +- bug/bug.go | 594 ++++--------------------- bug/bug_actions.go | 106 +---- bug/bug_actions_test.go | 394 ---------------- bug/bug_test.go | 190 -------- bug/clocks.go | 40 -- bug/err.go | 17 + bug/git_tree.go | 84 ---- bug/identity.go | 27 -- bug/interface.go | 8 +- bug/op_add_comment.go | 14 +- bug/op_add_comment_test.go | 4 +- bug/op_create.go | 16 +- bug/op_create_test.go | 4 +- bug/op_edit_comment.go | 10 +- bug/op_edit_comment_test.go | 4 +- bug/op_label_change.go | 12 +- bug/op_label_change_test.go | 4 +- bug/op_noop.go | 8 +- bug/op_noop_test.go | 4 +- bug/op_set_metadata.go | 21 +- bug/op_set_metadata_test.go | 4 +- bug/op_set_status.go | 12 +- bug/op_set_status_test.go | 4 +- bug/op_set_title.go | 24 +- bug/op_set_title_test.go | 4 +- bug/operation.go | 152 +++++-- bug/operation_iterator.go | 72 --- bug/operation_iterator_test.go | 79 ---- bug/operation_pack.go | 187 -------- bug/operation_pack_test.go | 78 ---- bug/operation_test.go | 4 +- bug/sorting.go | 8 +- bug/with_snapshot.go | 6 - cache/bug_cache.go | 4 +- cache/bug_excerpt.go | 2 +- cache/repo_cache.go | 2 +- cache/repo_cache_bug.go | 2 +- cache/repo_cache_common.go | 15 +- cache/repo_cache_test.go | 4 + entity/TODO | 9 - entity/merge.go | 6 +- go.sum | 1 + misc/random_bugs/create_random_bugs.go | 46 -- tests/read_bugs_test.go | 4 +- 51 files changed, 335 insertions(+), 2040 deletions(-) delete mode 100644 bug/bug_actions_test.go delete mode 100644 bug/bug_test.go delete mode 100644 bug/clocks.go create mode 100644 bug/err.go delete mode 100644 bug/git_tree.go delete mode 100644 bug/identity.go delete mode 100644 bug/operation_iterator.go delete mode 100644 bug/operation_iterator_test.go delete mode 100644 bug/operation_pack.go delete mode 100644 bug/operation_pack_test.go delete mode 100644 entity/TODO diff --git a/api/graphql/resolvers/operations.go b/api/graphql/resolvers/operations.go index 8d3e5bba2783b41a3fe7c73959266a6e39c64beb..0ede9f1369b1011c0dae4dbd50b8ffded7f93977 100644 --- a/api/graphql/resolvers/operations.go +++ b/api/graphql/resolvers/operations.go @@ -19,7 +19,7 @@ func (createOperationResolver) ID(_ context.Context, obj *bug.CreateOperation) ( } func (createOperationResolver) Author(_ context.Context, obj *bug.CreateOperation) (models.IdentityWrapper, error) { - return models.NewLoadedIdentity(obj.Author), nil + return models.NewLoadedIdentity(obj.Author()), nil } func (createOperationResolver) Date(_ context.Context, obj *bug.CreateOperation) (*time.Time, error) { @@ -36,7 +36,7 @@ func (addCommentOperationResolver) ID(_ context.Context, obj *bug.AddCommentOper } func (addCommentOperationResolver) Author(_ context.Context, obj *bug.AddCommentOperation) (models.IdentityWrapper, error) { - return models.NewLoadedIdentity(obj.Author), nil + return models.NewLoadedIdentity(obj.Author()), nil } func (addCommentOperationResolver) Date(_ context.Context, obj *bug.AddCommentOperation) (*time.Time, error) { @@ -57,7 +57,7 @@ func (editCommentOperationResolver) Target(_ context.Context, obj *bug.EditComme } func (editCommentOperationResolver) Author(_ context.Context, obj *bug.EditCommentOperation) (models.IdentityWrapper, error) { - return models.NewLoadedIdentity(obj.Author), nil + return models.NewLoadedIdentity(obj.Author()), nil } func (editCommentOperationResolver) Date(_ context.Context, obj *bug.EditCommentOperation) (*time.Time, error) { @@ -74,7 +74,7 @@ func (labelChangeOperationResolver) ID(_ context.Context, obj *bug.LabelChangeOp } func (labelChangeOperationResolver) Author(_ context.Context, obj *bug.LabelChangeOperation) (models.IdentityWrapper, error) { - return models.NewLoadedIdentity(obj.Author), nil + return models.NewLoadedIdentity(obj.Author()), nil } func (labelChangeOperationResolver) Date(_ context.Context, obj *bug.LabelChangeOperation) (*time.Time, error) { @@ -91,7 +91,7 @@ func (setStatusOperationResolver) ID(_ context.Context, obj *bug.SetStatusOperat } func (setStatusOperationResolver) Author(_ context.Context, obj *bug.SetStatusOperation) (models.IdentityWrapper, error) { - return models.NewLoadedIdentity(obj.Author), nil + return models.NewLoadedIdentity(obj.Author()), nil } func (setStatusOperationResolver) Date(_ context.Context, obj *bug.SetStatusOperation) (*time.Time, error) { @@ -112,7 +112,7 @@ func (setTitleOperationResolver) ID(_ context.Context, obj *bug.SetTitleOperatio } func (setTitleOperationResolver) Author(_ context.Context, obj *bug.SetTitleOperation) (models.IdentityWrapper, error) { - return models.NewLoadedIdentity(obj.Author), nil + return models.NewLoadedIdentity(obj.Author()), nil } func (setTitleOperationResolver) Date(_ context.Context, obj *bug.SetTitleOperation) (*time.Time, error) { diff --git a/api/graphql/resolvers/query.go b/api/graphql/resolvers/query.go index 4ad7ae0c9104cb47401b8ef42fe37f3c54fcf966..b20035555a12054be441fe848bafdaff45e2fd6f 100644 --- a/api/graphql/resolvers/query.go +++ b/api/graphql/resolvers/query.go @@ -14,19 +14,6 @@ type rootQueryResolver struct { cache *cache.MultiRepoCache } -func (r rootQueryResolver) DefaultRepository(_ context.Context) (*models.Repository, error) { - repo, err := r.cache.DefaultRepo() - - if err != nil { - return nil, err - } - - return &models.Repository{ - Cache: r.cache, - Repo: repo, - }, nil -} - func (r rootQueryResolver) Repository(_ context.Context, ref *string) (*models.Repository, error) { var repo *cache.RepoCache var err error diff --git a/bridge/github/export.go b/bridge/github/export.go index 57b52ee06804cf65cda5e81140b905aa984728b1..1a59fbb3d96244804bb19da11ae370fe5de15bb4 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -294,7 +294,7 @@ func (ge *githubExporter) exportBug(ctx context.Context, b *cache.BugCache, out continue } - opAuthor := op.GetAuthor() + opAuthor := op.Author() client, err := ge.getClientForIdentity(opAuthor.Id()) if err != nil { continue diff --git a/bridge/github/import_test.go b/bridge/github/import_test.go index 84bf774e50d335d77eab6992aac8d412ab0bf0d3..324d54211538935236b2db6beb3deb9d65c6b71e 100644 --- a/bridge/github/import_test.go +++ b/bridge/github/import_test.go @@ -182,29 +182,24 @@ func TestGithubImporter(t *testing.T) { for i, op := range tt.bug.Operations { require.IsType(t, ops[i], op) + require.Equal(t, op.Author().Name(), ops[i].Author().Name()) - switch op.(type) { + switch op := op.(type) { case *bug.CreateOperation: - require.Equal(t, op.(*bug.CreateOperation).Title, ops[i].(*bug.CreateOperation).Title) - require.Equal(t, op.(*bug.CreateOperation).Message, ops[i].(*bug.CreateOperation).Message) - require.Equal(t, op.(*bug.CreateOperation).Author.Name(), ops[i].(*bug.CreateOperation).Author.Name()) + require.Equal(t, op.Title, ops[i].(*bug.CreateOperation).Title) + require.Equal(t, op.Message, ops[i].(*bug.CreateOperation).Message) case *bug.SetStatusOperation: - require.Equal(t, op.(*bug.SetStatusOperation).Status, ops[i].(*bug.SetStatusOperation).Status) - require.Equal(t, op.(*bug.SetStatusOperation).Author.Name(), ops[i].(*bug.SetStatusOperation).Author.Name()) + require.Equal(t, op.Status, ops[i].(*bug.SetStatusOperation).Status) case *bug.SetTitleOperation: - require.Equal(t, op.(*bug.SetTitleOperation).Was, ops[i].(*bug.SetTitleOperation).Was) - require.Equal(t, op.(*bug.SetTitleOperation).Title, ops[i].(*bug.SetTitleOperation).Title) - require.Equal(t, op.(*bug.SetTitleOperation).Author.Name(), ops[i].(*bug.SetTitleOperation).Author.Name()) + require.Equal(t, op.Was, ops[i].(*bug.SetTitleOperation).Was) + require.Equal(t, op.Title, ops[i].(*bug.SetTitleOperation).Title) case *bug.LabelChangeOperation: - require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Added, ops[i].(*bug.LabelChangeOperation).Added) - require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Removed, ops[i].(*bug.LabelChangeOperation).Removed) - require.Equal(t, op.(*bug.LabelChangeOperation).Author.Name(), ops[i].(*bug.LabelChangeOperation).Author.Name()) + require.ElementsMatch(t, op.Added, ops[i].(*bug.LabelChangeOperation).Added) + require.ElementsMatch(t, op.Removed, ops[i].(*bug.LabelChangeOperation).Removed) case *bug.AddCommentOperation: - require.Equal(t, op.(*bug.AddCommentOperation).Message, ops[i].(*bug.AddCommentOperation).Message) - require.Equal(t, op.(*bug.AddCommentOperation).Author.Name(), ops[i].(*bug.AddCommentOperation).Author.Name()) + require.Equal(t, op.Message, ops[i].(*bug.AddCommentOperation).Message) case *bug.EditCommentOperation: - require.Equal(t, op.(*bug.EditCommentOperation).Message, ops[i].(*bug.EditCommentOperation).Message) - require.Equal(t, op.(*bug.EditCommentOperation).Author.Name(), ops[i].(*bug.EditCommentOperation).Author.Name()) + require.Equal(t, op.Message, ops[i].(*bug.EditCommentOperation).Message) default: panic("unknown operation type") diff --git a/bridge/gitlab/export.go b/bridge/gitlab/export.go index c3aa6202192b7a727a9baad7f974cc7492929ad0..9c3864ecc6dd78b04321fda03ad025f4545bd497 100644 --- a/bridge/gitlab/export.go +++ b/bridge/gitlab/export.go @@ -267,7 +267,7 @@ func (ge *gitlabExporter) exportBug(ctx context.Context, b *cache.BugCache, out continue } - opAuthor := op.GetAuthor() + opAuthor := op.Author() client, err := ge.getIdentityClient(opAuthor.Id()) if err != nil { continue diff --git a/bridge/gitlab/import_test.go b/bridge/gitlab/import_test.go index 2956ad8b68e866c06239a5fb964f002c6ac4c29e..1405e43bba6101eee5b7a2dd4bfcf9c2503f6a3e 100644 --- a/bridge/gitlab/import_test.go +++ b/bridge/gitlab/import_test.go @@ -138,29 +138,24 @@ func TestGitlabImport(t *testing.T) { for i, op := range tt.bug.Operations { require.IsType(t, ops[i], op) + require.Equal(t, op.Author().Name(), ops[i].Author().Name()) - switch op.(type) { + switch op := op.(type) { case *bug.CreateOperation: - require.Equal(t, op.(*bug.CreateOperation).Title, ops[i].(*bug.CreateOperation).Title) - require.Equal(t, op.(*bug.CreateOperation).Message, ops[i].(*bug.CreateOperation).Message) - require.Equal(t, op.(*bug.CreateOperation).Author.Name(), ops[i].(*bug.CreateOperation).Author.Name()) + require.Equal(t, op.Title, ops[i].(*bug.CreateOperation).Title) + require.Equal(t, op.Message, ops[i].(*bug.CreateOperation).Message) case *bug.SetStatusOperation: - require.Equal(t, op.(*bug.SetStatusOperation).Status, ops[i].(*bug.SetStatusOperation).Status) - require.Equal(t, op.(*bug.SetStatusOperation).Author.Name(), ops[i].(*bug.SetStatusOperation).Author.Name()) + require.Equal(t, op.Status, ops[i].(*bug.SetStatusOperation).Status) case *bug.SetTitleOperation: - require.Equal(t, op.(*bug.SetTitleOperation).Was, ops[i].(*bug.SetTitleOperation).Was) - require.Equal(t, op.(*bug.SetTitleOperation).Title, ops[i].(*bug.SetTitleOperation).Title) - require.Equal(t, op.(*bug.SetTitleOperation).Author.Name(), ops[i].(*bug.SetTitleOperation).Author.Name()) + require.Equal(t, op.Was, ops[i].(*bug.SetTitleOperation).Was) + require.Equal(t, op.Title, ops[i].(*bug.SetTitleOperation).Title) case *bug.LabelChangeOperation: - require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Added, ops[i].(*bug.LabelChangeOperation).Added) - require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Removed, ops[i].(*bug.LabelChangeOperation).Removed) - require.Equal(t, op.(*bug.LabelChangeOperation).Author.Name(), ops[i].(*bug.LabelChangeOperation).Author.Name()) + require.ElementsMatch(t, op.Added, ops[i].(*bug.LabelChangeOperation).Added) + require.ElementsMatch(t, op.Removed, ops[i].(*bug.LabelChangeOperation).Removed) case *bug.AddCommentOperation: - require.Equal(t, op.(*bug.AddCommentOperation).Message, ops[i].(*bug.AddCommentOperation).Message) - require.Equal(t, op.(*bug.AddCommentOperation).Author.Name(), ops[i].(*bug.AddCommentOperation).Author.Name()) + require.Equal(t, op.Message, ops[i].(*bug.AddCommentOperation).Message) case *bug.EditCommentOperation: - require.Equal(t, op.(*bug.EditCommentOperation).Message, ops[i].(*bug.EditCommentOperation).Message) - require.Equal(t, op.(*bug.EditCommentOperation).Author.Name(), ops[i].(*bug.EditCommentOperation).Author.Name()) + require.Equal(t, op.Message, ops[i].(*bug.EditCommentOperation).Message) default: panic("unknown operation type") diff --git a/bridge/jira/export.go b/bridge/jira/export.go index e61679668a64c31d2934f7814f5c6fc755b58174..34f41d097d5ede2169af182003afdfd8e50c986d 100644 --- a/bridge/jira/export.go +++ b/bridge/jira/export.go @@ -309,7 +309,7 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, out ch continue } - opAuthor := op.GetAuthor() + opAuthor := op.Author() client, err := je.getClientForIdentity(opAuthor.Id()) if err != nil { out <- core.NewExportError( diff --git a/bug/bug.go b/bug/bug.go index 0c66f8acdd7358320c0f91400492cfd70eaea6bf..9d19a42cbdc3d58e440ba63975ca70f0d5bdeb36 100644 --- a/bug/bug.go +++ b/bug/bug.go @@ -2,222 +2,62 @@ package bug import ( - "encoding/json" "fmt" - "github.com/pkg/errors" - "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/entity/dag" "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" - "github.com/MichaelMure/git-bug/util/lamport" ) -const bugsRefPattern = "refs/bugs/" -const bugsRemoteRefPattern = "refs/remotes/%s/bugs/" - -const opsEntryName = "ops" -const mediaEntryName = "media" - -const createClockEntryPrefix = "create-clock-" -const createClockEntryPattern = "create-clock-%d" -const editClockEntryPrefix = "edit-clock-" -const editClockEntryPattern = "edit-clock-%d" - -const creationClockName = "bug-create" -const editClockName = "bug-edit" +var _ Interface = &Bug{} +var _ entity.Interface = &Bug{} -var ErrBugNotExist = errors.New("bug doesn't exist") +// 1: original format +// 2: no more legacy identities +// 3: Ids are generated from the create operation serialized data instead of from the first git commit +// 4: with DAG entity framework +const formatVersion = 4 -func NewErrMultipleMatchBug(matching []entity.Id) *entity.ErrMultipleMatch { - return entity.NewErrMultipleMatch("bug", matching) +var def = dag.Definition{ + Typename: "bug", + Namespace: "bugs", + OperationUnmarshaler: operationUnmarshaller, + FormatVersion: formatVersion, } -func NewErrMultipleMatchOp(matching []entity.Id) *entity.ErrMultipleMatch { - return entity.NewErrMultipleMatch("operation", matching) -} - -var _ Interface = &Bug{} -var _ entity.Interface = &Bug{} +var ClockLoader = dag.ClockLoader(def) // Bug hold the data of a bug thread, organized in a way close to // how it will be persisted inside Git. This is the data structure // used to merge two different version of the same Bug. type Bug struct { - // A Lamport clock is a logical clock that allow to order event - // inside a distributed system. - // It must be the first field in this struct due to https://github.com/golang/go/issues/599 - createTime lamport.Time - editTime lamport.Time - - lastCommit repository.Hash - - // all the committed operations - packs []OperationPack - - // a temporary pack of operations used for convenience to pile up new operations - // before a commit - staging OperationPack + *dag.Entity } // NewBug create a new Bug func NewBug() *Bug { - // No logical clock yet - return &Bug{} -} - -// ReadLocal will read a local bug from its hash -func ReadLocal(repo repository.ClockedRepo, id entity.Id) (*Bug, error) { - ref := bugsRefPattern + id.String() - return read(repo, identity.NewSimpleResolver(repo), ref) -} - -// ReadLocalWithResolver will read a local bug from its hash -func ReadLocalWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver, id entity.Id) (*Bug, error) { - ref := bugsRefPattern + id.String() - return read(repo, identityResolver, ref) -} - -// ReadRemote will read a remote bug from its hash -func ReadRemote(repo repository.ClockedRepo, remote string, id entity.Id) (*Bug, error) { - ref := fmt.Sprintf(bugsRemoteRefPattern, remote) + id.String() - return read(repo, identity.NewSimpleResolver(repo), ref) -} - -// ReadRemoteWithResolver will read a remote bug from its hash -func ReadRemoteWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver, remote string, id entity.Id) (*Bug, error) { - ref := fmt.Sprintf(bugsRemoteRefPattern, remote) + id.String() - return read(repo, identityResolver, ref) -} - -// read will read and parse a Bug from git -func read(repo repository.ClockedRepo, identityResolver identity.Resolver, ref string) (*Bug, error) { - id := entity.RefToId(ref) - - if err := id.Validate(); err != nil { - return nil, errors.Wrap(err, "invalid ref ") - } - - hashes, err := repo.ListCommits(ref) - if err != nil { - return nil, ErrBugNotExist - } - if len(hashes) == 0 { - return nil, fmt.Errorf("empty bug") - } - - bug := Bug{} - - // Load each OperationPack - for _, hash := range hashes { - tree, err := readTree(repo, hash) - if err != nil { - return nil, err - } - - // Due to rebase, edit Lamport time are not necessarily ordered - if tree.editTime > bug.editTime { - bug.editTime = tree.editTime - } - - // Update the clocks - err = repo.Witness(creationClockName, bug.createTime) - if err != nil { - return nil, errors.Wrap(err, "failed to update create lamport clock") - } - err = repo.Witness(editClockName, bug.editTime) - if err != nil { - return nil, errors.Wrap(err, "failed to update edit lamport clock") - } - - data, err := repo.ReadData(tree.opsEntry.Hash) - if err != nil { - return nil, errors.Wrap(err, "failed to read git blob data") - } - - opp := &OperationPack{} - err = json.Unmarshal(data, &opp) - if err != nil { - return nil, errors.Wrap(err, "failed to decode OperationPack json") - } - - // tag the pack with the commit hash - opp.commitHash = hash - bug.lastCommit = hash - - // if it's the first OperationPack read - if len(bug.packs) == 0 { - bug.createTime = tree.createTime - } - - bug.packs = append(bug.packs, *opp) - } - - // Bug Id is the Id of the first operation - if len(bug.packs[0].Operations) == 0 { - return nil, fmt.Errorf("first OperationPack is empty") - } - if id != bug.packs[0].Operations[0].Id() { - return nil, fmt.Errorf("bug ID doesn't match the first operation ID") + return &Bug{ + Entity: dag.New(def), } +} - // Make sure that the identities are properly loaded - err = bug.EnsureIdentities(identityResolver) +// Read will read a bug from a repository +func Read(repo repository.ClockedRepo, id entity.Id) (*Bug, error) { + e, err := dag.Read(def, repo, identity.NewSimpleResolver(repo), id) if err != nil { return nil, err } - - return &bug, nil + return &Bug{Entity: e}, nil } -// RemoveBug will remove a local bug from its entity.Id -func RemoveBug(repo repository.ClockedRepo, id entity.Id) error { - var fullMatches []string - - refs, err := repo.ListRefs(bugsRefPattern + id.String()) +// ReadWithResolver will read a bug from its Id, with a custom identity.Resolver +func ReadWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver, id entity.Id) (*Bug, error) { + e, err := dag.Read(def, repo, identityResolver, id) if err != nil { - return err - } - if len(refs) > 1 { - return NewErrMultipleMatchBug(entity.RefsToIds(refs)) - } - if len(refs) == 1 { - // we have the bug locally - fullMatches = append(fullMatches, refs[0]) - } - - remotes, err := repo.GetRemotes() - if err != nil { - return err - } - - for remote := range remotes { - remotePrefix := fmt.Sprintf(bugsRemoteRefPattern+id.String(), remote) - remoteRefs, err := repo.ListRefs(remotePrefix) - if err != nil { - return err - } - if len(remoteRefs) > 1 { - return NewErrMultipleMatchBug(entity.RefsToIds(refs)) - } - if len(remoteRefs) == 1 { - // found the bug in a remote - fullMatches = append(fullMatches, remoteRefs[0]) - } - } - - if len(fullMatches) == 0 { - return ErrBugNotExist - } - - for _, ref := range fullMatches { - err = repo.RemoveRef(ref) - if err != nil { - return err - } + return nil, err } - - return nil + return &Bug{Entity: e}, nil } type StreamedBug struct { @@ -225,50 +65,33 @@ type StreamedBug struct { Err error } -// ReadAllLocal read and parse all local bugs -func ReadAllLocal(repo repository.ClockedRepo) <-chan StreamedBug { - return readAll(repo, identity.NewSimpleResolver(repo), bugsRefPattern) +// ReadAll read and parse all local bugs +func ReadAll(repo repository.ClockedRepo) <-chan StreamedBug { + return readAll(repo, identity.NewSimpleResolver(repo)) } -// ReadAllLocalWithResolver read and parse all local bugs -func ReadAllLocalWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver) <-chan StreamedBug { - return readAll(repo, identityResolver, bugsRefPattern) -} - -// ReadAllRemote read and parse all remote bugs for a given remote -func ReadAllRemote(repo repository.ClockedRepo, remote string) <-chan StreamedBug { - refPrefix := fmt.Sprintf(bugsRemoteRefPattern, remote) - return readAll(repo, identity.NewSimpleResolver(repo), refPrefix) -} - -// ReadAllRemoteWithResolver read and parse all remote bugs for a given remote -func ReadAllRemoteWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver, remote string) <-chan StreamedBug { - refPrefix := fmt.Sprintf(bugsRemoteRefPattern, remote) - return readAll(repo, identityResolver, refPrefix) +// ReadAllWithResolver read and parse all local bugs +func ReadAllWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver) <-chan StreamedBug { + return readAll(repo, identityResolver) } // Read and parse all available bug with a given ref prefix -func readAll(repo repository.ClockedRepo, identityResolver identity.Resolver, refPrefix string) <-chan StreamedBug { +func readAll(repo repository.ClockedRepo, identityResolver identity.Resolver) <-chan StreamedBug { out := make(chan StreamedBug) go func() { defer close(out) - refs, err := repo.ListRefs(refPrefix) - if err != nil { - out <- StreamedBug{Err: err} - return - } - - for _, ref := range refs { - b, err := read(repo, identityResolver, ref) - - if err != nil { - out <- StreamedBug{Err: err} - return + for streamedEntity := range dag.ReadAll(def, repo, identityResolver) { + if streamedEntity.Err != nil { + out <- StreamedBug{ + Err: streamedEntity.Err, + } + } else { + out <- StreamedBug{ + Bug: &Bug{Entity: streamedEntity.Entity}, + } } - - out <- StreamedBug{Bug: b} } }() @@ -277,345 +100,78 @@ func readAll(repo repository.ClockedRepo, identityResolver identity.Resolver, re // ListLocalIds list all the available local bug ids func ListLocalIds(repo repository.Repo) ([]entity.Id, error) { - refs, err := repo.ListRefs(bugsRefPattern) - if err != nil { - return nil, err - } - - return entity.RefsToIds(refs), nil + return dag.ListLocalIds(def, repo) } // Validate check if the Bug data is valid func (bug *Bug) Validate() error { - // non-empty - if len(bug.packs) == 0 && bug.staging.IsEmpty() { - return fmt.Errorf("bug has no operations") - } - - // check if each pack and operations are valid - for _, pack := range bug.packs { - if err := pack.Validate(); err != nil { - return err - } - } - - // check if staging is valid if needed - if !bug.staging.IsEmpty() { - if err := bug.staging.Validate(); err != nil { - return errors.Wrap(err, "staging") - } + if err := bug.Entity.Validate(); err != nil { + return err } // The very first Op should be a CreateOp firstOp := bug.FirstOp() - if firstOp == nil || firstOp.base().OperationType != CreateOp { + if firstOp == nil || firstOp.Type() != CreateOp { return fmt.Errorf("first operation should be a Create op") } // Check that there is no more CreateOp op - // Check that there is no colliding operation's ID - it := NewOperationIterator(bug) - createCount := 0 - ids := make(map[entity.Id]struct{}) - for it.Next() { - if it.Value().base().OperationType == CreateOp { - createCount++ + for i, op := range bug.Operations() { + if i == 0 { + continue } - if _, ok := ids[it.Value().Id()]; ok { - return fmt.Errorf("id collision: %s", it.Value().Id()) + if op.Type() == CreateOp { + return fmt.Errorf("only one Create op allowed") } - ids[it.Value().Id()] = struct{}{} - } - - if createCount != 1 { - return fmt.Errorf("only one Create op allowed") } return nil } -// Append an operation into the staging area, to be committed later +// Append add a new Operation to the Bug func (bug *Bug) Append(op Operation) { - if len(bug.packs) == 0 && len(bug.staging.Operations) == 0 { - if op.base().OperationType != CreateOp { - panic("first operation should be a Create") - } - } - bug.staging.Append(op) + bug.Entity.Append(op) } -// Commit write the staging area in Git and move the operations to the packs -func (bug *Bug) Commit(repo repository.ClockedRepo) error { - if !bug.NeedCommit() { - return fmt.Errorf("can't commit a bug with no pending operation") - } - - if err := bug.Validate(); err != nil { - return errors.Wrap(err, "can't commit a bug with invalid data") - } - - // update clocks - var err error - bug.editTime, err = repo.Increment(editClockName) - if err != nil { - return err - } - if bug.lastCommit == "" { - bug.createTime, err = repo.Increment(creationClockName) - if err != nil { - return err - } +// Operations return the ordered operations +func (bug *Bug) Operations() []Operation { + source := bug.Entity.Operations() + result := make([]Operation, len(source)) + for i, op := range source { + result[i] = op.(Operation) } - - // Write the Ops as a Git blob containing the serialized array - hash, err := bug.staging.Write(repo) - if err != nil { - return err - } - - // Make a Git tree referencing this blob - tree := []repository.TreeEntry{ - // the last pack of ops - {ObjectType: repository.Blob, Hash: hash, Name: opsEntryName}, - } - - // Store the logical clocks as well - // --> edit clock for each OperationPack/commits - // --> create clock only for the first OperationPack/commits - // - // To avoid having one blob for each clock value, clocks are serialized - // directly into the entry name - emptyBlobHash, err := repo.StoreData([]byte{}) - if err != nil { - return err - } - tree = append(tree, repository.TreeEntry{ - ObjectType: repository.Blob, - Hash: emptyBlobHash, - Name: fmt.Sprintf(editClockEntryPattern, bug.editTime), - }) - if bug.lastCommit == "" { - tree = append(tree, repository.TreeEntry{ - ObjectType: repository.Blob, - Hash: emptyBlobHash, - Name: fmt.Sprintf(createClockEntryPattern, bug.createTime), - }) - } - - // Reference, if any, all the files required by the ops - // Git will check that they actually exist in the storage and will make sure - // to push/pull them as needed. - mediaTree := makeMediaTree(bug.staging) - if len(mediaTree) > 0 { - mediaTreeHash, err := repo.StoreTree(mediaTree) - if err != nil { - return err - } - tree = append(tree, repository.TreeEntry{ - ObjectType: repository.Tree, - Hash: mediaTreeHash, - Name: mediaEntryName, - }) - } - - // Store the tree - hash, err = repo.StoreTree(tree) - if err != nil { - return err - } - - // Write a Git commit referencing the tree, with the previous commit as parent - if bug.lastCommit != "" { - hash, err = repo.StoreCommit(hash, bug.lastCommit) - } else { - hash, err = repo.StoreCommit(hash) - } - if err != nil { - return err - } - - bug.lastCommit = hash - bug.staging.commitHash = hash - bug.packs = append(bug.packs, bug.staging) - bug.staging = OperationPack{} - - // Create or update the Git reference for this bug - // When pushing later, the remote will ensure that this ref update - // is fast-forward, that is no data has been overwritten - ref := fmt.Sprintf("%s%s", bugsRefPattern, bug.Id().String()) - return repo.UpdateRef(ref, hash) + return result } -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() -} - -// Merge a different version of the same bug by rebasing operations of this bug -// that are not present in the other on top of the chain of operations of the -// other version. -func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) { - var otherBug = bugFromInterface(other) - - // Note: a faster merge should be possible without actually reading and parsing - // all operations pack of our side. - // Reading the other side is still necessary to validate remote data, at least - // for new operations - - if bug.Id() != otherBug.Id() { - return false, errors.New("merging unrelated bugs is not supported") - } - - if len(otherBug.staging.Operations) > 0 { - return false, errors.New("merging a bug with a non-empty staging is not supported") - } - - if bug.lastCommit == "" || otherBug.lastCommit == "" { - return false, errors.New("can't merge a bug that has never been stored") - } - - ancestor, err := repo.FindCommonAncestor(bug.lastCommit, otherBug.lastCommit) - if err != nil { - return false, errors.Wrap(err, "can't find common ancestor") - } - - ancestorIndex := 0 - newPacks := make([]OperationPack, 0, len(bug.packs)) - - // Find the root of the rebase - for i, pack := range bug.packs { - newPacks = append(newPacks, pack) - - if pack.commitHash == ancestor { - ancestorIndex = i - break - } - } - - if len(otherBug.packs) == ancestorIndex+1 { - // Nothing to rebase, return early - return false, nil - } - - // get other bug's extra packs - for i := ancestorIndex + 1; i < len(otherBug.packs); i++ { - // clone is probably not necessary - newPack := otherBug.packs[i].Clone() - - newPacks = append(newPacks, newPack) - bug.lastCommit = newPack.commitHash - } - - // rebase our extra packs - for i := ancestorIndex + 1; i < len(bug.packs); i++ { - pack := bug.packs[i] - - // get the referenced git tree - treeHash, err := repo.GetTreeHash(pack.commitHash) - - if err != nil { - return false, err - } - - // create a new commit with the correct ancestor - hash, err := repo.StoreCommit(treeHash, bug.lastCommit) - - if err != nil { - return false, err - } - - // replace the pack - newPack := pack.Clone() - newPack.commitHash = hash - newPacks = append(newPacks, newPack) - - // update the bug - bug.lastCommit = hash +// Compile a bug in a easily usable snapshot +func (bug *Bug) Compile() Snapshot { + snap := Snapshot{ + id: bug.Id(), + Status: OpenStatus, } - bug.packs = newPacks - - // Update the git ref - err = repo.UpdateRef(bugsRefPattern+bug.Id().String(), bug.lastCommit) - if err != nil { - return false, err + for _, op := range bug.Operations() { + op.Apply(&snap) + snap.Operations = append(snap.Operations, op) } - return true, nil -} - -// Id return the Bug identifier -func (bug *Bug) Id() entity.Id { - // id is the id of the first operation - return bug.FirstOp().Id() -} - -// CreateLamportTime return the Lamport time of creation -func (bug *Bug) CreateLamportTime() lamport.Time { - return bug.createTime -} - -// EditLamportTime return the Lamport time of the last edit -func (bug *Bug) EditLamportTime() lamport.Time { - return bug.editTime + return snap } // Lookup for the very first operation of the bug. // For a valid Bug, this operation should be a CreateOp func (bug *Bug) FirstOp() Operation { - for _, pack := range bug.packs { - for _, op := range pack.Operations { - return op - } + if fo := bug.Entity.FirstOp(); fo != nil { + return fo.(Operation) } - - if !bug.staging.IsEmpty() { - return bug.staging.Operations[0] - } - return nil } // Lookup for the very last operation of the bug. // For a valid Bug, should never be nil func (bug *Bug) LastOp() Operation { - if !bug.staging.IsEmpty() { - return bug.staging.Operations[len(bug.staging.Operations)-1] + if lo := bug.Entity.LastOp(); lo != nil { + return lo.(Operation) } - - if len(bug.packs) == 0 { - return nil - } - - lastPack := bug.packs[len(bug.packs)-1] - - if len(lastPack.Operations) == 0 { - return nil - } - - return lastPack.Operations[len(lastPack.Operations)-1] -} - -// Compile a bug in a easily usable snapshot -func (bug *Bug) Compile() Snapshot { - snap := Snapshot{ - id: bug.Id(), - Status: OpenStatus, - } - - it := NewOperationIterator(bug) - - for it.Next() { - op := it.Value() - op.Apply(&snap) - snap.Operations = append(snap.Operations, op) - } - - return snap + return nil } diff --git a/bug/bug_actions.go b/bug/bug_actions.go index bf894ef8a55f50659fd5d2265eca709337bf3bbf..6ca5ffd776a0836d72a656363c931f686c0244a4 100644 --- a/bug/bug_actions.go +++ b/bug/bug_actions.go @@ -1,12 +1,10 @@ package bug import ( - "fmt" - "strings" - "github.com/pkg/errors" "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/entity/dag" "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" ) @@ -14,23 +12,23 @@ import ( // Fetch retrieve updates from a remote // This does not change the local bugs state func Fetch(repo repository.Repo, remote string) (string, error) { - return repo.FetchRefs(remote, "bugs") + return dag.Fetch(def, repo, remote) } // Push update a remote with the local changes func Push(repo repository.Repo, remote string) (string, error) { - return repo.PushRefs(remote, "bugs") + return dag.Push(def, repo, remote) } // Pull will do a Fetch + MergeAll // This function will return an error if a merge fail -func Pull(repo repository.ClockedRepo, remote string) error { +func Pull(repo repository.ClockedRepo, remote string, author identity.Interface) error { _, err := Fetch(repo, remote) if err != nil { return err } - for merge := range MergeAll(repo, remote) { + for merge := range MergeAll(repo, remote, author) { if merge.Err != nil { return merge.Err } @@ -42,95 +40,19 @@ func Pull(repo repository.ClockedRepo, remote string) error { return nil } -// 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 entity.MergeResult { - out := make(chan entity.MergeResult) - +// MergeAll will merge all the available remote bug +// Note: an author is necessary for the case where a merge commit is created, as this commit will +// have an author and may be signed if a signing key is available. +func MergeAll(repo repository.ClockedRepo, remote string, author identity.Interface) <-chan entity.MergeResult { // no caching for the merge, we load everything from git even if that means multiple // copy of the same entity in memory. The cache layer will intercept the results to // invalidate entities if necessary. identityResolver := identity.NewSimpleResolver(repo) - go func() { - defer close(out) - - remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote) - remoteRefs, err := repo.ListRefs(remoteRefSpec) - if err != nil { - out <- entity.MergeResult{Err: err} - return - } - - for _, remoteRef := range remoteRefs { - refSplit := strings.Split(remoteRef, "/") - id := entity.Id(refSplit[len(refSplit)-1]) - - if err := id.Validate(); err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "invalid ref").Error()) - continue - } - - remoteBug, err := read(repo, identityResolver, remoteRef) - - if err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "remote bug is not readable").Error()) - continue - } - - // Check for error in remote data - if err := remoteBug.Validate(); err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "remote bug is invalid").Error()) - continue - } - - localRef := bugsRefPattern + remoteBug.Id().String() - localExist, err := repo.RefExist(localRef) - - if err != nil { - out <- entity.NewMergeError(err, id) - continue - } - - // the bug is not local yet, simply create the reference - if !localExist { - err := repo.CopyRef(remoteRef, localRef) - - if err != nil { - out <- entity.NewMergeError(err, id) - return - } - - out <- entity.NewMergeNewStatus(id, remoteBug) - continue - } - - localBug, err := read(repo, identityResolver, localRef) - - if err != nil { - out <- entity.NewMergeError(errors.Wrap(err, "local bug is not readable"), id) - return - } - - updated, err := localBug.Merge(repo, remoteBug) - - if err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "merge failed").Error()) - return - } - - if updated { - out <- entity.NewMergeUpdatedStatus(id, localBug) - } else { - out <- entity.NewMergeNothingStatus(id) - } - } - }() + return dag.MergeAll(def, repo, identityResolver, remote, author) +} - return out +// RemoveBug will remove a local bug from its entity.Id +func RemoveBug(repo repository.ClockedRepo, id entity.Id) error { + return dag.Remove(def, repo, id) } diff --git a/bug/bug_actions_test.go b/bug/bug_actions_test.go deleted file mode 100644 index fc67106347134f76e430cc46c0ce974547783cea..0000000000000000000000000000000000000000 --- a/bug/bug_actions_test.go +++ /dev/null @@ -1,394 +0,0 @@ -package bug - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/repository" -) - -func TestPushPull(t *testing.T) { - repoA, repoB, remote := repository.SetupGoGitReposAndRemote() - defer repository.CleanupTestRepos(repoA, repoB, remote) - - reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = reneA.Commit(repoA) - require.NoError(t, err) - - bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message") - require.NoError(t, err) - assert.True(t, bug1.NeedCommit()) - err = bug1.Commit(repoA) - require.NoError(t, err) - assert.False(t, bug1.NeedCommit()) - - // distribute the identity - _, err = identity.Push(repoA, "origin") - require.NoError(t, err) - err = identity.Pull(repoB, "origin") - require.NoError(t, err) - - // A --> remote --> B - _, err = Push(repoA, "origin") - require.NoError(t, err) - - err = Pull(repoB, "origin") - require.NoError(t, err) - - bugs := allBugs(t, ReadAllLocal(repoB)) - - if len(bugs) != 1 { - t.Fatal("Unexpected number of bugs") - } - - // B --> remote --> A - 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) - require.NoError(t, err) - - _, err = Push(repoB, "origin") - require.NoError(t, err) - - err = Pull(repoA, "origin") - require.NoError(t, err) - - bugs = allBugs(t, ReadAllLocal(repoA)) - - if len(bugs) != 2 { - t.Fatal("Unexpected number of bugs") - } -} - -func allBugs(t testing.TB, bugs <-chan StreamedBug) []*Bug { - var result []*Bug - for streamed := range bugs { - if streamed.Err != nil { - t.Fatal(streamed.Err) - } - result = append(result, streamed.Bug) - } - return result -} - -func TestRebaseTheirs(t *testing.T) { - _RebaseTheirs(t) -} - -func BenchmarkRebaseTheirs(b *testing.B) { - for n := 0; n < b.N; n++ { - _RebaseTheirs(b) - } -} - -func _RebaseTheirs(t testing.TB) { - repoA, repoB, remote := repository.SetupGoGitReposAndRemote() - defer repository.CleanupTestRepos(repoA, repoB, remote) - - reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = reneA.Commit(repoA) - require.NoError(t, err) - - bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message") - require.NoError(t, err) - assert.True(t, bug1.NeedCommit()) - err = bug1.Commit(repoA) - require.NoError(t, err) - assert.False(t, bug1.NeedCommit()) - - // distribute the identity - _, err = identity.Push(repoA, "origin") - require.NoError(t, err) - err = identity.Pull(repoB, "origin") - require.NoError(t, err) - - // A --> remote - - _, err = Push(repoA, "origin") - require.NoError(t, err) - - // remote --> B - err = Pull(repoB, "origin") - require.NoError(t, err) - - bug2, err := ReadLocal(repoB, bug1.Id()) - require.NoError(t, err) - assert.False(t, bug2.NeedCommit()) - - reneB, err := identity.ReadLocal(repoA, reneA.Id()) - require.NoError(t, err) - - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message2") - require.NoError(t, err) - assert.True(t, bug2.NeedCommit()) - _, 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) - require.NoError(t, err) - assert.False(t, bug2.NeedCommit()) - - // B --> remote - _, err = Push(repoB, "origin") - require.NoError(t, err) - - // remote --> A - err = Pull(repoA, "origin") - require.NoError(t, err) - - bugs := allBugs(t, ReadAllLocal(repoB)) - - if len(bugs) != 1 { - t.Fatal("Unexpected number of bugs") - } - - bug3, err := ReadLocal(repoA, bug1.Id()) - require.NoError(t, err) - - if nbOps(bug3) != 4 { - t.Fatal("Unexpected number of operations") - } -} - -func TestRebaseOurs(t *testing.T) { - _RebaseOurs(t) -} - -func BenchmarkRebaseOurs(b *testing.B) { - for n := 0; n < b.N; n++ { - _RebaseOurs(b) - } -} - -func _RebaseOurs(t testing.TB) { - repoA, repoB, remote := repository.SetupGoGitReposAndRemote() - defer repository.CleanupTestRepos(repoA, repoB, remote) - - reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = reneA.Commit(repoA) - require.NoError(t, err) - - bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message") - require.NoError(t, err) - err = bug1.Commit(repoA) - require.NoError(t, err) - - // distribute the identity - _, err = identity.Push(repoA, "origin") - require.NoError(t, err) - err = identity.Pull(repoB, "origin") - require.NoError(t, err) - - // A --> remote - _, err = Push(repoA, "origin") - require.NoError(t, err) - - // remote --> B - err = Pull(repoB, "origin") - 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) - 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) - 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) - require.NoError(t, err) - - // remote --> A - err = Pull(repoA, "origin") - require.NoError(t, err) - - bugs := allBugs(t, ReadAllLocal(repoA)) - - if len(bugs) != 1 { - t.Fatal("Unexpected number of bugs") - } - - bug2, err := ReadLocal(repoA, bug1.Id()) - require.NoError(t, err) - - if nbOps(bug2) != 10 { - t.Fatal("Unexpected number of operations") - } -} - -func nbOps(b *Bug) int { - it := NewOperationIterator(b) - counter := 0 - for it.Next() { - counter++ - } - return counter -} - -func TestRebaseConflict(t *testing.T) { - _RebaseConflict(t) -} - -func BenchmarkRebaseConflict(b *testing.B) { - for n := 0; n < b.N; n++ { - _RebaseConflict(b) - } -} - -func _RebaseConflict(t testing.TB) { - repoA, repoB, remote := repository.SetupGoGitReposAndRemote() - defer repository.CleanupTestRepos(repoA, repoB, remote) - - reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = reneA.Commit(repoA) - require.NoError(t, err) - - bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message") - require.NoError(t, err) - err = bug1.Commit(repoA) - require.NoError(t, err) - - // distribute the identity - _, err = identity.Push(repoA, "origin") - require.NoError(t, err) - err = identity.Pull(repoB, "origin") - require.NoError(t, err) - - // A --> remote - _, err = Push(repoA, "origin") - require.NoError(t, err) - - // remote --> B - err = Pull(repoB, "origin") - 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) - 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) - 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) - require.NoError(t, err) - - bug2, err := ReadLocal(repoB, bug1.Id()) - 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) - 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) - 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) - require.NoError(t, err) - - // A --> remote - _, err = Push(repoA, "origin") - require.NoError(t, err) - - // remote --> B - err = Pull(repoB, "origin") - require.NoError(t, err) - - bugs := allBugs(t, ReadAllLocal(repoB)) - - if len(bugs) != 1 { - t.Fatal("Unexpected number of bugs") - } - - bug3, err := ReadLocal(repoB, bug1.Id()) - require.NoError(t, err) - - if nbOps(bug3) != 19 { - t.Fatal("Unexpected number of operations") - } - - // B --> remote - _, err = Push(repoB, "origin") - require.NoError(t, err) - - // remote --> A - err = Pull(repoA, "origin") - require.NoError(t, err) - - bugs = allBugs(t, ReadAllLocal(repoA)) - - if len(bugs) != 1 { - t.Fatal("Unexpected number of bugs") - } - - bug4, err := ReadLocal(repoA, bug1.Id()) - 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 deleted file mode 100644 index a8987ac1143795982601034562f75679e0d75e80..0000000000000000000000000000000000000000 --- a/bug/bug_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package bug - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/repository" -) - -func TestBugId(t *testing.T) { - repo := repository.NewMockRepo() - - bug1 := NewBug() - - rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = rene.Commit(repo) - require.NoError(t, err) - - createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil) - - bug1.Append(createOp) - - err = bug1.Commit(repo) - - if err != nil { - t.Fatal(err) - } - - bug1.Id() -} - -func TestBugValidity(t *testing.T) { - repo := repository.NewMockRepo() - - bug1 := NewBug() - - rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = rene.Commit(repo) - require.NoError(t, err) - - createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil) - - if bug1.Validate() == nil { - t.Fatal("Empty bug should be invalid") - } - - bug1.Append(createOp) - - if bug1.Validate() != nil { - t.Fatal("Bug with just a CreateOp should be valid") - } - - err = bug1.Commit(repo) - if err != nil { - t.Fatal(err) - } - - bug1.Append(createOp) - - if bug1.Validate() == nil { - t.Fatal("Bug with multiple CreateOp should be invalid") - } - - err = bug1.Commit(repo) - if err == nil { - t.Fatal("Invalid bug should not commit") - } -} - -func TestBugCommitLoad(t *testing.T) { - repo := repository.NewMockRepo() - - bug1 := NewBug() - - rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = rene.Commit(repo) - require.NoError(t, err) - - 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) - - require.True(t, bug1.NeedCommit()) - - err = bug1.Commit(repo) - require.Nil(t, err) - require.False(t, bug1.NeedCommit()) - - bug2, err := ReadLocal(repo, bug1.Id()) - require.NoError(t, err) - equivalentBug(t, bug1, bug2) - - // add more op - - bug1.Append(addCommentOp) - - require.True(t, bug1.NeedCommit()) - - err = bug1.Commit(repo) - require.Nil(t, err) - require.False(t, bug1.NeedCommit()) - - bug3, err := ReadLocal(repo, bug1.Id()) - require.NoError(t, err) - equivalentBug(t, bug1, bug3) -} - -func equivalentBug(t *testing.T, expected, actual *Bug) { - require.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().id = expected.packs[i].Operations[j].base().id - } - } - - require.Equal(t, expected, actual) -} - -func TestBugRemove(t *testing.T) { - repo := repository.CreateGoGitTestRepo(false) - remoteA := repository.CreateGoGitTestRepo(true) - remoteB := repository.CreateGoGitTestRepo(true) - defer repository.CleanupTestRepos(repo, remoteA, remoteB) - - err := repo.AddRemote("remoteA", remoteA.GetLocalRemote()) - require.NoError(t, err) - - err = repo.AddRemote("remoteB", remoteB.GetLocalRemote()) - require.NoError(t, err) - - // generate a bunch of bugs - rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = rene.Commit(repo) - require.NoError(t, err) - - for i := 0; i < 100; i++ { - b := NewBug() - createOp := NewCreateOp(rene, time.Now().Unix(), "title", fmt.Sprintf("message%v", i), nil) - b.Append(createOp) - err = b.Commit(repo) - require.NoError(t, err) - } - - // and one more for testing - b := NewBug() - createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil) - b.Append(createOp) - err = b.Commit(repo) - require.NoError(t, err) - - _, err = Push(repo, "remoteA") - require.NoError(t, err) - - _, err = Push(repo, "remoteB") - require.NoError(t, err) - - _, err = Fetch(repo, "remoteA") - require.NoError(t, err) - - _, err = Fetch(repo, "remoteB") - require.NoError(t, err) - - err = RemoveBug(repo, b.Id()) - require.NoError(t, err) - - _, err = ReadLocal(repo, b.Id()) - require.Error(t, ErrBugNotExist, err) - - _, err = ReadRemote(repo, "remoteA", b.Id()) - require.Error(t, ErrBugNotExist, err) - - _, err = ReadRemote(repo, "remoteB", b.Id()) - require.Error(t, ErrBugNotExist, err) - - ids, err := ListLocalIds(repo) - require.NoError(t, err) - require.Len(t, ids, 100) -} diff --git a/bug/clocks.go b/bug/clocks.go deleted file mode 100644 index 58fce923e4e495d2dcf5bd4e9f5d5bc5b1e44b2a..0000000000000000000000000000000000000000 --- a/bug/clocks.go +++ /dev/null @@ -1,40 +0,0 @@ -package bug - -import ( - "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/repository" -) - -// ClockLoader is the repository.ClockLoader for the Bug entity -var ClockLoader = repository.ClockLoader{ - Clocks: []string{creationClockName, editClockName}, - Witnesser: func(repo repository.ClockedRepo) error { - // We don't care about the actual identity so an IdentityStub will do - resolver := identity.NewStubResolver() - for b := range ReadAllLocalWithResolver(repo, resolver) { - if b.Err != nil { - return b.Err - } - - createClock, err := repo.GetOrCreateClock(creationClockName) - if err != nil { - return err - } - err = createClock.Witness(b.Bug.createTime) - if err != nil { - return err - } - - editClock, err := repo.GetOrCreateClock(editClockName) - if err != nil { - return err - } - err = editClock.Witness(b.Bug.editTime) - if err != nil { - return err - } - } - - return nil - }, -} diff --git a/bug/err.go b/bug/err.go new file mode 100644 index 0000000000000000000000000000000000000000..1bd174bb349814d0e6830a472d5e724699d33dc1 --- /dev/null +++ b/bug/err.go @@ -0,0 +1,17 @@ +package bug + +import ( + "errors" + + "github.com/MichaelMure/git-bug/entity" +) + +var ErrBugNotExist = errors.New("bug doesn't exist") + +func NewErrMultipleMatchBug(matching []entity.Id) *entity.ErrMultipleMatch { + return entity.NewErrMultipleMatch("bug", matching) +} + +func NewErrMultipleMatchOp(matching []entity.Id) *entity.ErrMultipleMatch { + return entity.NewErrMultipleMatch("operation", matching) +} diff --git a/bug/git_tree.go b/bug/git_tree.go deleted file mode 100644 index a5583bda4411affb342d83c84a915bae2c1f94eb..0000000000000000000000000000000000000000 --- a/bug/git_tree.go +++ /dev/null @@ -1,84 +0,0 @@ -package bug - -import ( - "fmt" - "strings" - - "github.com/pkg/errors" - - "github.com/MichaelMure/git-bug/repository" - "github.com/MichaelMure/git-bug/util/lamport" -) - -type gitTree struct { - opsEntry repository.TreeEntry - createTime lamport.Time - editTime lamport.Time -} - -func readTree(repo repository.RepoData, hash repository.Hash) (*gitTree, error) { - tree := &gitTree{} - - entries, err := repo.ReadTree(hash) - if err != nil { - return nil, errors.Wrap(err, "can't list git tree entries") - } - - opsFound := false - - for _, entry := range entries { - if entry.Name == opsEntryName { - tree.opsEntry = entry - opsFound = true - continue - } - if strings.HasPrefix(entry.Name, createClockEntryPrefix) { - n, err := fmt.Sscanf(entry.Name, createClockEntryPattern, &tree.createTime) - if err != nil { - return nil, errors.Wrap(err, "can't read create lamport time") - } - if n != 1 { - return nil, fmt.Errorf("could not parse create time lamport value") - } - } - if strings.HasPrefix(entry.Name, editClockEntryPrefix) { - n, err := fmt.Sscanf(entry.Name, editClockEntryPattern, &tree.editTime) - if err != nil { - return nil, errors.Wrap(err, "can't read edit lamport time") - } - if n != 1 { - return nil, fmt.Errorf("could not parse edit time lamport value") - } - } - } - - if !opsFound { - return nil, errors.New("invalid tree, missing the ops entry") - } - - return tree, nil -} - -func makeMediaTree(pack OperationPack) []repository.TreeEntry { - var tree []repository.TreeEntry - counter := 0 - added := make(map[repository.Hash]interface{}) - - for _, ops := range pack.Operations { - for _, file := range ops.GetFiles() { - if _, has := added[file]; !has { - tree = append(tree, repository.TreeEntry{ - ObjectType: repository.Blob, - Hash: file, - // The name is not important here, we only need to - // reference the blob. - Name: fmt.Sprintf("file%d", counter), - }) - counter++ - added[file] = struct{}{} - } - } - } - - return tree -} diff --git a/bug/identity.go b/bug/identity.go deleted file mode 100644 index 2eb2bcaf022bc1ba28fadca4f7a3ec0d677c7b87..0000000000000000000000000000000000000000 --- a/bug/identity.go +++ /dev/null @@ -1,27 +0,0 @@ -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/interface.go b/bug/interface.go index 5c8f27293e58772834b030a45da661375d977df2..e71496a950435605b116f62d4e23360886b2d545 100644 --- a/bug/interface.go +++ b/bug/interface.go @@ -16,17 +16,15 @@ type Interface interface { // Append an operation into the staging area, to be committed later Append(op Operation) + // Operations return the ordered operations + Operations() []Operation + // Indicate that the in-memory state changed and need to be commit in the repository NeedCommit() bool // Commit write the staging area in Git and move the operations to the packs Commit(repo repository.ClockedRepo) error - // Merge a different version of the same bug by rebasing operations of this bug - // that are not present in the other on top of the chain of operations of the - // other version. - Merge(repo repository.Repo, other Interface) (bool, error) - // Lookup for the very first operation of the bug. // For a valid Bug, this operation should be a CreateOp FirstOp() Operation diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go index e52c46fdef3b6ad44f9960296c1016f6553026f7..fd00860b2941fae558b4dd50cf0b82a9eb71db8a 100644 --- a/bug/op_add_comment.go +++ b/bug/op_add_comment.go @@ -24,23 +24,19 @@ type AddCommentOperation struct { // Sign-post method for gqlgen func (op *AddCommentOperation) IsOperation() {} -func (op *AddCommentOperation) base() *OpBase { - return &op.OpBase -} - func (op *AddCommentOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } func (op *AddCommentOperation) Apply(snapshot *Snapshot) { - snapshot.addActor(op.Author) - snapshot.addParticipant(op.Author) + snapshot.addActor(op.Author_) + snapshot.addParticipant(op.Author_) commentId := entity.CombineIds(snapshot.Id(), op.Id()) comment := Comment{ id: commentId, Message: op.Message, - Author: op.Author, + Author: op.Author_, Files: op.Files, UnixTime: timestamp.Timestamp(op.UnixTime), } @@ -59,7 +55,7 @@ func (op *AddCommentOperation) GetFiles() []repository.Hash { } func (op *AddCommentOperation) Validate() error { - if err := opBaseValidate(op, AddCommentOp); err != nil { + if err := op.OpBase.Validate(op, AddCommentOp); err != nil { return err } diff --git a/bug/op_add_comment_test.go b/bug/op_add_comment_test.go index 3b41d62d3b83323f9b8083d409d632fb6d57854c..fb6fa8ed9e1038aa222e529c204582735ea25250 100644 --- a/bug/op_add_comment_test.go +++ b/bug/op_add_comment_test.go @@ -32,8 +32,8 @@ func TestAddCommentSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - assert.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + assert.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene assert.Equal(t, before, &after) } diff --git a/bug/op_create.go b/bug/op_create.go index 1e944d1362a8e95b16caf7d4e70a1275ca408265..2423e5714cf56f06171a6b64a1bb05a00247093c 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -30,12 +30,8 @@ type CreateOperation struct { // Sign-post method for gqlgen func (op *CreateOperation) IsOperation() {} -func (op *CreateOperation) base() *OpBase { - return &op.OpBase -} - func (op *CreateOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } // OVERRIDE @@ -61,8 +57,8 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) { snapshot.id = op.Id() - snapshot.addActor(op.Author) - snapshot.addParticipant(op.Author) + snapshot.addActor(op.Author_) + snapshot.addParticipant(op.Author_) snapshot.Title = op.Title @@ -70,12 +66,12 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) { comment := Comment{ id: commentId, Message: op.Message, - Author: op.Author, + Author: op.Author_, UnixTime: timestamp.Timestamp(op.UnixTime), } snapshot.Comments = []Comment{comment} - snapshot.Author = op.Author + snapshot.Author = op.Author_ snapshot.CreateTime = op.Time() snapshot.Timeline = []TimelineItem{ @@ -90,7 +86,7 @@ func (op *CreateOperation) GetFiles() []repository.Hash { } func (op *CreateOperation) Validate() error { - if err := opBaseValidate(op, CreateOp); err != nil { + if err := op.OpBase.Validate(op, CreateOp); err != nil { return err } diff --git a/bug/op_create_test.go b/bug/op_create_test.go index 456357c4bcc7c1dd3c90a6d5526786dae9012009..1b359deea0f6ef78dc966b3b5e314450d567d7af 100644 --- a/bug/op_create_test.go +++ b/bug/op_create_test.go @@ -79,8 +79,8 @@ func TestCreateSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - require.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + require.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene require.Equal(t, before, &after) } diff --git a/bug/op_edit_comment.go b/bug/op_edit_comment.go index 5bfc36bf62b3795d340478d7341a0b2d935731f0..f9e30e62b47ba2478c1afd6abcc712bff2227ef6 100644 --- a/bug/op_edit_comment.go +++ b/bug/op_edit_comment.go @@ -27,19 +27,15 @@ type EditCommentOperation struct { // Sign-post method for gqlgen func (op *EditCommentOperation) IsOperation() {} -func (op *EditCommentOperation) base() *OpBase { - return &op.OpBase -} - func (op *EditCommentOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } func (op *EditCommentOperation) Apply(snapshot *Snapshot) { // Todo: currently any message can be edited, even by a different author // crypto signature are needed. - snapshot.addActor(op.Author) + snapshot.addActor(op.Author_) var target TimelineItem @@ -85,7 +81,7 @@ func (op *EditCommentOperation) GetFiles() []repository.Hash { } func (op *EditCommentOperation) Validate() error { - if err := opBaseValidate(op, EditCommentOp); err != nil { + if err := op.OpBase.Validate(op, EditCommentOp); err != nil { return err } diff --git a/bug/op_edit_comment_test.go b/bug/op_edit_comment_test.go index a7330932924c29c174066fbe50f3ab993e1f6c3a..777f5f87aaacf9a36ec4bd33dc8c7062daff545e 100644 --- a/bug/op_edit_comment_test.go +++ b/bug/op_edit_comment_test.go @@ -97,8 +97,8 @@ func TestEditCommentSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - require.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + require.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene require.Equal(t, before, &after) } diff --git a/bug/op_label_change.go b/bug/op_label_change.go index fefe2402a6efa160bbe343fbc0138b9e24fad021..5e51534d6f2e6c1ecfce624a30ea2f3fef5e8d47 100644 --- a/bug/op_label_change.go +++ b/bug/op_label_change.go @@ -24,17 +24,13 @@ type LabelChangeOperation struct { // Sign-post method for gqlgen func (op *LabelChangeOperation) IsOperation() {} -func (op *LabelChangeOperation) base() *OpBase { - return &op.OpBase -} - func (op *LabelChangeOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } // Apply apply the operation func (op *LabelChangeOperation) Apply(snapshot *Snapshot) { - snapshot.addActor(op.Author) + snapshot.addActor(op.Author_) // Add in the set AddLoop: @@ -66,7 +62,7 @@ AddLoop: item := &LabelChangeTimelineItem{ id: op.Id(), - Author: op.Author, + Author: op.Author_, UnixTime: timestamp.Timestamp(op.UnixTime), Added: op.Added, Removed: op.Removed, @@ -76,7 +72,7 @@ AddLoop: } func (op *LabelChangeOperation) Validate() error { - if err := opBaseValidate(op, LabelChangeOp); err != nil { + if err := op.OpBase.Validate(op, LabelChangeOp); err != nil { return err } diff --git a/bug/op_label_change_test.go b/bug/op_label_change_test.go index 96716ffe490e412663d7305c5b134773a34756a2..40dc4f0dcd28eaba2948f02ba21d4a745b267066 100644 --- a/bug/op_label_change_test.go +++ b/bug/op_label_change_test.go @@ -31,8 +31,8 @@ func TestLabelChangeSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - require.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + require.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene require.Equal(t, before, &after) } diff --git a/bug/op_noop.go b/bug/op_noop.go index 6364f9187b317f7d835160dc8475bddc54cf96c5..d59666ae38dacf50cbc020cc5b7f27ca229ae120 100644 --- a/bug/op_noop.go +++ b/bug/op_noop.go @@ -19,12 +19,8 @@ type NoOpOperation struct { // Sign-post method for gqlgen func (op *NoOpOperation) IsOperation() {} -func (op *NoOpOperation) base() *OpBase { - return &op.OpBase -} - func (op *NoOpOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } func (op *NoOpOperation) Apply(snapshot *Snapshot) { @@ -32,7 +28,7 @@ func (op *NoOpOperation) Apply(snapshot *Snapshot) { } func (op *NoOpOperation) Validate() error { - return opBaseValidate(op, NoOpOp) + return op.OpBase.Validate(op, NoOpOp) } // UnmarshalJSON is a two step JSON unmarshaling diff --git a/bug/op_noop_test.go b/bug/op_noop_test.go index ce2f98afa4089f3de66bf23d22dbf0540c72a6ec..0e3727c2c125d440ee40dfbf78152eeec98d4559 100644 --- a/bug/op_noop_test.go +++ b/bug/op_noop_test.go @@ -33,8 +33,8 @@ func TestNoopSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - assert.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + assert.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene assert.Equal(t, before, &after) } diff --git a/bug/op_set_metadata.go b/bug/op_set_metadata.go index 23d11461c5fed9296fe84a4927d3722c53c81876..c4988b84f67163a648b98b6445c0cc265486ad03 100644 --- a/bug/op_set_metadata.go +++ b/bug/op_set_metadata.go @@ -20,38 +20,25 @@ type SetMetadataOperation struct { // Sign-post method for gqlgen func (op *SetMetadataOperation) IsOperation() {} -func (op *SetMetadataOperation) base() *OpBase { - return &op.OpBase -} - func (op *SetMetadataOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } func (op *SetMetadataOperation) Apply(snapshot *Snapshot) { for _, target := range snapshot.Operations { if target.Id() == op.Target { - base := target.base() - - if base.extraMetadata == nil { - base.extraMetadata = make(map[string]string) - } - // Apply the metadata in an immutable way: if a metadata already // exist, it's not possible to override it. - for key, val := range op.NewMetadata { - if _, exist := base.extraMetadata[key]; !exist { - base.extraMetadata[key] = val - } + for key, value := range op.NewMetadata { + target.setExtraMetadataImmutable(key, value) } - return } } } func (op *SetMetadataOperation) Validate() error { - if err := opBaseValidate(op, SetMetadataOp); err != nil { + if err := op.OpBase.Validate(op, SetMetadataOp); err != nil { return err } diff --git a/bug/op_set_metadata_test.go b/bug/op_set_metadata_test.go index c0c91617c1926f1abcd7a2049f2350655694b481..78f7d883a5698bf119184864bfe83abb192184a9 100644 --- a/bug/op_set_metadata_test.go +++ b/bug/op_set_metadata_test.go @@ -120,8 +120,8 @@ func TestSetMetadataSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - require.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + require.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene require.Equal(t, before, &after) } diff --git a/bug/op_set_status.go b/bug/op_set_status.go index eb2c0ba4d9383b5be8a0eb5f76f5c81e84a3a7fc..fcf33f8c55d15791f1574ec08864e2c6eb7da0c8 100644 --- a/bug/op_set_status.go +++ b/bug/op_set_status.go @@ -21,21 +21,17 @@ type SetStatusOperation struct { // Sign-post method for gqlgen func (op *SetStatusOperation) IsOperation() {} -func (op *SetStatusOperation) base() *OpBase { - return &op.OpBase -} - func (op *SetStatusOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } func (op *SetStatusOperation) Apply(snapshot *Snapshot) { snapshot.Status = op.Status - snapshot.addActor(op.Author) + snapshot.addActor(op.Author_) item := &SetStatusTimelineItem{ id: op.Id(), - Author: op.Author, + Author: op.Author_, UnixTime: timestamp.Timestamp(op.UnixTime), Status: op.Status, } @@ -44,7 +40,7 @@ func (op *SetStatusOperation) Apply(snapshot *Snapshot) { } func (op *SetStatusOperation) Validate() error { - if err := opBaseValidate(op, SetStatusOp); err != nil { + if err := op.OpBase.Validate(op, SetStatusOp); err != nil { return err } diff --git a/bug/op_set_status_test.go b/bug/op_set_status_test.go index 3b26282f72d611609309985b8409c9a72b005b31..83ff22aef648e2bd6c78fc39ed2204d4340e41af 100644 --- a/bug/op_set_status_test.go +++ b/bug/op_set_status_test.go @@ -31,8 +31,8 @@ func TestSetStatusSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - require.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + require.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene require.Equal(t, before, &after) } diff --git a/bug/op_set_title.go b/bug/op_set_title.go index ddd98f0e0a42ddf05ee50b52af69e57828d937f2..7fd7c20d16039c22322d33cbeceee16a5e0c9598 100644 --- a/bug/op_set_title.go +++ b/bug/op_set_title.go @@ -24,21 +24,17 @@ type SetTitleOperation struct { // Sign-post method for gqlgen func (op *SetTitleOperation) IsOperation() {} -func (op *SetTitleOperation) base() *OpBase { - return &op.OpBase -} - func (op *SetTitleOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } func (op *SetTitleOperation) Apply(snapshot *Snapshot) { snapshot.Title = op.Title - snapshot.addActor(op.Author) + snapshot.addActor(op.Author_) item := &SetTitleTimelineItem{ id: op.Id(), - Author: op.Author, + Author: op.Author_, UnixTime: timestamp.Timestamp(op.UnixTime), Title: op.Title, Was: op.Was, @@ -48,7 +44,7 @@ func (op *SetTitleOperation) Apply(snapshot *Snapshot) { } func (op *SetTitleOperation) Validate() error { - if err := opBaseValidate(op, SetTitleOp); err != nil { + if err := op.OpBase.Validate(op, SetTitleOp); err != nil { return err } @@ -132,19 +128,17 @@ func (s *SetTitleTimelineItem) IsAuthored() {} // Convenience function to apply the operation func SetTitle(b Interface, author identity.Interface, unixTime int64, title string) (*SetTitleOperation, error) { - it := NewOperationIterator(b) - - var lastTitleOp Operation - for it.Next() { - op := it.Value() - if op.base().OperationType == SetTitleOp { + var lastTitleOp *SetTitleOperation + for _, op := range b.Operations() { + switch op := op.(type) { + case *SetTitleOperation: lastTitleOp = op } } var was string if lastTitleOp != nil { - was = lastTitleOp.(*SetTitleOperation).Title + was = lastTitleOp.Title } else { was = b.FirstOp().(*CreateOperation).Title } diff --git a/bug/op_set_title_test.go b/bug/op_set_title_test.go index 6ae325bedde03a597368173da104e11794153282..7059c4c7a99f2eb02f1a627155c4fbe5ec6bca56 100644 --- a/bug/op_set_title_test.go +++ b/bug/op_set_title_test.go @@ -31,8 +31,8 @@ func TestSetTitleSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - require.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + require.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene require.Equal(t, before, &after) } diff --git a/bug/operation.go b/bug/operation.go index bdaa2016e6ee77c4ee593a85df7c7f26c5c3420c..71a5c15dc23d32b5db46bcdc82367a981f0036e4 100644 --- a/bug/operation.go +++ b/bug/operation.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/entity/dag" "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" ) @@ -29,34 +30,31 @@ const ( // Operation define the interface to fulfill for an edit operation of a Bug type Operation interface { - // base return the OpBase of the Operation, for package internal use - base() *OpBase - // Id return the identifier of the operation, to be used for back references - Id() entity.Id + dag.Operation + + // Type return the type of the operation + Type() OperationType + // Time return the time when the operation was added Time() time.Time // GetFiles return the files needed by this operation GetFiles() []repository.Hash // Apply the operation to a Snapshot to create the final state Apply(snapshot *Snapshot) - // Validate check if the operation is valid (ex: a title is a single line) - Validate() error // SetMetadata store arbitrary metadata about the operation SetMetadata(key string, value string) // GetMetadata retrieve arbitrary metadata about the operation GetMetadata(key string) (string, bool) // AllMetadata return all metadata for this operation AllMetadata() map[string]string - // GetAuthor return the author identity - GetAuthor() identity.Interface + + setExtraMetadataImmutable(key string, value string) // sign-post method for gqlgen IsOperation() } -func idOperation(op Operation) entity.Id { - base := op.base() - +func idOperation(op Operation, base *OpBase) entity.Id { if base.id == "" { // something went really wrong panic("op's id not set") @@ -77,10 +75,69 @@ func idOperation(op Operation) entity.Id { return base.id } +func operationUnmarshaller(author identity.Interface, raw json.RawMessage) (dag.Operation, error) { + var t struct { + OperationType OperationType `json:"type"` + } + + if err := json.Unmarshal(raw, &t); err != nil { + return nil, err + } + + var op Operation + + switch t.OperationType { + case AddCommentOp: + op = &AddCommentOperation{} + case CreateOp: + op = &CreateOperation{} + case EditCommentOp: + op = &EditCommentOperation{} + case LabelChangeOp: + op = &LabelChangeOperation{} + case NoOpOp: + op = &NoOpOperation{} + case SetMetadataOp: + op = &SetMetadataOperation{} + case SetStatusOp: + op = &SetStatusOperation{} + case SetTitleOp: + op = &SetTitleOperation{} + default: + panic(fmt.Sprintf("unknown operation type %v", t.OperationType)) + } + + err := json.Unmarshal(raw, &op) + if err != nil { + return nil, err + } + + switch op := op.(type) { + case *AddCommentOperation: + op.Author_ = author + case *CreateOperation: + op.Author_ = author + case *LabelChangeOperation: + op.Author_ = author + case *NoOpOperation: + op.Author_ = author + case *SetMetadataOperation: + op.Author_ = author + case *SetStatusOperation: + op.Author_ = author + case *SetTitleOperation: + op.Author_ = author + default: + panic(fmt.Sprintf("unknown operation type %T", op)) + } + + return op, nil +} + // OpBase implement the common code for all operations type OpBase struct { OperationType OperationType `json:"type"` - Author identity.Interface `json:"author"` + Author_ identity.Interface `json:"author"` // TODO: part of the data model upgrade, this should eventually be a timestamp + lamport UnixTime int64 `json:"timestamp"` Metadata map[string]string `json:"metadata,omitempty"` @@ -95,15 +152,15 @@ type OpBase struct { func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase { return OpBase{ OperationType: opType, - Author: author, + Author_: author, UnixTime: unixTime, id: entity.UnsetId, } } -func (op *OpBase) UnmarshalJSON(data []byte) error { +func (base *OpBase) UnmarshalJSON(data []byte) error { // Compute the Id when loading the op from disk. - op.id = entity.DeriveId(data) + base.id = entity.DeriveId(data) aux := struct { OperationType OperationType `json:"type"` @@ -122,39 +179,43 @@ func (op *OpBase) UnmarshalJSON(data []byte) error { return err } - op.OperationType = aux.OperationType - op.Author = author - op.UnixTime = aux.UnixTime - op.Metadata = aux.Metadata + base.OperationType = aux.OperationType + base.Author_ = author + base.UnixTime = aux.UnixTime + base.Metadata = aux.Metadata return nil } +func (base *OpBase) Type() OperationType { + return base.OperationType +} + // Time return the time when the operation was added -func (op *OpBase) Time() time.Time { - return time.Unix(op.UnixTime, 0) +func (base *OpBase) Time() time.Time { + return time.Unix(base.UnixTime, 0) } // GetFiles return the files needed by this operation -func (op *OpBase) GetFiles() []repository.Hash { +func (base *OpBase) GetFiles() []repository.Hash { return nil } // Validate check the OpBase for errors -func opBaseValidate(op Operation, opType OperationType) error { - if op.base().OperationType != opType { - return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, op.base().OperationType) +func (base *OpBase) Validate(op Operation, opType OperationType) error { + if base.OperationType != opType { + return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, base.OperationType) } if op.Time().Unix() == 0 { return fmt.Errorf("time not set") } - if op.base().Author == nil { + if base.Author_ == nil { return fmt.Errorf("author not set") } - if err := op.base().Author.Validate(); err != nil { + if err := op.Author().Validate(); err != nil { return errors.Wrap(err, "author") } @@ -168,46 +229,55 @@ func opBaseValidate(op Operation, opType OperationType) error { } // SetMetadata store arbitrary metadata about the operation -func (op *OpBase) SetMetadata(key string, value string) { - if op.Metadata == nil { - op.Metadata = make(map[string]string) +func (base *OpBase) SetMetadata(key string, value string) { + if base.Metadata == nil { + base.Metadata = make(map[string]string) } - op.Metadata[key] = value - op.id = entity.UnsetId + base.Metadata[key] = value + base.id = entity.UnsetId } // GetMetadata retrieve arbitrary metadata about the operation -func (op *OpBase) GetMetadata(key string) (string, bool) { - val, ok := op.Metadata[key] +func (base *OpBase) GetMetadata(key string) (string, bool) { + val, ok := base.Metadata[key] if ok { return val, true } // extraMetadata can't replace the original operations value if any - val, ok = op.extraMetadata[key] + val, ok = base.extraMetadata[key] return val, ok } // AllMetadata return all metadata for this operation -func (op *OpBase) AllMetadata() map[string]string { +func (base *OpBase) AllMetadata() map[string]string { result := make(map[string]string) - for key, val := range op.extraMetadata { + for key, val := range base.extraMetadata { result[key] = val } // Original metadata take precedence - for key, val := range op.Metadata { + for key, val := range base.Metadata { result[key] = val } return result } -// GetAuthor return author identity -func (op *OpBase) GetAuthor() identity.Interface { - return op.Author +func (base *OpBase) setExtraMetadataImmutable(key string, value string) { + if base.extraMetadata == nil { + base.extraMetadata = make(map[string]string) + } + if _, exist := base.extraMetadata[key]; !exist { + base.extraMetadata[key] = value + } +} + +// Author return author identity +func (base *OpBase) Author() identity.Interface { + return base.Author_ } diff --git a/bug/operation_iterator.go b/bug/operation_iterator.go deleted file mode 100644 index f42b1776a71b608915490e1675017458acbee0c3..0000000000000000000000000000000000000000 --- a/bug/operation_iterator.go +++ /dev/null @@ -1,72 +0,0 @@ -package bug - -type OperationIterator struct { - bug *Bug - packIndex int - opIndex int -} - -func NewOperationIterator(bug Interface) *OperationIterator { - return &OperationIterator{ - bug: bugFromInterface(bug), - packIndex: 0, - opIndex: -1, - } -} - -func (it *OperationIterator) Next() bool { - // Special case of the staging area - if it.packIndex == len(it.bug.packs) { - pack := it.bug.staging - it.opIndex++ - return it.opIndex < len(pack.Operations) - } - - if it.packIndex >= len(it.bug.packs) { - return false - } - - pack := it.bug.packs[it.packIndex] - - it.opIndex++ - - if it.opIndex < len(pack.Operations) { - return true - } - - // Note: this iterator doesn't handle the empty pack case - it.opIndex = 0 - it.packIndex++ - - // Special case of the non-empty staging area - if it.packIndex == len(it.bug.packs) && len(it.bug.staging.Operations) > 0 { - return true - } - - return it.packIndex < len(it.bug.packs) -} - -func (it *OperationIterator) Value() Operation { - // Special case of the staging area - if it.packIndex == len(it.bug.packs) { - pack := it.bug.staging - - if it.opIndex >= len(pack.Operations) { - panic("Iterator is not valid anymore") - } - - return pack.Operations[it.opIndex] - } - - if it.packIndex >= len(it.bug.packs) { - panic("Iterator is not valid anymore") - } - - pack := it.bug.packs[it.packIndex] - - if it.opIndex >= len(pack.Operations) { - panic("Iterator is not valid anymore") - } - - return pack.Operations[it.opIndex] -} diff --git a/bug/operation_iterator_test.go b/bug/operation_iterator_test.go deleted file mode 100644 index 81d87a5fd3de0faf52e77f5e7a6a11c12358a4e2..0000000000000000000000000000000000000000 --- a/bug/operation_iterator_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package bug - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/repository" -) - -func ExampleOperationIterator() { - b := NewBug() - - // add operations - - it := NewOperationIterator(b) - - for it.Next() { - // do something with each operations - _ = it.Value() - } -} - -func TestOpIterator(t *testing.T) { - repo := repository.NewMockRepo() - - rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = rene.Commit(repo) - require.NoError(t, err) - - unix := time.Now().Unix() - - createOp := NewCreateOp(rene, unix, "title", "message", nil) - addCommentOp := NewAddCommentOp(rene, unix, "message2", nil) - setStatusOp := NewSetStatusOp(rene, unix, ClosedStatus) - labelChangeOp := NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"}) - - var i int - genTitleOp := func() Operation { - i++ - return NewSetTitleOp(rene, unix, fmt.Sprintf("title%d", i), "") - } - - bug1 := NewBug() - - // first pack - bug1.Append(createOp) - bug1.Append(addCommentOp) - bug1.Append(setStatusOp) - bug1.Append(labelChangeOp) - err = bug1.Commit(repo) - require.NoError(t, err) - - // second pack - bug1.Append(genTitleOp()) - bug1.Append(genTitleOp()) - bug1.Append(genTitleOp()) - err = bug1.Commit(repo) - require.NoError(t, err) - - // staging - bug1.Append(genTitleOp()) - bug1.Append(genTitleOp()) - bug1.Append(genTitleOp()) - - it := NewOperationIterator(bug1) - - counter := 0 - for it.Next() { - _ = it.Value() - counter++ - } - - require.Equal(t, 10, counter) -} diff --git a/bug/operation_pack.go b/bug/operation_pack.go deleted file mode 100644 index 74d15f50888b41aa8e8583fb18d591231590e6b2..0000000000000000000000000000000000000000 --- a/bug/operation_pack.go +++ /dev/null @@ -1,187 +0,0 @@ -package bug - -import ( - "encoding/json" - "fmt" - - "github.com/pkg/errors" - - "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/repository" -) - -// 1: original format -// 2: no more legacy identities -// 3: Ids are generated from the create operation serialized data instead of from the first git commit -const formatVersion = 3 - -// OperationPack represent an ordered set of operation to apply -// to a Bug. These operations are stored in a single Git commit. -// -// These commits will be linked together in a linear chain of commits -// inside Git to form the complete ordered chain of operation to -// apply to get the final state of the Bug -type OperationPack struct { - Operations []Operation - - // Private field so not serialized - commitHash repository.Hash -} - -func (opp *OperationPack) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - Version uint `json:"version"` - Operations []Operation `json:"ops"` - }{ - Version: formatVersion, - Operations: opp.Operations, - }) -} - -func (opp *OperationPack) UnmarshalJSON(data []byte) error { - aux := struct { - Version uint `json:"version"` - Operations []json.RawMessage `json:"ops"` - }{} - - if err := json.Unmarshal(data, &aux); err != nil { - return err - } - - if aux.Version < formatVersion { - return entity.NewErrOldFormatVersion(aux.Version) - } - if aux.Version > formatVersion { - return entity.NewErrNewFormatVersion(aux.Version) - } - - for _, raw := range aux.Operations { - var t struct { - OperationType OperationType `json:"type"` - } - - if err := json.Unmarshal(raw, &t); err != nil { - return err - } - - // delegate to specialized unmarshal function - op, err := opp.unmarshalOp(raw, t.OperationType) - if err != nil { - return err - } - - opp.Operations = append(opp.Operations, op) - } - - return nil -} - -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 EditCommentOp: - op := &EditCommentOperation{} - err := json.Unmarshal(raw, &op) - return op, err - case LabelChangeOp: - op := &LabelChangeOperation{} - err := json.Unmarshal(raw, &op) - return op, err - case NoOpOp: - op := &NoOpOperation{} - err := json.Unmarshal(raw, &op) - return op, err - case SetMetadataOp: - op := &SetMetadataOperation{} - err := json.Unmarshal(raw, &op) - return op, err - 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: - return nil, fmt.Errorf("unknown operation type %v", _type) - } -} - -// Append a new operation to the pack -func (opp *OperationPack) Append(op Operation) { - opp.Operations = append(opp.Operations, op) -} - -// IsEmpty tell if the OperationPack is empty -func (opp *OperationPack) IsEmpty() bool { - return len(opp.Operations) == 0 -} - -// IsValid tell if the OperationPack is considered valid -func (opp *OperationPack) Validate() error { - if opp.IsEmpty() { - return fmt.Errorf("empty") - } - - for _, op := range opp.Operations { - if err := op.Validate(); err != nil { - return errors.Wrap(err, "op") - } - } - - return nil -} - -// Write will serialize and store the OperationPack as a git blob and return -// its hash -func (opp *OperationPack) Write(repo repository.ClockedRepo) (repository.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 - // TODO: this might be downgraded to "make sure it exist in git" but then, what make - // sure no data is lost on identities ? - for _, op := range opp.Operations { - if op.base().Author.NeedCommit() { - return "", fmt.Errorf("identity need commmit") - } - } - - data, err := json.Marshal(opp) - if err != nil { - return "", err - } - - hash, err := repo.StoreData(data) - if err != nil { - return "", err - } - - return hash, nil -} - -// Make a deep copy -func (opp *OperationPack) Clone() OperationPack { - - clone := OperationPack{ - Operations: make([]Operation, len(opp.Operations)), - commitHash: opp.commitHash, - } - - for i, op := range opp.Operations { - clone.Operations[i] = op - } - - return clone -} diff --git a/bug/operation_pack_test.go b/bug/operation_pack_test.go deleted file mode 100644 index 02d72f0f9f847d774f0a98658bdc3374f836c7c5..0000000000000000000000000000000000000000 --- a/bug/operation_pack_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package bug - -import ( - "encoding/json" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/repository" -) - -func TestOperationPackSerialize(t *testing.T) { - opp := &OperationPack{} - - repo := repository.NewMockRepo() - - rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - - 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 := NewSetTitleOp(rene, time.Now().Unix(), "title3", "title2") - opMeta.SetMetadata("key", "value") - opp.Append(opMeta) - - require.Equal(t, 1, len(opMeta.Metadata)) - - opFile := NewAddCommentOp(rene, time.Now().Unix(), "message", []repository.Hash{ - "abcdef", - "ghijkl", - }) - opp.Append(opFile) - - require.Equal(t, 2, len(opFile.Files)) - - data, err := json.Marshal(opp) - require.NoError(t, err) - - var opp2 *OperationPack - err = json.Unmarshal(data, &opp2) - require.NoError(t, err) - - ensureIds(opp) - ensureAuthors(t, opp, opp2) - - require.Equal(t, opp, opp2) -} - -func ensureIds(opp *OperationPack) { - for _, op := range opp.Operations { - op.Id() - } -} - -func ensureAuthors(t *testing.T, opp1 *OperationPack, opp2 *OperationPack) { - require.Equal(t, len(opp1.Operations), len(opp2.Operations)) - for i := 0; i < len(opp1.Operations); i++ { - op1 := opp1.Operations[i] - op2 := opp2.Operations[i] - - // ensure we have equivalent authors (IdentityStub vs Identity) then - // enforce equality - require.Equal(t, op1.base().Author.Id(), op2.base().Author.Id()) - op1.base().Author = op2.base().Author - } -} diff --git a/bug/operation_test.go b/bug/operation_test.go index f66938aded94d29f8941a267fa7b7c77a382fb2d..619f2b43785060238cd7a41aca14c0c9b23133f8 100644 --- a/bug/operation_test.go +++ b/bug/operation_test.go @@ -45,7 +45,7 @@ func TestValidate(t *testing.T) { NewSetStatusOp(makeIdentity(t, "René \nDescartes", "rene@descartes.fr"), unix, ClosedStatus), NewSetStatusOp(makeIdentity(t, "René Descartes", "rene@\ndescartes.fr"), unix, ClosedStatus), &CreateOperation{OpBase: OpBase{ - Author: rene, + Author_: rene, UnixTime: 0, OperationType: CreateOp, }, @@ -121,7 +121,7 @@ func TestID(t *testing.T) { require.NoError(t, id2.Validate()) require.Equal(t, id1, id2) - b2, err := ReadLocal(repo, b.Id()) + b2, err := Read(repo, b.Id()) require.NoError(t, err) op3 := b2.FirstOp() diff --git a/bug/sorting.go b/bug/sorting.go index d1c370d300d88379e50e4f2d403f6233bd064d99..2e64b92dcfb672a5499e55b32069d761c4c862c6 100644 --- a/bug/sorting.go +++ b/bug/sorting.go @@ -7,11 +7,11 @@ func (b BugsByCreationTime) Len() int { } func (b BugsByCreationTime) Less(i, j int) bool { - if b[i].createTime < b[j].createTime { + if b[i].CreateLamportTime() < b[j].CreateLamportTime() { return true } - if b[i].createTime > b[j].createTime { + if b[i].CreateLamportTime() > b[j].CreateLamportTime() { return false } @@ -35,11 +35,11 @@ func (b BugsByEditTime) Len() int { } func (b BugsByEditTime) Less(i, j int) bool { - if b[i].editTime < b[j].editTime { + if b[i].EditLamportTime() < b[j].EditLamportTime() { return true } - if b[i].editTime > b[j].editTime { + if b[i].EditLamportTime() > b[j].EditLamportTime() { return false } diff --git a/bug/with_snapshot.go b/bug/with_snapshot.go index 41192d3965964cb5831b78c689a2ea57f4d579e3..9b706d61df8de9ec9b15eb141793f6d420bb0306 100644 --- a/bug/with_snapshot.go +++ b/bug/with_snapshot.go @@ -50,9 +50,3 @@ func (b *WithSnapshot) Commit(repo repository.ClockedRepo) error { b.snap.id = b.Bug.Id() return nil } - -// Merge intercept Bug.Merge() and clear the snapshot -func (b *WithSnapshot) Merge(repo repository.Repo, other Interface) (bool, error) { - b.snap = nil - return b.Bug.Merge(repo, other) -} diff --git a/cache/bug_cache.go b/cache/bug_cache.go index ca526f7b316f49f477c38a8d1573b74e81f47fbb..bbe9830f97071c586a2ba918470c9f15caf3eccb 100644 --- a/cache/bug_cache.go +++ b/cache/bug_cache.go @@ -51,9 +51,7 @@ func (c *BugCache) ResolveOperationWithMetadata(key string, value string) (entit // preallocate but empty matching := make([]entity.Id, 0, 5) - it := bug.NewOperationIterator(c.bug) - for it.Next() { - op := it.Value() + for _, op := range c.bug.Operations() { opValue, ok := op.GetMetadata(key) if ok && value == opValue { matching = append(matching, op.Id()) diff --git a/cache/bug_excerpt.go b/cache/bug_excerpt.go index 6a9e7f75a92f1feb9fcd5b86a8c9f874237c3c5d..152bdacfed0cf87182774f8b7bc05ca4ab3bfa1f 100644 --- a/cache/bug_excerpt.go +++ b/cache/bug_excerpt.go @@ -87,7 +87,7 @@ func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt { } switch snap.Author.(type) { - case *identity.Identity, *IdentityCache: + case *identity.Identity, *identity.IdentityStub, *IdentityCache: e.AuthorId = snap.Author.Id() default: panic("unhandled identity type") diff --git a/cache/repo_cache.go b/cache/repo_cache.go index ab3e1bcb6b320085b0605f0cc0f49a902f692a27..58022bdad0ffee890f01531765fd0de986fded3e 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -195,7 +195,7 @@ func (c *RepoCache) buildCache() error { c.bugExcerpts = make(map[entity.Id]*BugExcerpt) - allBugs := bug.ReadAllLocal(c.repo) + allBugs := bug.ReadAll(c.repo) // wipe the index just to be sure err := c.repo.ClearBleveIndex("bug") diff --git a/cache/repo_cache_bug.go b/cache/repo_cache_bug.go index 9f011c0450d66c15ba2fe16df91dd15f80527304..c05f30cfd94b4b507d5e6b3ca42529e4699f025e 100644 --- a/cache/repo_cache_bug.go +++ b/cache/repo_cache_bug.go @@ -151,7 +151,7 @@ func (c *RepoCache) ResolveBug(id entity.Id) (*BugCache, error) { } c.muBug.RUnlock() - b, err := bug.ReadLocalWithResolver(c.repo, newIdentityCacheResolver(c), id) + b, err := bug.ReadWithResolver(c.repo, newIdentityCacheResolver(c), id) if err != nil { return nil, err } diff --git a/cache/repo_cache_common.go b/cache/repo_cache_common.go index 5dc19d22ed83082d8dd0445aacd7e06c9721b511..e23315f921c4f0665ddcc04fcb1a3975ae77e643 100644 --- a/cache/repo_cache_common.go +++ b/cache/repo_cache_common.go @@ -95,6 +95,12 @@ func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult { go func() { defer close(out) + author, err := c.GetUserIdentity() + if err != nil { + out <- entity.NewMergeError(err, "") + return + } + results := identity.MergeAll(c.repo, remote) for result := range results { out <- result @@ -112,7 +118,7 @@ func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult { } } - results = bug.MergeAll(c.repo, remote) + results = bug.MergeAll(c.repo, remote, author) for result := range results { out <- result @@ -130,11 +136,10 @@ func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult { } } - err := c.write() - - // No easy way out here .. + err = c.write() if err != nil { - panic(err) + out <- entity.NewMergeError(err, "") + return } }() diff --git a/cache/repo_cache_test.go b/cache/repo_cache_test.go index 9cdd584df6161c16053011f2b9055a4497aab76b..a85fde66be5bbb9efbfe41cc83f963061b957c84 100644 --- a/cache/repo_cache_test.go +++ b/cache/repo_cache_test.go @@ -123,6 +123,10 @@ func TestPushPull(t *testing.T) { require.NoError(t, err) err = cacheA.SetUserIdentity(reneA) require.NoError(t, err) + isaacB, err := cacheB.NewIdentity("Isaac Newton", "isaac@newton.uk") + require.NoError(t, err) + err = cacheB.SetUserIdentity(isaacB) + require.NoError(t, err) // distribute the identity _, err = cacheA.Push("origin") diff --git a/entity/TODO b/entity/TODO deleted file mode 100644 index 9f33dd09cae2672ca6e947d8118fdf751f1eebe0..0000000000000000000000000000000000000000 --- a/entity/TODO +++ /dev/null @@ -1,9 +0,0 @@ -- is the pack Lamport clock really useful vs only topological sort? - - topological order is enforced on the clocks, so what's the point? - - is EditTime equivalent to PackTime? no, avoid the gaps. Is it better? - --> PackTime is contained within a bug and might avoid extreme reordering? -- how to do commit signature? -- how to avoid id collision between Operations? -- write tests for actions -- migrate Bug to the new structure -- migrate Identity to the new structure? \ No newline at end of file diff --git a/entity/merge.go b/entity/merge.go index 1b68b4de284dc8a1952c46e946861a87fc6bfdc7..0661b7fc16112c435cabffa73a1813f55663b3b3 100644 --- a/entity/merge.go +++ b/entity/merge.go @@ -42,13 +42,15 @@ func (mr MergeResult) String() string { case MergeStatusNothing: return "nothing to do" case MergeStatusError: - return fmt.Sprintf("merge error on %s: %s", mr.Id, mr.Err.Error()) + if mr.Id != "" { + return fmt.Sprintf("merge error on %s: %s", mr.Id, mr.Err.Error()) + } + return fmt.Sprintf("merge error: %s", mr.Err.Error()) default: panic("unknown merge status") } } -// TODO: Interface --> *Entity ? func NewMergeNewStatus(id Id, entity Interface) MergeResult { return MergeResult{ Id: id, diff --git a/go.sum b/go.sum index 9d0a8c825ce06b1577af362396ef22f3ea585eb5..e316fb66e1d7e9b328636cb6515d90f66b752cf0 100644 --- a/go.sum +++ b/go.sum @@ -575,6 +575,7 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/misc/random_bugs/create_random_bugs.go b/misc/random_bugs/create_random_bugs.go index 3d93135ece4bf3b78e28c2cdbd72234525f1669f..a69918f4379fa611eeb0554a45f12dc57a5b9567 100644 --- a/misc/random_bugs/create_random_bugs.go +++ b/misc/random_bugs/create_random_bugs.go @@ -111,52 +111,6 @@ func generateRandomBugsWithSeed(opts Options, seed int64) []*bug.Bug { return result } -func GenerateRandomOperationPacks(packNumber int, opNumber int) []*bug.OperationPack { - return GenerateRandomOperationPacksWithSeed(packNumber, opNumber, time.Now().UnixNano()) -} - -func GenerateRandomOperationPacksWithSeed(packNumber int, opNumber int, seed int64) []*bug.OperationPack { - // Note: this is a bit crude, only generate a Create + Comments - - panic("this piece of code needs to be updated to make sure that the identities " + - "are properly commit before usage. That is, generateRandomPersons() need to be called.") - - rand.Seed(seed) - fake.Seed(seed) - - result := make([]*bug.OperationPack, packNumber) - - for i := 0; i < packNumber; i++ { - opp := &bug.OperationPack{} - - var op bug.Operation - - op = bug.NewCreateOp( - randomPerson(), - time.Now().Unix(), - fake.Sentence(), - paragraphs(), - nil, - ) - - opp.Append(op) - - for j := 0; j < opNumber-1; j++ { - op = bug.NewAddCommentOp( - randomPerson(), - time.Now().Unix(), - paragraphs(), - nil, - ) - opp.Append(op) - } - - result[i] = opp - } - - return result -} - func person(repo repository.RepoClock) (*identity.Identity, error) { return identity.NewIdentity(repo, fake.FullName(), fake.EmailAddress()) } diff --git a/tests/read_bugs_test.go b/tests/read_bugs_test.go index 53b84fd5402b471b7d3b278d1854fb257ec0fa02..b198368952b66aed6df0bb2ae4bc45479c5aee67 100644 --- a/tests/read_bugs_test.go +++ b/tests/read_bugs_test.go @@ -14,7 +14,7 @@ func TestReadBugs(t *testing.T) { random_bugs.FillRepoWithSeed(repo, 15, 42) - bugs := bug.ReadAllLocal(repo) + bugs := bug.ReadAll(repo) for b := range bugs { if b.Err != nil { t.Fatal(b.Err) @@ -30,7 +30,7 @@ func benchmarkReadBugs(bugNumber int, t *testing.B) { t.ResetTimer() for n := 0; n < t.N; n++ { - bugs := bug.ReadAllLocal(repo) + bugs := bug.ReadAll(repo) for b := range bugs { if b.Err != nil { t.Fatal(b.Err) From 4b9862e239deb939c87be2b02970a7bfe2996e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 14 Feb 2021 12:13:33 +0100 Subject: [PATCH 34/42] entity: make sure merge commit don't have operations --- entity/dag/entity.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/entity/dag/entity.go b/entity/dag/entity.go index 196280a8b287c57da6defcd247246166e6ce7b8a..c43685146664ddfec9c24dae12a1251ec22cf895 100644 --- a/entity/dag/entity.go +++ b/entity/dag/entity.go @@ -148,6 +148,10 @@ func read(def Definition, repo repository.ClockedRepo, resolver identity.Resolve return nil, err } + if isMerge && len(opp.Operations) > 0 { + return nil, fmt.Errorf("merge commit cannot have operations") + } + // Check that the create lamport clock is set (not checked in Validate() as it's optional) if isFirstCommit && opp.CreateTime <= 0 { return nil, fmt.Errorf("creation lamport time not set") From d0d7be8db010e2c68c98d0a34387e4fac0c4d6ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 14 Feb 2021 12:14:03 +0100 Subject: [PATCH 35/42] minor cleanups --- .gitignore | 1 + bug/op_edit_comment.go | 2 +- bug/op_label_change.go | 2 +- bug/op_noop.go | 2 +- bug/op_set_metadata.go | 2 +- bug/op_set_status.go | 2 +- bug/op_set_title.go | 2 +- doc/man/git-bug-comment-edit.1 | 2 +- doc/md/git-bug_comment_edit.md | 2 +- entity/err.go | 2 +- 10 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 012dadcd0ddb028876efa40314b1ef46ca4bd097..be1fc3f1c79365fda767169a31550b13d22bf273 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ git-bug dist coverage.txt .idea/ +.git_bak* diff --git a/bug/op_edit_comment.go b/bug/op_edit_comment.go index f9e30e62b47ba2478c1afd6abcc712bff2227ef6..e08aeaad9aa05082b2c12802a5d3de71aa6419b8 100644 --- a/bug/op_edit_comment.go +++ b/bug/op_edit_comment.go @@ -96,7 +96,7 @@ func (op *EditCommentOperation) Validate() error { return nil } -// UnmarshalJSON is a two step JSON unmarshaling +// UnmarshalJSON is a two step JSON unmarshalling // This workaround is necessary to avoid the inner OpBase.MarshalJSON // overriding the outer op's MarshalJSON func (op *EditCommentOperation) UnmarshalJSON(data []byte) error { diff --git a/bug/op_label_change.go b/bug/op_label_change.go index 5e51534d6f2e6c1ecfce624a30ea2f3fef5e8d47..d682fe54d0e0932d8b7cd77bc64b7d201f417f99 100644 --- a/bug/op_label_change.go +++ b/bug/op_label_change.go @@ -95,7 +95,7 @@ func (op *LabelChangeOperation) Validate() error { return nil } -// UnmarshalJSON is a two step JSON unmarshaling +// UnmarshalJSON is a two step JSON unmarshalling // This workaround is necessary to avoid the inner OpBase.MarshalJSON // overriding the outer op's MarshalJSON func (op *LabelChangeOperation) UnmarshalJSON(data []byte) error { diff --git a/bug/op_noop.go b/bug/op_noop.go index d59666ae38dacf50cbc020cc5b7f27ca229ae120..81efdb257c86b5f95e10350bd98b1d7908582ef4 100644 --- a/bug/op_noop.go +++ b/bug/op_noop.go @@ -31,7 +31,7 @@ func (op *NoOpOperation) Validate() error { return op.OpBase.Validate(op, NoOpOp) } -// UnmarshalJSON is a two step JSON unmarshaling +// UnmarshalJSON is a two step JSON unmarshalling // This workaround is necessary to avoid the inner OpBase.MarshalJSON // overriding the outer op's MarshalJSON func (op *NoOpOperation) UnmarshalJSON(data []byte) error { diff --git a/bug/op_set_metadata.go b/bug/op_set_metadata.go index c4988b84f67163a648b98b6445c0cc265486ad03..4e596728cb7c6742a5f6b8413c6df18213f434d8 100644 --- a/bug/op_set_metadata.go +++ b/bug/op_set_metadata.go @@ -49,7 +49,7 @@ func (op *SetMetadataOperation) Validate() error { return nil } -// UnmarshalJSON is a two step JSON unmarshaling +// UnmarshalJSON is a two step JSON unmarshalling // This workaround is necessary to avoid the inner OpBase.MarshalJSON // overriding the outer op's MarshalJSON func (op *SetMetadataOperation) UnmarshalJSON(data []byte) error { diff --git a/bug/op_set_status.go b/bug/op_set_status.go index fcf33f8c55d15791f1574ec08864e2c6eb7da0c8..ca8c434a9bf912b7e295f44f6cd1e2fecf697798 100644 --- a/bug/op_set_status.go +++ b/bug/op_set_status.go @@ -51,7 +51,7 @@ func (op *SetStatusOperation) Validate() error { return nil } -// UnmarshalJSON is a two step JSON unmarshaling +// UnmarshalJSON is a two step JSON unmarshalling // This workaround is necessary to avoid the inner OpBase.MarshalJSON // overriding the outer op's MarshalJSON func (op *SetStatusOperation) UnmarshalJSON(data []byte) error { diff --git a/bug/op_set_title.go b/bug/op_set_title.go index 7fd7c20d16039c22322d33cbeceee16a5e0c9598..899b4fa3796ff7993697f30fcbb057aab506f7ef 100644 --- a/bug/op_set_title.go +++ b/bug/op_set_title.go @@ -71,7 +71,7 @@ func (op *SetTitleOperation) Validate() error { return nil } -// UnmarshalJSON is a two step JSON unmarshaling +// UnmarshalJSON is a two step JSON unmarshalling // This workaround is necessary to avoid the inner OpBase.MarshalJSON // overriding the outer op's MarshalJSON func (op *SetTitleOperation) UnmarshalJSON(data []byte) error { diff --git a/doc/man/git-bug-comment-edit.1 b/doc/man/git-bug-comment-edit.1 index e3cb2daf0347848d58aa3dbaf473737eaf325a82..741743a679497e08f1fba54512af9b20b050b7cf 100644 --- a/doc/man/git-bug-comment-edit.1 +++ b/doc/man/git-bug-comment-edit.1 @@ -8,7 +8,7 @@ git\-bug\-comment\-edit \- Edit an existing comment on a bug. .SH SYNOPSIS .PP -\fBgit\-bug comment edit [flags]\fP +\fBgit\-bug comment edit [COMMENT\_ID] [flags]\fP .SH DESCRIPTION diff --git a/doc/md/git-bug_comment_edit.md b/doc/md/git-bug_comment_edit.md index 2546dff16df46b4048d4db12ebf39ca31741e7fe..26571927744c9a1582151110779bf70ceec2e02e 100644 --- a/doc/md/git-bug_comment_edit.md +++ b/doc/md/git-bug_comment_edit.md @@ -3,7 +3,7 @@ Edit an existing comment on a bug. ``` -git-bug comment edit [flags] +git-bug comment edit [COMMENT_ID] [flags] ``` ### Options diff --git a/entity/err.go b/entity/err.go index 9222e4dac460118d074a524b0c37c99440af80d3..9f7f5a1a18dfb9b0db01a78317bc710e3d7ed8c5 100644 --- a/entity/err.go +++ b/entity/err.go @@ -52,7 +52,7 @@ func NewErrUnknowFormat(expected uint) *ErrInvalidFormat { func (e ErrInvalidFormat) Error() string { if e.version == 0 { - return fmt.Sprintf("unreadable data, expected format version %v", e.expected) + return fmt.Sprintf("unreadable data, you likely have an outdated repository format, please use https://github.com/MichaelMure/git-bug-migration to upgrade to format version %v", e.expected) } if e.version < e.expected { return fmt.Sprintf("outdated repository format %v, please use https://github.com/MichaelMure/git-bug-migration to upgrade to format version %v", e.version, e.expected) From 1ced77af1a4bdbaa212a74bf0c56b2b81cdc5bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 14 Feb 2021 12:24:40 +0100 Subject: [PATCH 36/42] fix merge --- entity/dag/operation_pack.go | 2 +- entity/err.go | 2 +- go.mod | 2 -- go.sum | 44 +++--------------------------------- identity/version.go | 7 ++---- 5 files changed, 7 insertions(+), 50 deletions(-) diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go index 00cf2557878609cfc9ed9e7b6779bdfa6267e034..a436fd332f56db0ffc6b57e7b60d2b0e5c526efe 100644 --- a/entity/dag/operation_pack.go +++ b/entity/dag/operation_pack.go @@ -188,7 +188,7 @@ func readOperationPack(def Definition, repo repository.RepoData, resolver identi } } if version == 0 { - return nil, entity.NewErrUnknowFormat(def.FormatVersion) + return nil, entity.NewErrUnknownFormat(def.FormatVersion) } if version != def.FormatVersion { return nil, entity.NewErrInvalidFormat(version, def.FormatVersion) diff --git a/entity/err.go b/entity/err.go index 9f7f5a1a18dfb9b0db01a78317bc710e3d7ed8c5..408e27b4b38c0955654a9470d73105a75fb31c57 100644 --- a/entity/err.go +++ b/entity/err.go @@ -43,7 +43,7 @@ func NewErrInvalidFormat(version uint, expected uint) *ErrInvalidFormat { } } -func NewErrUnknowFormat(expected uint) *ErrInvalidFormat { +func NewErrUnknownFormat(expected uint) *ErrInvalidFormat { return &ErrInvalidFormat{ version: 0, expected: expected, diff --git a/go.mod b/go.mod index fa75caaf8189c7834c317762d0b9bcbd6a4d9e4c..6057a63c432a31f2d094c9a6f599ed4384ab1e32 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/MichaelMure/go-term-text v0.2.10 github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195 github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986 - github.com/blang/semver v3.5.1+incompatible github.com/blevesearch/bleve v1.0.14 github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9 github.com/corpix/uarand v0.1.1 // indirect @@ -30,7 +29,6 @@ require ( github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e github.com/spf13/cobra v1.1.1 github.com/stretchr/testify v1.7.0 - github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect github.com/vektah/gqlparser v1.3.1 github.com/xanzy/go-gitlab v0.40.1 github.com/xanzy/ssh-agent v0.3.0 // indirect diff --git a/go.sum b/go.sum index e316fb66e1d7e9b328636cb6515d90f66b752cf0..14a9e11937ee19ba93c34d0c20d8aa132f37ac21 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,7 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= -github.com/MichaelMure/go-term-text v0.2.9 h1:jUxInT3rDhl4WoJgLnmMS3hR79zigyJS1TqKFDTI6xE= -github.com/MichaelMure/go-term-text v0.2.9/go.mod h1:2QSU/Nn2u41Tqoar+90RlYuhjngJPYgod7evnsYwkWc= +github.com/MichaelMure/go-term-text v0.2.10 h1:5OGpCDINh6V7KcZUtff+T2gtnIgbDdYmNlFSa5Cct1k= github.com/MichaelMure/go-term-text v0.2.10/go.mod h1:DrWFodEEZsSgK1PQY9dqTn+pw3zGeYDmVF5PA8ECZhs= github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= @@ -75,13 +74,6 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/blevesearch/bleve v1.0.10 h1:DxFXeC+faL+5LVTlljUDpP9eXj3mleiQem3DuSjepqQ= -github.com/blevesearch/bleve v1.0.10/go.mod h1:KHAOH5HuVGn9fo+dN5TkqcA1HcuOQ89goLWVWXZDl8w= -github.com/blevesearch/bleve v1.0.12/go.mod h1:G0ErXWdIrUSYZLPoMpS9Z3saTnTsk4ebhPsVv/+0nxk= -github.com/blevesearch/bleve v1.0.13 h1:NtqdA+2UL715y2/9Epg9Ie9uspNcilGMYNM+tT+HfAo= -github.com/blevesearch/bleve v1.0.13/go.mod h1:3y+16vR4Cwtis/bOGCt7r+CHKB2/ewizEqKBUycXomA= github.com/blevesearch/bleve v1.0.14 h1:Q8r+fHTt35jtGXJUM0ULwM3Tzg+MRfyai4ZkWDy2xO4= github.com/blevesearch/bleve v1.0.14/go.mod h1:e/LJTr+E7EaoVdkQZTfoz7dt4KoDNvDbLb8MSKuNTLQ= github.com/blevesearch/blevex v1.0.0 h1:pnilj2Qi3YSEGdWgLj1Pn9Io7ukfXPoQcpAI1Bv8n/o= @@ -95,45 +87,22 @@ github.com/blevesearch/segment v0.9.0 h1:5lG7yBCx98or7gK2cHMKPukPZ/31Kag7nONpoBt github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ= github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= -github.com/blevesearch/zap/v11 v11.0.10 h1:zJdl+cnxT0Yt2hA6meG+OIat3oSA4rERfrNX2CSchII= -github.com/blevesearch/zap/v11 v11.0.10/go.mod h1:BdqdgKy6u0Jgw/CqrMfP2Gue/EldcfvB/3eFzrzhIfw= -github.com/blevesearch/zap/v11 v11.0.12/go.mod h1:JLfFhc8DWP01zMG/6VwEY2eAnlJsTN1vDE4S0rC5Y78= -github.com/blevesearch/zap/v11 v11.0.13 h1:NDvmjAyeEQsBbPElubVPqrBtSDOftXYwxkHeZfflU4A= -github.com/blevesearch/zap/v11 v11.0.13/go.mod h1:qKkNigeXbxZwym02wsxoQpbme1DgAwTvRlT/beIGfTM= github.com/blevesearch/zap/v11 v11.0.14 h1:IrDAvtlzDylh6H2QCmS0OGcN9Hpf6mISJlfKjcwJs7k= github.com/blevesearch/zap/v11 v11.0.14 h1:IrDAvtlzDylh6H2QCmS0OGcN9Hpf6mISJlfKjcwJs7k= github.com/blevesearch/zap/v11 v11.0.14/go.mod h1:MUEZh6VHGXv1PKx3WnCbdP404LGG2IZVa/L66pyFwnY= github.com/blevesearch/zap/v11 v11.0.14/go.mod h1:MUEZh6VHGXv1PKx3WnCbdP404LGG2IZVa/L66pyFwnY= -github.com/blevesearch/zap/v12 v12.0.10 h1:T1/GXNBxC9eetfuMwCM5RLWXeharSMyAdNEdXVtBuHA= -github.com/blevesearch/zap/v12 v12.0.10/go.mod h1:QtKkjpmV/sVFEnKSaIWPXZJAaekL97TrTV3ImhNx+nw= -github.com/blevesearch/zap/v12 v12.0.12/go.mod h1:1HrB4hhPfI8u8x4SPYbluhb8xhflpPvvj8EcWImNnJY= -github.com/blevesearch/zap/v12 v12.0.13 h1:05Ebdmv2tRTUytypG4DlOIHLLw995DtVV0Zl3YwwDew= -github.com/blevesearch/zap/v12 v12.0.13/go.mod h1:0RTeU1uiLqsPoybUn6G/Zgy6ntyFySL3uWg89NgX3WU= github.com/blevesearch/zap/v12 v12.0.14 h1:2o9iRtl1xaRjsJ1xcqTyLX414qPAwykHNV7wNVmbp3w= github.com/blevesearch/zap/v12 v12.0.14 h1:2o9iRtl1xaRjsJ1xcqTyLX414qPAwykHNV7wNVmbp3w= github.com/blevesearch/zap/v12 v12.0.14/go.mod h1:rOnuZOiMKPQj18AEKEHJxuI14236tTQ1ZJz4PAnWlUg= github.com/blevesearch/zap/v12 v12.0.14/go.mod h1:rOnuZOiMKPQj18AEKEHJxuI14236tTQ1ZJz4PAnWlUg= -github.com/blevesearch/zap/v13 v13.0.2 h1:quhI5OVFX33dhPpUW+nLyXGpu7QT8qTgzu6qA/fRRXM= -github.com/blevesearch/zap/v13 v13.0.2/go.mod h1:/9QLKla8/8mloJvQQutPhB+tw6y35urvKeAFeun2JGA= -github.com/blevesearch/zap/v13 v13.0.4/go.mod h1:YdB7UuG7TBWu/1dz9e2SaLp1RKfFfdJx+ulIK5HR1bA= -github.com/blevesearch/zap/v13 v13.0.5 h1:+Gcwl95uei3MgBlJAddBFRv9gl+FMNcXpMa7BX3byJw= -github.com/blevesearch/zap/v13 v13.0.5/go.mod h1:HTfWECmzBN7BbdBxdEigpUsD6MOPFOO84tZ0z/g3CnE= github.com/blevesearch/zap/v13 v13.0.6 h1:r+VNSVImi9cBhTNNR+Kfl5uiGy8kIbb0JMz/h8r6+O4= github.com/blevesearch/zap/v13 v13.0.6 h1:r+VNSVImi9cBhTNNR+Kfl5uiGy8kIbb0JMz/h8r6+O4= github.com/blevesearch/zap/v13 v13.0.6/go.mod h1:L89gsjdRKGyGrRN6nCpIScCvvkyxvmeDCwZRcjjPCrw= github.com/blevesearch/zap/v13 v13.0.6/go.mod h1:L89gsjdRKGyGrRN6nCpIScCvvkyxvmeDCwZRcjjPCrw= -github.com/blevesearch/zap/v14 v14.0.1 h1:s8KeqX53Vc4eRaziHsnY2bYUE+8IktWqRL9W5H5VDMY= -github.com/blevesearch/zap/v14 v14.0.1/go.mod h1:Y+tUL9TypMca5+96m7iJb2lpcntETXSeDoI5BBX2tvY= -github.com/blevesearch/zap/v14 v14.0.3/go.mod h1:oObAhcDHw7p1ahiTCqhRkdxdl7UA8qpvX10pSgrTMHc= -github.com/blevesearch/zap/v14 v14.0.4 h1:BnWWkdgmPhK50J9dkBlQrWB4UDa22OMPIUzn1oXcXfY= -github.com/blevesearch/zap/v14 v14.0.4/go.mod h1:sTwuFoe1n/+VtaHNAjY3W5GzHZ5UxFkw1MZ82P/WKpA= github.com/blevesearch/zap/v14 v14.0.5 h1:NdcT+81Nvmp2zL+NhwSvGSLh7xNgGL8QRVZ67njR0NU= github.com/blevesearch/zap/v14 v14.0.5 h1:NdcT+81Nvmp2zL+NhwSvGSLh7xNgGL8QRVZ67njR0NU= github.com/blevesearch/zap/v14 v14.0.5/go.mod h1:bWe8S7tRrSBTIaZ6cLRbgNH4TUDaC9LZSpRGs85AsGY= github.com/blevesearch/zap/v14 v14.0.5/go.mod h1:bWe8S7tRrSBTIaZ6cLRbgNH4TUDaC9LZSpRGs85AsGY= -github.com/blevesearch/zap/v15 v15.0.1/go.mod h1:ho0frqAex2ktT9cYFAxQpoQXsxb/KEfdjpx4s49rf/M= -github.com/blevesearch/zap/v15 v15.0.2 h1:7wV4ksnKzBibLaWBolzbxngxdVAUmF7HJ+gMOqkzsdQ= -github.com/blevesearch/zap/v15 v15.0.2/go.mod h1:nfycXPgfbio8l+ZSkXUkhSNjTpp57jZ0/MKa6TigWvM= github.com/blevesearch/zap/v15 v15.0.3 h1:Ylj8Oe+mo0P25tr9iLPp33lN6d4qcztGjaIsP51UxaY= github.com/blevesearch/zap/v15 v15.0.3/go.mod h1:iuwQrImsh1WjWJ0Ue2kBqY83a0rFtJTqfa9fp1rbVVU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -378,8 +347,7 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -439,6 +407,7 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -489,9 +458,6 @@ github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= @@ -510,8 +476,6 @@ github.com/vektah/gqlparser v1.3.1 h1:8b0IcD3qZKWJQHSzynbDlrtP3IxVydZ2DZepCGofqf github.com/vektah/gqlparser v1.3.1/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74= github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= -github.com/xanzy/go-gitlab v0.39.0 h1:7aiZ03fJfCdqoHFhsZq/SoVYp2lR91hfYWmiXLOU5Qo= -github.com/xanzy/go-gitlab v0.39.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= github.com/xanzy/go-gitlab v0.40.1 h1:jHueLh5Inzv20TL5Yki+CaLmyvtw3Yq7blbWx7GmglQ= github.com/xanzy/go-gitlab v0.40.1/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= @@ -674,8 +638,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/identity/version.go b/identity/version.go index ce5cc7d615443038bca0fbf966c862b61ca999ae..cbd56a983aaa6e3d5f3eaaede8d6112a81291d5a 100644 --- a/identity/version.go +++ b/identity/version.go @@ -159,11 +159,8 @@ func (v *version) UnmarshalJSON(data []byte) error { return err } - if aux.FormatVersion < formatVersion { - return entity.NewErrOldFormatVersion(aux.FormatVersion) - } - if aux.FormatVersion > formatVersion { - return entity.NewErrNewFormatVersion(aux.FormatVersion) + if aux.FormatVersion != formatVersion { + return entity.NewErrInvalidFormat(aux.FormatVersion, formatVersion) } v.id = entity.DeriveId(data) From 45e540c178533ef9aab01b1c3e782bc63061e313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 14 Feb 2021 12:38:09 +0100 Subject: [PATCH 37/42] bug: wrap dag.Entity into a full Bug in MergeAll --- bug/bug_actions.go | 21 ++++++++++++++++++++- cache/repo_cache_test.go | 2 +- entity/dag/entity_actions_test.go | 2 +- identity/identity_actions_test.go | 2 +- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/bug/bug_actions.go b/bug/bug_actions.go index 6ca5ffd776a0836d72a656363c931f686c0244a4..420fb08af5c96230fc7ba199156d9a6337fd288f 100644 --- a/bug/bug_actions.go +++ b/bug/bug_actions.go @@ -49,7 +49,26 @@ func MergeAll(repo repository.ClockedRepo, remote string, author identity.Interf // invalidate entities if necessary. identityResolver := identity.NewSimpleResolver(repo) - return dag.MergeAll(def, repo, identityResolver, remote, author) + out := make(chan entity.MergeResult) + + go func() { + defer close(out) + + results := dag.MergeAll(def, repo, identityResolver, remote, author) + + // wrap the dag.Entity into a complete Bug + for result := range results { + result := result + if result.Entity != nil { + result.Entity = &Bug{ + Entity: result.Entity.(*dag.Entity), + } + } + out <- result + } + }() + + return out } // RemoveBug will remove a local bug from its entity.Id diff --git a/cache/repo_cache_test.go b/cache/repo_cache_test.go index a85fde66be5bbb9efbfe41cc83f963061b957c84..35dc4ffdfe7cd23306b053b5d46fa5fb8bca7eb7 100644 --- a/cache/repo_cache_test.go +++ b/cache/repo_cache_test.go @@ -108,7 +108,7 @@ func TestCache(t *testing.T) { require.NoError(t, err) } -func TestPushPull(t *testing.T) { +func TestCachePushPull(t *testing.T) { repoA, repoB, remote := repository.SetupGoGitReposAndRemote() defer repository.CleanupTestRepos(repoA, repoB, remote) diff --git a/entity/dag/entity_actions_test.go b/entity/dag/entity_actions_test.go index 402f459cd14ce8ac5d15d71223e7d6a01997838b..45e69c7d9b3067248ce0241c85141e700e06de8e 100644 --- a/entity/dag/entity_actions_test.go +++ b/entity/dag/entity_actions_test.go @@ -23,7 +23,7 @@ func allEntities(t testing.TB, bugs <-chan StreamedEntity) []*Entity { return result } -func TestPushPull(t *testing.T) { +func TestEntityPushPull(t *testing.T) { repoA, repoB, remote, id1, id2, resolver, def := makeTestContextRemote(t) defer repository.CleanupTestRepos(repoA, repoB, remote) diff --git a/identity/identity_actions_test.go b/identity/identity_actions_test.go index 54cb2a4682c610cfdfcdfd33f2652883a163f27d..2a5954d6af84983a5b67909da9d5ef93d85ff14a 100644 --- a/identity/identity_actions_test.go +++ b/identity/identity_actions_test.go @@ -8,7 +8,7 @@ import ( "github.com/MichaelMure/git-bug/repository" ) -func TestPushPull(t *testing.T) { +func TestIdentityPushPull(t *testing.T) { repoA, repoB, remote := repository.SetupGoGitReposAndRemote() defer repository.CleanupTestRepos(repoA, repoB, remote) From bd09541752ef4db008500d238762ebe7f2f7be39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sat, 20 Feb 2021 14:37:06 +0100 Subject: [PATCH 38/42] entity: no sign-post needed --- bug/op_add_comment.go | 3 --- bug/op_create.go | 3 --- bug/op_edit_comment.go | 3 --- bug/op_label_change.go | 3 --- bug/op_noop.go | 3 --- bug/op_set_metadata.go | 3 --- bug/op_set_status.go | 3 --- bug/op_set_title.go | 3 --- bug/operation.go | 3 --- 9 files changed, 27 deletions(-) diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go index fd00860b2941fae558b4dd50cf0b82a9eb71db8a..4cba200f43efad4df5d866cc08d3cf92eed25554 100644 --- a/bug/op_add_comment.go +++ b/bug/op_add_comment.go @@ -21,9 +21,6 @@ type AddCommentOperation struct { Files []repository.Hash `json:"files"` } -// Sign-post method for gqlgen -func (op *AddCommentOperation) IsOperation() {} - func (op *AddCommentOperation) Id() entity.Id { return idOperation(op, &op.OpBase) } diff --git a/bug/op_create.go b/bug/op_create.go index 2423e5714cf56f06171a6b64a1bb05a00247093c..e3e38ade40c53de895054a9eb79d559901bb825c 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -27,9 +27,6 @@ type CreateOperation struct { Files []repository.Hash `json:"files"` } -// Sign-post method for gqlgen -func (op *CreateOperation) IsOperation() {} - func (op *CreateOperation) Id() entity.Id { return idOperation(op, &op.OpBase) } diff --git a/bug/op_edit_comment.go b/bug/op_edit_comment.go index e08aeaad9aa05082b2c12802a5d3de71aa6419b8..653ab71e565ab85d012142b9578f5c7673237563 100644 --- a/bug/op_edit_comment.go +++ b/bug/op_edit_comment.go @@ -24,9 +24,6 @@ type EditCommentOperation struct { Files []repository.Hash `json:"files"` } -// Sign-post method for gqlgen -func (op *EditCommentOperation) IsOperation() {} - func (op *EditCommentOperation) Id() entity.Id { return idOperation(op, &op.OpBase) } diff --git a/bug/op_label_change.go b/bug/op_label_change.go index d682fe54d0e0932d8b7cd77bc64b7d201f417f99..8b0e5ec8979f6ba495cbbb3da3a0708951452d7d 100644 --- a/bug/op_label_change.go +++ b/bug/op_label_change.go @@ -21,9 +21,6 @@ type LabelChangeOperation struct { Removed []Label `json:"removed"` } -// Sign-post method for gqlgen -func (op *LabelChangeOperation) IsOperation() {} - func (op *LabelChangeOperation) Id() entity.Id { return idOperation(op, &op.OpBase) } diff --git a/bug/op_noop.go b/bug/op_noop.go index 81efdb257c86b5f95e10350bd98b1d7908582ef4..1b11e69419dbce7122895ea36ee4582781af80ef 100644 --- a/bug/op_noop.go +++ b/bug/op_noop.go @@ -16,9 +16,6 @@ type NoOpOperation struct { OpBase } -// Sign-post method for gqlgen -func (op *NoOpOperation) IsOperation() {} - func (op *NoOpOperation) Id() entity.Id { return idOperation(op, &op.OpBase) } diff --git a/bug/op_set_metadata.go b/bug/op_set_metadata.go index 4e596728cb7c6742a5f6b8413c6df18213f434d8..ca19a8384b930abfaf8e6210f184257f7a5f9a3f 100644 --- a/bug/op_set_metadata.go +++ b/bug/op_set_metadata.go @@ -17,9 +17,6 @@ type SetMetadataOperation struct { NewMetadata map[string]string `json:"new_metadata"` } -// Sign-post method for gqlgen -func (op *SetMetadataOperation) IsOperation() {} - func (op *SetMetadataOperation) Id() entity.Id { return idOperation(op, &op.OpBase) } diff --git a/bug/op_set_status.go b/bug/op_set_status.go index ca8c434a9bf912b7e295f44f6cd1e2fecf697798..e22ded54c80715e3404c2eb33e5b668d032022dc 100644 --- a/bug/op_set_status.go +++ b/bug/op_set_status.go @@ -18,9 +18,6 @@ type SetStatusOperation struct { Status Status `json:"status"` } -// Sign-post method for gqlgen -func (op *SetStatusOperation) IsOperation() {} - func (op *SetStatusOperation) Id() entity.Id { return idOperation(op, &op.OpBase) } diff --git a/bug/op_set_title.go b/bug/op_set_title.go index 899b4fa3796ff7993697f30fcbb057aab506f7ef..c6a26746c6d294c18178d6f71ba6c3fc97d9a0ee 100644 --- a/bug/op_set_title.go +++ b/bug/op_set_title.go @@ -21,9 +21,6 @@ type SetTitleOperation struct { Was string `json:"was"` } -// Sign-post method for gqlgen -func (op *SetTitleOperation) IsOperation() {} - func (op *SetTitleOperation) Id() entity.Id { return idOperation(op, &op.OpBase) } diff --git a/bug/operation.go b/bug/operation.go index 71a5c15dc23d32b5db46bcdc82367a981f0036e4..0423c2295550cee954dbe95b82df9a1e2b2a8e9f 100644 --- a/bug/operation.go +++ b/bug/operation.go @@ -49,9 +49,6 @@ type Operation interface { AllMetadata() map[string]string setExtraMetadataImmutable(key string, value string) - - // sign-post method for gqlgen - IsOperation() } func idOperation(op Operation, base *OpBase) entity.Id { From f1d4a19af81fcc05ae9d90e018ff141f6521335a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 14 Mar 2021 18:39:04 +0100 Subject: [PATCH 39/42] bug: nonce on all operation to prevent id collision --- bug/op_create.go | 15 --------------- bug/operation.go | 26 ++++++++++++++++++++++++++ entity/dag/operation.go | 16 +++++++++++++--- entity/id.go | 2 +- identity/version.go | 2 +- 5 files changed, 41 insertions(+), 20 deletions(-) diff --git a/bug/op_create.go b/bug/op_create.go index e3e38ade40c53de895054a9eb79d559901bb825c..37e1ddc566f359fb2b8a95327da3efe54b318471 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -1,7 +1,6 @@ package bug import ( - "crypto/rand" "encoding/json" "fmt" "strings" @@ -18,10 +17,6 @@ var _ Operation = &CreateOperation{} // CreateOperation define the initial creation of a bug type CreateOperation struct { OpBase - // mandatory random bytes to ensure a better randomness of the data of the first - // operation of a bug, used to later generate the ID - // len(Nonce) should be > 20 and < 64 bytes - Nonce []byte `json:"nonce"` Title string `json:"title"` Message string `json:"message"` Files []repository.Hash `json:"files"` @@ -147,19 +142,9 @@ func (op *CreateOperation) UnmarshalJSON(data []byte) error { // Sign post method for gqlgen func (op *CreateOperation) IsAuthored() {} -func makeNonce(len int) []byte { - result := make([]byte, len) - _, err := rand.Read(result) - if err != nil { - panic(err) - } - return result -} - func NewCreateOp(author identity.Interface, unixTime int64, title, message string, files []repository.Hash) *CreateOperation { return &CreateOperation{ OpBase: newOpBase(CreateOp, author, unixTime), - Nonce: makeNonce(20), Title: title, Message: message, Files: files, diff --git a/bug/operation.go b/bug/operation.go index 0423c2295550cee954dbe95b82df9a1e2b2a8e9f..d01f1cc9971553c25ceecb92e352a2010be41091 100644 --- a/bug/operation.go +++ b/bug/operation.go @@ -1,6 +1,7 @@ package bug import ( + "crypto/rand" "encoding/json" "fmt" "time" @@ -138,6 +139,12 @@ type OpBase struct { // TODO: part of the data model upgrade, this should eventually be a timestamp + lamport UnixTime int64 `json:"timestamp"` Metadata map[string]string `json:"metadata,omitempty"` + + // mandatory random bytes to ensure a better randomness of the data used to later generate the ID + // len(Nonce) should be > 20 and < 64 bytes + // It has no functional purpose and should be ignored. + Nonce []byte `json:"nonce"` + // Not serialized. Store the op's id in memory. id entity.Id // Not serialized. Store the extra metadata in memory, @@ -151,10 +158,20 @@ func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OperationType: opType, Author_: author, UnixTime: unixTime, + Nonce: makeNonce(20), id: entity.UnsetId, } } +func makeNonce(len int) []byte { + result := make([]byte, len) + _, err := rand.Read(result) + if err != nil { + panic(err) + } + return result +} + func (base *OpBase) UnmarshalJSON(data []byte) error { // Compute the Id when loading the op from disk. base.id = entity.DeriveId(data) @@ -164,6 +181,7 @@ func (base *OpBase) UnmarshalJSON(data []byte) error { Author json.RawMessage `json:"author"` UnixTime int64 `json:"timestamp"` Metadata map[string]string `json:"metadata,omitempty"` + Nonce []byte `json:"nonce"` }{} if err := json.Unmarshal(data, &aux); err != nil { @@ -180,6 +198,7 @@ func (base *OpBase) UnmarshalJSON(data []byte) error { base.Author_ = author base.UnixTime = aux.UnixTime base.Metadata = aux.Metadata + base.Nonce = aux.Nonce return nil } @@ -222,6 +241,13 @@ func (base *OpBase) Validate(op Operation, opType OperationType) error { } } + if len(base.Nonce) > 64 { + return fmt.Errorf("nonce is too big") + } + if len(base.Nonce) < 20 { + return fmt.Errorf("nonce is too small") + } + return nil } diff --git a/entity/dag/operation.go b/entity/dag/operation.go index b0a78de6f7f6a438d447459db4c0d8d4b97fc8ca..94974a825a514611e1ad3186e516347c9490a6cc 100644 --- a/entity/dag/operation.go +++ b/entity/dag/operation.go @@ -10,13 +10,23 @@ import ( // data structure and storage. type Operation interface { // Id return the Operation identifier + // // Some care need to be taken to define a correct Id derivation and enough entropy in the data used to avoid // collisions. Notably: - // - the Id of the first Operation will be used as the Id of the Entity. Collision need to be avoided across entities of the same type - // (example: no collision within the "bug" namespace). + // - the Id of the first Operation will be used as the Id of the Entity. Collision need to be avoided across entities + // of the same type (example: no collision within the "bug" namespace). // - collisions can also happen within the set of Operations of an Entity. Simple Operation might not have enough // entropy to yield unique Ids (example: two "close" operation within the same second, same author). - // A common way to derive an Id will be to use the DeriveId function on the serialized operation data. + // If this is a concern, it is recommended to include a piece of random data in the operation's data, to guarantee + // a minimal amount of entropy and avoid collision. + // + // Author's note: I tried to find a clever way around that inelegance (stuffing random useless data into the stored + // structure is not exactly elegant) but I failed to find a proper way. Essentially, anything that would reuse some + // other data (parent operation's Id, lamport clock) or the graph structure (depth) impose that the Id would only + // make sense in the context of the graph and yield some deep coupling between Entity and Operation. This in turn + // make the whole thing even less elegant. + // + // A common way to derive an Id will be to use the entity.DeriveId() function on the serialized operation data. Id() entity.Id // Validate check if the Operation data is valid Validate() error diff --git a/entity/id.go b/entity/id.go index b602452e7112298fda2a6dea5d8723a3388c5312..c8dbdb94d9ce4e497426ce1005bc8fb4c77524c6 100644 --- a/entity/id.go +++ b/entity/id.go @@ -18,7 +18,7 @@ const UnsetId = Id("unset") // Id is an identifier for an entity or part of an entity type Id string -// DeriveId generate an Id from some data, taken from a root part of the entity. +// DeriveId generate an Id from the serialization of the object or part of the object. func DeriveId(data []byte) Id { // My understanding is that sha256 is enough to prevent collision (git use that, so ...?) // If you read this code, I'd be happy to be schooled. diff --git a/identity/version.go b/identity/version.go index cbd56a983aaa6e3d5f3eaaede8d6112a81291d5a..1c35831e0e7e89984178a014d92a63150d5502a0 100644 --- a/identity/version.go +++ b/identity/version.go @@ -37,7 +37,7 @@ type version struct { keys []*Key // mandatory random bytes to ensure a better randomness of the data of the first - // version of a bug, used to later generate the ID + // version of an identity, used to later generate the ID // len(Nonce) should be > 20 and < 64 bytes // It has no functional purpose and should be ignored. // TODO: optional after first version? From 214abe4dea1984086e45d1399538fb12aa010642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sat, 20 Feb 2021 15:48:44 +0100 Subject: [PATCH 40/42] WIP operation with files --- bug/op_add_comment.go | 2 ++ bug/op_create.go | 2 ++ bug/op_edit_comment.go | 2 ++ bug/operation.go | 17 +++++------- entity/dag/common_test.go | 15 +++++++---- entity/dag/operation.go | 9 +++++++ entity/dag/operation_pack.go | 45 ++++++++++++++++++++++++++++--- entity/dag/operation_pack_test.go | 20 ++++++++++++-- 8 files changed, 91 insertions(+), 21 deletions(-) diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go index 4cba200f43efad4df5d866cc08d3cf92eed25554..f835866b54d58142e69ff2e2f18e3195b1c984f7 100644 --- a/bug/op_add_comment.go +++ b/bug/op_add_comment.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/entity/dag" "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/text" @@ -12,6 +13,7 @@ import ( ) var _ Operation = &AddCommentOperation{} +var _ dag.OperationWithFiles = &AddCommentOperation{} // AddCommentOperation will add a new comment in the bug type AddCommentOperation struct { diff --git a/bug/op_create.go b/bug/op_create.go index 37e1ddc566f359fb2b8a95327da3efe54b318471..75b60bd8b3b446399e5f065c9172e9c0d758f7fe 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/entity/dag" "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/text" @@ -13,6 +14,7 @@ import ( ) var _ Operation = &CreateOperation{} +var _ dag.OperationWithFiles = &CreateOperation{} // CreateOperation define the initial creation of a bug type CreateOperation struct { diff --git a/bug/op_edit_comment.go b/bug/op_edit_comment.go index 653ab71e565ab85d012142b9578f5c7673237563..3e6634e4ce9f171108ba2f0dea7ff0d7cc112834 100644 --- a/bug/op_edit_comment.go +++ b/bug/op_edit_comment.go @@ -7,6 +7,7 @@ import ( "github.com/pkg/errors" "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/entity/dag" "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/timestamp" @@ -15,6 +16,7 @@ import ( ) var _ Operation = &EditCommentOperation{} +var _ dag.OperationWithFiles = &EditCommentOperation{} // EditCommentOperation will change a comment in the bug type EditCommentOperation struct { diff --git a/bug/operation.go b/bug/operation.go index d01f1cc9971553c25ceecb92e352a2010be41091..8daa2cde9ed0fe657aa69d3f9518c1d6d575bae2 100644 --- a/bug/operation.go +++ b/bug/operation.go @@ -11,7 +11,6 @@ import ( "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity/dag" "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/repository" ) // OperationType is an operation type identifier @@ -38,10 +37,9 @@ type Operation interface { // Time return the time when the operation was added Time() time.Time - // GetFiles return the files needed by this operation - GetFiles() []repository.Hash // Apply the operation to a Snapshot to create the final state Apply(snapshot *Snapshot) + // SetMetadata store arbitrary metadata about the operation SetMetadata(key string, value string) // GetMetadata retrieve arbitrary metadata about the operation @@ -212,11 +210,6 @@ func (base *OpBase) Time() time.Time { return time.Unix(base.UnixTime, 0) } -// GetFiles return the files needed by this operation -func (base *OpBase) GetFiles() []repository.Hash { - return nil -} - // Validate check the OpBase for errors func (base *OpBase) Validate(op Operation, opType OperationType) error { if base.OperationType != opType { @@ -235,9 +228,11 @@ func (base *OpBase) Validate(op Operation, opType OperationType) error { return errors.Wrap(err, "author") } - for _, hash := range op.GetFiles() { - if !hash.IsValid() { - return fmt.Errorf("file with invalid hash %v", hash) + if op, ok := op.(dag.OperationWithFiles); ok { + for _, hash := range op.GetFiles() { + if !hash.IsValid() { + return fmt.Errorf("file with invalid hash %v", hash) + } } } diff --git a/entity/dag/common_test.go b/entity/dag/common_test.go index fa15cd1f41bd81a8e22ff6fa22db06f2fa5fdac8..1898451d017c4bd035b8d91693628656f84ebb77 100644 --- a/entity/dag/common_test.go +++ b/entity/dag/common_test.go @@ -23,10 +23,11 @@ type op1 struct { OperationType int `json:"type"` Field1 string `json:"field_1"` + Files []repository.Hash } -func newOp1(author identity.Interface, field1 string) *op1 { - return &op1{author: author, OperationType: 1, Field1: field1} +func newOp1(author identity.Interface, field1 string, files ...repository.Hash) *op1 { + return &op1{author: author, OperationType: 1, Field1: field1, Files: files} } func (o *op1) Id() entity.Id { @@ -34,11 +35,15 @@ func (o *op1) Id() entity.Id { return entity.DeriveId(data) } +func (o *op1) Validate() error { return nil } + func (o *op1) Author() identity.Interface { return o.author } -func (o *op1) Validate() error { return nil } +func (o *op1) GetFiles() []repository.Hash { + return o.Files +} type op2 struct { author identity.Interface @@ -56,12 +61,12 @@ func (o *op2) Id() entity.Id { return entity.DeriveId(data) } +func (o *op2) Validate() error { return nil } + func (o *op2) Author() identity.Interface { return o.author } -func (o *op2) Validate() error { return nil } - func unmarshaler(author identity.Interface, raw json.RawMessage) (Operation, error) { var t struct { OperationType int `json:"type"` diff --git a/entity/dag/operation.go b/entity/dag/operation.go index 94974a825a514611e1ad3186e516347c9490a6cc..1bfb3d3df3d3a98a859f7126a9d6f48832732c08 100644 --- a/entity/dag/operation.go +++ b/entity/dag/operation.go @@ -3,6 +3,7 @@ package dag import ( "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/repository" ) // Operation is a piece of data defining a change to reflect on the state of an Entity. @@ -33,3 +34,11 @@ type Operation interface { // Author returns the author of this operation Author() identity.Interface } + +// OperationWithFiles is an extended Operation that has files dependency, stored in git. +type OperationWithFiles interface { + Operation + + // GetFiles return the files needed by this operation + GetFiles() []repository.Hash +} diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go index a436fd332f56db0ffc6b57e7b60d2b0e5c526efe..72063c604aff4b8549199b05b9653d9bd119b22e 100644 --- a/entity/dag/operation_pack.go +++ b/entity/dag/operation_pack.go @@ -15,10 +15,8 @@ import ( "github.com/MichaelMure/git-bug/util/lamport" ) -// TODO: extra data tree -const extraEntryName = "extra" - const opsEntryName = "ops" +const extraEntryName = "extra" const versionEntryPrefix = "version-" const createClockEntryPrefix = "create-clock-" const editClockEntryPrefix = "edit-clock-" @@ -118,6 +116,7 @@ func (opp *operationPack) Write(def Definition, repo repository.Repo, parentComm // Make a Git tree referencing this blob and encoding the other values: // - format version // - clocks + // - extra data tree := []repository.TreeEntry{ {ObjectType: repository.Blob, Hash: emptyBlobHash, Name: fmt.Sprintf(versionEntryPrefix+"%d", def.FormatVersion)}, @@ -133,6 +132,17 @@ func (opp *operationPack) Write(def Definition, repo repository.Repo, parentComm Name: fmt.Sprintf(createClockEntryPrefix+"%d", opp.CreateTime), }) } + if extraTree := opp.makeExtraTree(); len(extraTree) > 0 { + extraTreeHash, err := repo.StoreTree(extraTree) + if err != nil { + return "", err + } + tree = append(tree, repository.TreeEntry{ + ObjectType: repository.Tree, + Hash: extraTreeHash, + Name: extraEntryName, + }) + } // Store the tree treeHash, err := repo.StoreTree(tree) @@ -163,6 +173,35 @@ func (opp *operationPack) Write(def Definition, repo repository.Repo, parentComm return commitHash, nil } +func (opp *operationPack) makeExtraTree() []repository.TreeEntry { + var tree []repository.TreeEntry + counter := 0 + added := make(map[repository.Hash]interface{}) + + for _, ops := range opp.Operations { + ops, ok := ops.(OperationWithFiles) + if !ok { + continue + } + + for _, file := range ops.GetFiles() { + if _, has := added[file]; !has { + tree = append(tree, repository.TreeEntry{ + ObjectType: repository.Blob, + Hash: file, + // The name is not important here, we only need to + // reference the blob. + Name: fmt.Sprintf("file%d", counter), + }) + counter++ + added[file] = struct{}{} + } + } + } + + return tree +} + // readOperationPack read the operationPack encoded in git at the given Tree hash. // // Validity of the Lamport clocks is left for the caller to decide. diff --git a/entity/dag/operation_pack_test.go b/entity/dag/operation_pack_test.go index a12382afe021adda78affc9d53139d76ead70b25..0fe98dc7dee41ce64b4acea6540f3689c61da6e6 100644 --- a/entity/dag/operation_pack_test.go +++ b/entity/dag/operation_pack_test.go @@ -1,6 +1,7 @@ package dag import ( + "math/rand" "testing" "github.com/stretchr/testify/require" @@ -11,10 +12,16 @@ import ( func TestOperationPackReadWrite(t *testing.T) { repo, id1, _, resolver, def := makeTestContext() + blobHash1, err := repo.StoreData(randomData()) + require.NoError(t, err) + + blobHash2, err := repo.StoreData(randomData()) + require.NoError(t, err) + opp := &operationPack{ Author: id1, Operations: []Operation{ - newOp1(id1, "foo"), + newOp1(id1, "foo", blobHash1, blobHash2), newOp2(id1, "bar"), }, CreateTime: 123, @@ -36,7 +43,7 @@ func TestOperationPackReadWrite(t *testing.T) { opp3 := &operationPack{ Author: id1, Operations: []Operation{ - newOp1(id1, "foo"), + newOp1(id1, "foo", blobHash1, blobHash2), newOp2(id1, "bar"), }, CreateTime: 123, @@ -86,3 +93,12 @@ func TestOperationPackSignedReadWrite(t *testing.T) { } require.Equal(t, opp.Id(), opp3.Id()) } + +func randomData() []byte { + var letterRunes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + b := make([]byte, 32) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return b +} From 5215634d0dca37c545904fbc8a12ddd9b8eb72df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 21 Mar 2021 18:22:04 +0100 Subject: [PATCH 41/42] entity: add support for storing files --- entity/dag/common_test.go | 6 +-- entity/dag/operation_pack_test.go | 71 +++++++++++++++++++++++++++---- repository/tree_entry.go | 10 +++++ 3 files changed, 76 insertions(+), 11 deletions(-) diff --git a/entity/dag/common_test.go b/entity/dag/common_test.go index 1898451d017c4bd035b8d91693628656f84ebb77..25289b764b38943b55e3efe2d89c489ba3247a35 100644 --- a/entity/dag/common_test.go +++ b/entity/dag/common_test.go @@ -21,9 +21,9 @@ import ( type op1 struct { author identity.Interface - OperationType int `json:"type"` - Field1 string `json:"field_1"` - Files []repository.Hash + OperationType int `json:"type"` + Field1 string `json:"field_1"` + Files []repository.Hash `json:"files"` } func newOp1(author identity.Interface, field1 string, files ...repository.Hash) *op1 { diff --git a/entity/dag/operation_pack_test.go b/entity/dag/operation_pack_test.go index 0fe98dc7dee41ce64b4acea6540f3689c61da6e6..73960800c33ddfa31008186433fe2ab26dbc3549 100644 --- a/entity/dag/operation_pack_test.go +++ b/entity/dag/operation_pack_test.go @@ -7,21 +7,16 @@ import ( "github.com/stretchr/testify/require" "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/repository" ) func TestOperationPackReadWrite(t *testing.T) { repo, id1, _, resolver, def := makeTestContext() - blobHash1, err := repo.StoreData(randomData()) - require.NoError(t, err) - - blobHash2, err := repo.StoreData(randomData()) - require.NoError(t, err) - opp := &operationPack{ Author: id1, Operations: []Operation{ - newOp1(id1, "foo", blobHash1, blobHash2), + newOp1(id1, "foo"), newOp2(id1, "bar"), }, CreateTime: 123, @@ -43,7 +38,7 @@ func TestOperationPackReadWrite(t *testing.T) { opp3 := &operationPack{ Author: id1, Operations: []Operation{ - newOp1(id1, "foo", blobHash1, blobHash2), + newOp1(id1, "foo"), newOp2(id1, "bar"), }, CreateTime: 123, @@ -94,6 +89,66 @@ func TestOperationPackSignedReadWrite(t *testing.T) { require.Equal(t, opp.Id(), opp3.Id()) } +func TestOperationPackFiles(t *testing.T) { + repo, id1, _, resolver, def := makeTestContext() + + blobHash1, err := repo.StoreData(randomData()) + require.NoError(t, err) + + blobHash2, err := repo.StoreData(randomData()) + require.NoError(t, err) + + opp := &operationPack{ + Author: id1, + Operations: []Operation{ + newOp1(id1, "foo", blobHash1, blobHash2), + newOp1(id1, "foo", blobHash2), + }, + CreateTime: 123, + EditTime: 456, + } + + commitHash, err := opp.Write(def, repo) + require.NoError(t, err) + + commit, err := repo.ReadCommit(commitHash) + require.NoError(t, err) + + opp2, err := readOperationPack(def, repo, resolver, commit) + require.NoError(t, err) + + require.Equal(t, opp, opp2) + + require.ElementsMatch(t, opp2.Operations[0].(OperationWithFiles).GetFiles(), []repository.Hash{ + blobHash1, + blobHash2, + }) + require.ElementsMatch(t, opp2.Operations[1].(OperationWithFiles).GetFiles(), []repository.Hash{ + blobHash2, + }) + + tree, err := repo.ReadTree(commit.TreeHash) + require.NoError(t, err) + + extraTreeHash, ok := repository.SearchTreeEntry(tree, extraEntryName) + require.True(t, ok) + + extraTree, err := repo.ReadTree(extraTreeHash.Hash) + require.NoError(t, err) + require.ElementsMatch(t, extraTree, []repository.TreeEntry{ + { + ObjectType: repository.Blob, + Hash: blobHash1, + Name: "file0", + }, + { + ObjectType: repository.Blob, + Hash: blobHash2, + Name: "file1", + }, + }) +} + func randomData() []byte { var letterRunes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" b := make([]byte, 32) diff --git a/repository/tree_entry.go b/repository/tree_entry.go index 6c5ec1a53a25bfd86b5772b9882f350f9f4dcd9e..9d70814cdad738a69542314557a3fd398791a73b 100644 --- a/repository/tree_entry.go +++ b/repository/tree_entry.go @@ -100,3 +100,13 @@ func readTreeEntries(s string) ([]TreeEntry, error) { return casted, nil } + +// SearchTreeEntry search a TreeEntry by name from an array +func SearchTreeEntry(entries []TreeEntry, name string) (TreeEntry, bool) { + for _, entry := range entries { + if entry.Name == name { + return entry, true + } + } + return TreeEntry{}, false +} From cb9b06551ddc1fae33046733f79ede20f8d09f9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 4 Apr 2021 11:23:04 +0200 Subject: [PATCH 42/42] entity: more comments --- entity/dag/operation.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/entity/dag/operation.go b/entity/dag/operation.go index 1bfb3d3df3d3a98a859f7126a9d6f48832732c08..a320859f4278a39562af5f0069ae108ad0c59e4e 100644 --- a/entity/dag/operation.go +++ b/entity/dag/operation.go @@ -40,5 +40,9 @@ type OperationWithFiles interface { Operation // GetFiles return the files needed by this operation + // This implies that the Operation maintain and store internally the references to those files. This is how + // this information is read later, when loading from storage. + // For example, an operation that has a text value referencing some files would maintain a mapping (text ref --> + // hash). GetFiles() []repository.Hash }