From a53a763f0ef2b5df93817bda45ae66dccadab353 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 16:18:54 +0000 Subject: [PATCH 001/157] build(deps): [security] bump node-notifier from 8.0.0 to 8.0.1 in /webui Bumps [node-notifier](https://github.com/mikaelbr/node-notifier) from 8.0.0 to 8.0.1. **This update includes a security fix.** - [Release notes](https://github.com/mikaelbr/node-notifier/releases) - [Changelog](https://github.com/mikaelbr/node-notifier/blob/v8.0.1/CHANGELOG.md) - [Commits](https://github.com/mikaelbr/node-notifier/compare/v8.0.0...v8.0.1) Signed-off-by: dependabot-preview[bot] --- webui/package-lock.json | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/webui/package-lock.json b/webui/package-lock.json index 47d5e17ddcd5afc26b34da0b5973f7b230f05c34..702eefc080d47db757ca1729eaeee50acf4f6504 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -12176,8 +12176,7 @@ "growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "optional": true + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=" }, "gzip-size": { "version": "5.1.1", @@ -15680,10 +15679,9 @@ "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=" }, "node-notifier": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.0.tgz", - "integrity": "sha512-46z7DUmcjoYdaWyXouuFNNfUo6eFa94t23c53c+lG/9Cvauk4a98rAUp9672X5dxGdQmLpPzTxzu8f/OeEPaFA==", - "optional": true, + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz", + "integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==", "requires": { "growly": "^1.3.0", "is-wsl": "^2.2.0", @@ -15694,22 +15692,22 @@ }, "dependencies": { "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "optional": true + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "requires": { + "lru-cache": "^6.0.0" + } }, "uuid": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", - "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==", - "optional": true + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "optional": true, "requires": { "isexe": "^2.0.0" } @@ -19767,8 +19765,7 @@ "shellwords": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "optional": true + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==" }, "side-channel": { "version": "1.0.3", 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 002/157] 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 003/157] 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 004/157] 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 005/157] 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 006/157] 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 007/157] 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 008/157] 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 009/157] 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 010/157] 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 011/157] 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 012/157] 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 013/157] 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 014/157] 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 015/157] 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 016/157] 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 017/157] 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 018/157] 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 019/157] 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 020/157] 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 021/157] 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 022/157] 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 023/157] 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 024/157] 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 025/157] 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 026/157] 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 027/157] 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 028/157] 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 029/157] 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 030/157] 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 031/157] 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 032/157] 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 033/157] 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 034/157] 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 035/157] 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 036/157] 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 037/157] 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 038/157] 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 039/157] 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 ea329aed6909cac85680dbae37f6f4dcca134f8b Mon Sep 17 00:00:00 2001 From: Sascha Date: Sun, 28 Feb 2021 16:27:14 +0100 Subject: [PATCH 040/157] Add option to specify host address '--host'-cmdline-option is added to the webui command. Previously, the WebUI couldn't be hosted inside of a container. As the WebUI-server only listend per default to localhost and there was no option to change the address, the server should listend to. This means, that the WebUI was only reachable from localhost. So only from inside of the container but never from outside. The '--host'-option allows to set the IP address or a hostname which the WebUI-server should listen to. E.g. by setting 0.0.0.0 or :: as address. Update documentation for new option. Update shell completion for new option. Compilation seems to add another go-gitlab version. --- commands/webui.go | 8 ++++++-- doc/man/git-bug-webui.1 | 6 +++++- doc/md/git-bug_webui.md | 11 ++++++----- go.sum | 1 + misc/bash_completion/git-bug | 4 ++++ 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/commands/webui.go b/commands/webui.go index 7e5fc75206d060fed6c015f0987f04bce37ceb7d..d910a7034794ecf0ff5d41fa3233c2a6d9944711 100644 --- a/commands/webui.go +++ b/commands/webui.go @@ -4,9 +4,11 @@ import ( "context" "fmt" "log" + "net" "net/http" "os" "os/signal" + "strconv" "time" "github.com/99designs/gqlgen/graphql/playground" @@ -27,6 +29,7 @@ import ( const webUIOpenConfigKey = "git-bug.webui.open" type webUIOptions struct { + host string port int open bool noOpen bool @@ -54,9 +57,10 @@ Available git config: flags := cmd.Flags() flags.SortFlags = false + flags.StringVar(&options.host, "host", "127.0.0.1", "Network address or hostname to listen to (default to 127.0.0.1)") flags.BoolVar(&options.open, "open", false, "Automatically open the web UI in the default browser") flags.BoolVar(&options.noOpen, "no-open", false, "Prevent the automatic opening of the web UI in the default browser") - flags.IntVarP(&options.port, "port", "p", 0, "Port to listen to (default is random)") + flags.IntVarP(&options.port, "port", "p", 0, "Port to listen to (default to random available port)") flags.BoolVar(&options.readOnly, "read-only", false, "Whether to run the web UI in read-only mode") return cmd @@ -71,7 +75,7 @@ func runWebUI(env *Env, opts webUIOptions, args []string) error { } } - addr := fmt.Sprintf("127.0.0.1:%d", opts.port) + addr := net.JoinHostPort(opts.host, strconv.Itoa(opts.port)) webUiAddr := fmt.Sprintf("http://%s", addr) router := mux.NewRouter() diff --git a/doc/man/git-bug-webui.1 b/doc/man/git-bug-webui.1 index 954250a280bf497abbf845d94a047e8da5c080e3..6e4622be77b4cb36d29f09200798bbc3d5afd811 100644 --- a/doc/man/git-bug-webui.1 +++ b/doc/man/git-bug-webui.1 @@ -21,6 +21,10 @@ Available git config: .SH OPTIONS +.PP +\fB\-\-host\fP="127.0.0.1" + Network address or hostname to listen to (default to 127.0.0.1) + .PP \fB\-\-open\fP[=false] Automatically open the web UI in the default browser @@ -31,7 +35,7 @@ Available git config: .PP \fB\-p\fP, \fB\-\-port\fP=0 - Port to listen to (default is random) + Port to listen to (default to random available port) .PP \fB\-\-read\-only\fP[=false] diff --git a/doc/md/git-bug_webui.md b/doc/md/git-bug_webui.md index 98a61eb2b8c6fdb5d49e504b77f6704d63dee7a9..ccfaff9a6e9540f084c7ae0ac420d8a1b410442e 100644 --- a/doc/md/git-bug_webui.md +++ b/doc/md/git-bug_webui.md @@ -17,11 +17,12 @@ git-bug webui [flags] ### Options ``` - --open Automatically open the web UI in the default browser - --no-open Prevent the automatic opening of the web UI in the default browser - -p, --port int Port to listen to (default is random) - --read-only Whether to run the web UI in read-only mode - -h, --help help for webui + --host string Network address or hostname to listen to (default to 127.0.0.1) (default "127.0.0.1") + --open Automatically open the web UI in the default browser + --no-open Prevent the automatic opening of the web UI in the default browser + -p, --port int Port to listen to (default to random available port) + --read-only Whether to run the web UI in read-only mode + -h, --help help for webui ``` ### SEE ALSO diff --git a/go.sum b/go.sum index 2cfdff75edf8320a667e0b8f9a3e4c5dd590993d..b13e190e94a8fb00d370a3a91fa441e9f92567ad 100644 --- a/go.sum +++ b/go.sum @@ -476,6 +476,7 @@ 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.40.1 h1:jHueLh5Inzv20TL5Yki+CaLmyvtw3Yq7blbWx7GmglQ= github.com/xanzy/go-gitlab v0.40.1/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= +github.com/xanzy/go-gitlab v0.44.0 h1:cEiGhqu7EpFGuei2a2etAwB+x6403E5CvpLn35y+GPs= github.com/xanzy/go-gitlab v0.44.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= diff --git a/misc/bash_completion/git-bug b/misc/bash_completion/git-bug index 3cedd86a118dbcf036e13a01e4f936a2fdf1d8e9..8f2a0f8f3e7e1c2bba629787b5f5ea790c82906c 100644 --- a/misc/bash_completion/git-bug +++ b/misc/bash_completion/git-bug @@ -1331,6 +1331,10 @@ _git-bug_webui() flags_with_completion=() flags_completion=() + flags+=("--host=") + two_word_flags+=("--host") + local_nonpersistent_flags+=("--host") + local_nonpersistent_flags+=("--host=") flags+=("--open") local_nonpersistent_flags+=("--open") flags+=("--no-open") From 8c17d730eff3bb59313a2f9f7aa469fe6fff95b2 Mon Sep 17 00:00:00 2001 From: Sascha Date: Tue, 23 Feb 2021 16:03:07 +0100 Subject: [PATCH 041/157] Add button to toggle between light- and dark-mode --- webui/src/components/Header/Header.tsx | 7 +++ webui/src/components/Themer.tsx | 64 ++++++++++++++++++++++++++ webui/src/index.tsx | 10 +--- webui/src/theme.ts | 1 + 4 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 webui/src/components/Themer.tsx diff --git a/webui/src/components/Header/Header.tsx b/webui/src/components/Header/Header.tsx index 3e39b5f35d7d860fdb8f99be567ef122fda965fa..3bdb252f4aa0ef308b5ccc2af4f635b1229c874c 100644 --- a/webui/src/components/Header/Header.tsx +++ b/webui/src/components/Header/Header.tsx @@ -5,6 +5,7 @@ import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import { makeStyles } from '@material-ui/core/styles'; +import { LightSwitch } from '../../components/Themer'; import CurrentIdentity from '../CurrentIdentity/CurrentIdentity'; const useStyles = makeStyles((theme) => ({ @@ -21,6 +22,9 @@ const useStyles = makeStyles((theme) => ({ display: 'flex', alignItems: 'center', }, + lightSwitch: { + padding: '0 20px', + }, logo: { height: '42px', marginRight: theme.spacing(2), @@ -39,6 +43,9 @@ function Header() { git-bug
+
+ +
diff --git a/webui/src/components/Themer.tsx b/webui/src/components/Themer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..13d098da2e4c2edef7401db9b6dc0a82c4c6b22c --- /dev/null +++ b/webui/src/components/Themer.tsx @@ -0,0 +1,64 @@ +import React, { createContext, useCallback, useContext, useState } from 'react'; + +import { ThemeProvider } from '@material-ui/core'; +import IconButton from '@material-ui/core/IconButton/IconButton'; +import Tooltip from '@material-ui/core/Tooltip/Tooltip'; +import { createMuiTheme, ThemeOptions } from '@material-ui/core/styles'; +import { NightsStayRounded, WbSunnyRounded } from '@material-ui/icons'; + +const defaultTheme: ThemeOptions = { + palette: { + type: 'light', + primary: { + main: '#263238', + }, + }, +}; + +const ThemeContext = createContext({ + toggleMode: () => {}, + mode: '', +}); + +const LightSwitch = () => { + const { mode, toggleMode } = useContext(ThemeContext); + + return ( + + + {mode === 'light' ? ( + + ) : ( + + )} + + + ); +}; + +type Props = { children: React.ReactNode }; +const Themer = ({ children }: Props) => { + const [theme, setTheme] = useState(defaultTheme); + + const toggleMode = useCallback(() => { + const newMode = theme.palette?.type === 'dark' ? 'light' : 'dark'; + const adjustedTheme: ThemeOptions = { + ...theme, + palette: { + ...theme.palette, + type: newMode, + }, + }; + setTheme(adjustedTheme); + }, [theme, setTheme]); + + const newMode = theme.palette?.type === 'dark' ? 'light' : 'dark'; + + return ( + + {children} + + ); +}; + +export { Themer as default, LightSwitch }; diff --git a/webui/src/index.tsx b/webui/src/index.tsx index f07b869d7d72e464d30963f0b76e05cc6fed017f..a7f52448a1dd8091ccb51cf961e65d31389c1faa 100644 --- a/webui/src/index.tsx +++ b/webui/src/index.tsx @@ -1,21 +1,13 @@ import { ApolloProvider } from '@apollo/client'; import React from 'react'; import ReactDOM from 'react-dom'; -import { BrowserRouter } from 'react-router-dom'; - -import ThemeProvider from '@material-ui/styles/ThemeProvider'; import App from './App'; import apolloClient from './apollo'; -import theme from './theme'; ReactDOM.render( - - - - - + , document.getElementById('root') ); diff --git a/webui/src/theme.ts b/webui/src/theme.ts index d41cd731d7e4de4f89536a335e182bebbe52c70e..3d53694155d77c056946b5158600a572a7d12d51 100644 --- a/webui/src/theme.ts +++ b/webui/src/theme.ts @@ -2,6 +2,7 @@ import { createMuiTheme } from '@material-ui/core/styles'; const theme = createMuiTheme({ palette: { + type: 'dark', primary: { main: '#263238', }, From c834c03b809f226801423d726c62608297cd6fc4 Mon Sep 17 00:00:00 2001 From: Sascha Date: Thu, 25 Feb 2021 14:41:17 +0100 Subject: [PATCH 042/157] Use brower preference and persist theme mode --- webui/src/components/Themer.tsx | 39 ++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/webui/src/components/Themer.tsx b/webui/src/components/Themer.tsx index 13d098da2e4c2edef7401db9b6dc0a82c4c6b22c..78e205648f0eda5adc8ebe18e05fd6894854ad99 100644 --- a/webui/src/components/Themer.tsx +++ b/webui/src/components/Themer.tsx @@ -1,12 +1,12 @@ -import React, { createContext, useCallback, useContext, useState } from 'react'; +import React, { createContext, useContext, useState } from 'react'; -import { ThemeProvider } from '@material-ui/core'; +import { PaletteType, ThemeProvider, useMediaQuery } from '@material-ui/core'; import IconButton from '@material-ui/core/IconButton/IconButton'; import Tooltip from '@material-ui/core/Tooltip/Tooltip'; -import { createMuiTheme, ThemeOptions } from '@material-ui/core/styles'; +import { createMuiTheme } from '@material-ui/core/styles'; import { NightsStayRounded, WbSunnyRounded } from '@material-ui/icons'; -const defaultTheme: ThemeOptions = { +const defaultTheme = { palette: { type: 'light', primary: { @@ -39,24 +39,37 @@ const LightSwitch = () => { type Props = { children: React.ReactNode }; const Themer = ({ children }: Props) => { const [theme, setTheme] = useState(defaultTheme); + const preferseDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); + const browserMode = preferseDarkMode ? 'dark' : 'light'; + const preferedMode = localStorage.getItem('themeMode'); + const curMode = preferedMode != null ? preferedMode : browserMode; - const toggleMode = useCallback(() => { - const newMode = theme.palette?.type === 'dark' ? 'light' : 'dark'; - const adjustedTheme: ThemeOptions = { + const adjustedTheme = { + ...theme, + palette: { + ...theme.palette, + type: (curMode === 'dark' ? 'dark' : 'light') as PaletteType, + }, + }; + + const toggleMode = () => { + const preferedMode = curMode === 'dark' ? 'light' : 'dark'; + localStorage.setItem('themeMode', preferedMode); + const adjustedTheme = { ...theme, palette: { ...theme.palette, - type: newMode, + type: preferedMode as PaletteType, }, }; setTheme(adjustedTheme); - }, [theme, setTheme]); - - const newMode = theme.palette?.type === 'dark' ? 'light' : 'dark'; + }; return ( - - {children} + + + {children} + ); }; From 30587cc03f0b1a8fe0c5621207882b9c6714093e Mon Sep 17 00:00:00 2001 From: Sascha Date: Fri, 26 Feb 2021 16:26:37 +0100 Subject: [PATCH 043/157] Fix color of open/close filter buttons in dark mode --- webui/src/pages/list/Filter.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webui/src/pages/list/Filter.tsx b/webui/src/pages/list/Filter.tsx index 5c4a3d17edc2511c4469aff709949ff443ce8a6b..667020786fbdf72720df1d38c8e90a3fdd1fa7bd 100644 --- a/webui/src/pages/list/Filter.tsx +++ b/webui/src/pages/list/Filter.tsx @@ -65,7 +65,7 @@ function stringify(params: Query): string { const useStyles = makeStyles((theme) => ({ element: { ...theme.typography.body2, - color: '#444', + color: theme.palette.text.secondary, padding: theme.spacing(0, 1), fontWeight: 400, textDecoration: 'none', @@ -75,7 +75,7 @@ const useStyles = makeStyles((theme) => ({ }, itemActive: { fontWeight: 600, - color: '#333', + color: theme.palette.text.primary, }, icon: { paddingRight: theme.spacing(0.5), From b996779197539a429c4ad54fba225421e523702b Mon Sep 17 00:00:00 2001 From: Sascha Date: Fri, 26 Feb 2021 16:39:18 +0100 Subject: [PATCH 044/157] Fix backgroundcolor of toolbar for dark mode --- webui/src/pages/list/FilterToolbar.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webui/src/pages/list/FilterToolbar.tsx b/webui/src/pages/list/FilterToolbar.tsx index 216264168a0bbe04f052bb39b8025a386109aef3..6847397444dca459670d79250a273cf34af3210d 100644 --- a/webui/src/pages/list/FilterToolbar.tsx +++ b/webui/src/pages/list/FilterToolbar.tsx @@ -3,7 +3,7 @@ import { LocationDescriptor } from 'history'; import React from 'react'; import Toolbar from '@material-ui/core/Toolbar'; -import { makeStyles } from '@material-ui/core/styles'; +import { fade, makeStyles } from '@material-ui/core/styles'; import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline'; import ErrorOutline from '@material-ui/icons/ErrorOutline'; @@ -19,8 +19,8 @@ import { useBugCountQuery } from './FilterToolbar.generated'; const useStyles = makeStyles((theme) => ({ toolbar: { - backgroundColor: theme.palette.grey['100'], - borderColor: theme.palette.grey['300'], + backgroundColor: fade(theme.palette.text.hint, 0.05), + borderColor: theme.palette.divider, borderWidth: '1px 0', borderStyle: 'solid', margin: theme.spacing(0, -1), From 7beffb87182355c72ceffa29325815e71ead5f59 Mon Sep 17 00:00:00 2001 From: Sascha Date: Fri, 26 Feb 2021 16:39:46 +0100 Subject: [PATCH 045/157] Fix searchbar background-color for dark mode --- webui/src/pages/list/ListQuery.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webui/src/pages/list/ListQuery.tsx b/webui/src/pages/list/ListQuery.tsx index 87c21e3c2d3ce25d129ed84314a2dc17d3f13441..58660b329d0636db46778cc877db8adafdb3bd11 100644 --- a/webui/src/pages/list/ListQuery.tsx +++ b/webui/src/pages/list/ListQuery.tsx @@ -56,10 +56,10 @@ const useStyles = makeStyles((theme) => ({ }, search: { borderRadius: theme.shape.borderRadius, - borderColor: fade(theme.palette.primary.main, 0.2), + borderColor: theme.palette.divider, borderStyle: 'solid', borderWidth: '1px', - backgroundColor: fade(theme.palette.primary.main, 0.05), + backgroundColor: fade(theme.palette.text.hint, 0.05), padding: theme.spacing(0, 1), width: ({ searching }) => (searching ? '20rem' : '15rem'), transition: theme.transitions.create([ From 9280e437b06c1812d9b4a62f18686878f9b7a769 Mon Sep 17 00:00:00 2001 From: Sascha Date: Fri, 26 Feb 2021 19:07:11 +0100 Subject: [PATCH 046/157] Fix Bug description header for dark mode --- webui/src/pages/bug/Message.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx index 91549483a0898c724a3e27fd5d252a92e9a7aa57..0cc9b15a83ba5d61dbe8b7d55f7213935cd356f4 100644 --- a/webui/src/pages/bug/Message.tsx +++ b/webui/src/pages/bug/Message.tsx @@ -1,7 +1,7 @@ import React from 'react'; import Paper from '@material-ui/core/Paper'; -import { makeStyles } from '@material-ui/core/styles'; +import { fade, makeStyles } from '@material-ui/core/styles'; import Author, { Avatar } from 'src/components/Author'; import Content from 'src/components/Content'; @@ -27,11 +27,11 @@ const useStyles = makeStyles((theme) => ({ }, header: { ...theme.typography.body1, - color: '#444', + color: theme.palette.text.secondary, padding: '0.5rem 1rem', - borderBottom: '1px solid #ddd', + borderBottom: `1px solid ${theme.palette.divider}`, display: 'flex', - backgroundColor: '#e2f1ff', + backgroundColor: fade(theme.palette.text.hint, 0.05), }, title: { flex: 1, From 9cac03652c410f943abe1e3a6b55dce0d79e48d6 Mon Sep 17 00:00:00 2001 From: Sascha Date: Mon, 1 Mar 2021 17:47:57 +0100 Subject: [PATCH 047/157] Inject theme instead of defining it in Themer.tsx --- webui/src/components/Themer.tsx | 57 +++++++++++---------------------- webui/src/theme.ts | 13 ++++++-- 2 files changed, 30 insertions(+), 40 deletions(-) diff --git a/webui/src/components/Themer.tsx b/webui/src/components/Themer.tsx index 78e205648f0eda5adc8ebe18e05fd6894854ad99..d831fca9f51c1552812ac96d4135a1865108d598 100644 --- a/webui/src/components/Themer.tsx +++ b/webui/src/components/Themer.tsx @@ -1,20 +1,11 @@ import React, { createContext, useContext, useState } from 'react'; -import { PaletteType, ThemeProvider, useMediaQuery } from '@material-ui/core'; +import { ThemeProvider, useMediaQuery } from '@material-ui/core'; import IconButton from '@material-ui/core/IconButton/IconButton'; import Tooltip from '@material-ui/core/Tooltip/Tooltip'; -import { createMuiTheme } from '@material-ui/core/styles'; +import { Theme } from '@material-ui/core/styles'; import { NightsStayRounded, WbSunnyRounded } from '@material-ui/icons'; -const defaultTheme = { - palette: { - type: 'light', - primary: { - main: '#263238', - }, - }, -}; - const ThemeContext = createContext({ toggleMode: () => {}, mode: '', @@ -22,10 +13,11 @@ const ThemeContext = createContext({ const LightSwitch = () => { const { mode, toggleMode } = useContext(ThemeContext); + const description = `Switch to ${mode === 'light' ? 'dark' : 'light'} theme`; return ( - - + + {mode === 'light' ? ( ) : ( @@ -36,40 +28,29 @@ const LightSwitch = () => { ); }; -type Props = { children: React.ReactNode }; -const Themer = ({ children }: Props) => { - const [theme, setTheme] = useState(defaultTheme); +type Props = { + children: React.ReactNode; + lightTheme: Theme; + darkTheme: Theme; +}; +const Themer = ({ children, lightTheme, darkTheme }: Props) => { const preferseDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); const browserMode = preferseDarkMode ? 'dark' : 'light'; - const preferedMode = localStorage.getItem('themeMode'); - const curMode = preferedMode != null ? preferedMode : browserMode; - - const adjustedTheme = { - ...theme, - palette: { - ...theme.palette, - type: (curMode === 'dark' ? 'dark' : 'light') as PaletteType, - }, - }; + const savedMode = localStorage.getItem('themeMode'); + const preferedMode = savedMode != null ? savedMode : browserMode; + const [curMode, setMode] = useState(preferedMode); const toggleMode = () => { - const preferedMode = curMode === 'dark' ? 'light' : 'dark'; + const preferedMode = curMode === 'light' ? 'dark' : 'light'; localStorage.setItem('themeMode', preferedMode); - const adjustedTheme = { - ...theme, - palette: { - ...theme.palette, - type: preferedMode as PaletteType, - }, - }; - setTheme(adjustedTheme); + setMode(preferedMode); }; + const preferedTheme = preferedMode === 'dark' ? darkTheme : lightTheme; + return ( - - {children} - + {children} ); }; diff --git a/webui/src/theme.ts b/webui/src/theme.ts index 3d53694155d77c056946b5158600a572a7d12d51..67c24526d34fb2562d7c7ede0c4de701e20c3901 100644 --- a/webui/src/theme.ts +++ b/webui/src/theme.ts @@ -1,6 +1,15 @@ import { createMuiTheme } from '@material-ui/core/styles'; -const theme = createMuiTheme({ +const defaultLightTheme = createMuiTheme({ + palette: { + type: 'light', + primary: { + main: '#263238', + }, + }, +}); + +const defaultDarkTheme = createMuiTheme({ palette: { type: 'dark', primary: { @@ -9,4 +18,4 @@ const theme = createMuiTheme({ }, }); -export default theme; +export { defaultLightTheme, defaultDarkTheme }; From 680ede3df68b868f38b0207f4c0829e93181f161 Mon Sep 17 00:00:00 2001 From: Sascha Date: Mon, 1 Mar 2021 18:55:01 +0100 Subject: [PATCH 048/157] Fix (hopefully) eslint error on node 14.x pipeline --- webui/src/components/Themer.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webui/src/components/Themer.tsx b/webui/src/components/Themer.tsx index d831fca9f51c1552812ac96d4135a1865108d598..4adef24a72ae980a019ede1b4536820de2ba4088 100644 --- a/webui/src/components/Themer.tsx +++ b/webui/src/components/Themer.tsx @@ -13,7 +13,8 @@ const ThemeContext = createContext({ const LightSwitch = () => { const { mode, toggleMode } = useContext(ThemeContext); - const description = `Switch to ${mode === 'light' ? 'dark' : 'light'} theme`; + const nextMode = mode === 'light' ? 'dark' : 'light'; + const description = `Switch to ${nextMode} theme`; return ( From 548febcbc7af50e8f33c393d481fa253f27aa795 Mon Sep 17 00:00:00 2001 From: Sascha Date: Tue, 2 Mar 2021 13:29:36 +0100 Subject: [PATCH 049/157] Define each theme in own file under themes dir --- webui/src/themes/DefaultDark.ts | 12 ++++++++++++ webui/src/{theme.ts => themes/DefaultLight.ts} | 11 +---------- webui/src/themes/index.ts | 4 ++++ 3 files changed, 17 insertions(+), 10 deletions(-) create mode 100644 webui/src/themes/DefaultDark.ts rename webui/src/{theme.ts => themes/DefaultLight.ts} (51%) create mode 100644 webui/src/themes/index.ts diff --git a/webui/src/themes/DefaultDark.ts b/webui/src/themes/DefaultDark.ts new file mode 100644 index 0000000000000000000000000000000000000000..8d961caa8809b43565ec89f10b927a94609c0708 --- /dev/null +++ b/webui/src/themes/DefaultDark.ts @@ -0,0 +1,12 @@ +import { createMuiTheme } from '@material-ui/core/styles'; + +const defaultDarkTheme = createMuiTheme({ + palette: { + type: 'dark', + primary: { + main: '#263238', + }, + }, +}); + +export default defaultDarkTheme; diff --git a/webui/src/theme.ts b/webui/src/themes/DefaultLight.ts similarity index 51% rename from webui/src/theme.ts rename to webui/src/themes/DefaultLight.ts index 67c24526d34fb2562d7c7ede0c4de701e20c3901..3a404fd5aa4a71c781980a7956353d84a0f25ee5 100644 --- a/webui/src/theme.ts +++ b/webui/src/themes/DefaultLight.ts @@ -9,13 +9,4 @@ const defaultLightTheme = createMuiTheme({ }, }); -const defaultDarkTheme = createMuiTheme({ - palette: { - type: 'dark', - primary: { - main: '#263238', - }, - }, -}); - -export { defaultLightTheme, defaultDarkTheme }; +export default defaultLightTheme; diff --git a/webui/src/themes/index.ts b/webui/src/themes/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..6c41c546fc58887c5dadd18ccae6f60b478aaa6b --- /dev/null +++ b/webui/src/themes/index.ts @@ -0,0 +1,4 @@ +import defaultDarkTheme from './DefaultDark'; +import defaultLightTheme from './DefaultLight'; + +export { defaultLightTheme, defaultDarkTheme }; From 9f6dcc887d31ce5c9cb5438b4e0e6c834c9d81d6 Mon Sep 17 00:00:00 2001 From: Sascha Date: Tue, 2 Mar 2021 17:13:11 +0100 Subject: [PATCH 050/157] Use proper semantic color values Adjust header colors on light theme - Use adjusted background-color for header instead of text.hint. - Use slightly darker secondary font color for better readability against the head background color. Use more semantic theme colors for bug list Use more semantic theme colors for bug messages Fix usage of text hint for filter header --- webui/src/pages/bug/Message.tsx | 6 ++++-- webui/src/pages/list/FilterToolbar.tsx | 4 ++-- webui/src/pages/list/ListQuery.tsx | 21 ++++++++++++--------- webui/src/themes/DefaultDark.ts | 8 ++++++++ webui/src/themes/DefaultLight.ts | 7 +++++++ 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx index 0cc9b15a83ba5d61dbe8b7d55f7213935cd356f4..f2f62b52b771380cd479d672b3059140cb619206 100644 --- a/webui/src/pages/bug/Message.tsx +++ b/webui/src/pages/bug/Message.tsx @@ -1,7 +1,7 @@ import React from 'react'; import Paper from '@material-ui/core/Paper'; -import { fade, makeStyles } from '@material-ui/core/styles'; +import { makeStyles } from '@material-ui/core/styles'; import Author, { Avatar } from 'src/components/Author'; import Content from 'src/components/Content'; @@ -31,7 +31,9 @@ const useStyles = makeStyles((theme) => ({ padding: '0.5rem 1rem', borderBottom: `1px solid ${theme.palette.divider}`, display: 'flex', - backgroundColor: fade(theme.palette.text.hint, 0.05), + borderTopRightRadius: theme.shape.borderRadius, + borderTopLeftRadius: theme.shape.borderRadius, + backgroundColor: theme.palette.info.main, }, title: { flex: 1, diff --git a/webui/src/pages/list/FilterToolbar.tsx b/webui/src/pages/list/FilterToolbar.tsx index 6847397444dca459670d79250a273cf34af3210d..e4cd8e6a244a338c74d26f073e1c181acb3ac3d8 100644 --- a/webui/src/pages/list/FilterToolbar.tsx +++ b/webui/src/pages/list/FilterToolbar.tsx @@ -3,7 +3,7 @@ import { LocationDescriptor } from 'history'; import React from 'react'; import Toolbar from '@material-ui/core/Toolbar'; -import { fade, makeStyles } from '@material-ui/core/styles'; +import { makeStyles } from '@material-ui/core/styles'; import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline'; import ErrorOutline from '@material-ui/icons/ErrorOutline'; @@ -19,7 +19,7 @@ import { useBugCountQuery } from './FilterToolbar.generated'; const useStyles = makeStyles((theme) => ({ toolbar: { - backgroundColor: fade(theme.palette.text.hint, 0.05), + backgroundColor: theme.palette.primary.light, borderColor: theme.palette.divider, borderWidth: '1px 0', borderStyle: 'solid', diff --git a/webui/src/pages/list/ListQuery.tsx b/webui/src/pages/list/ListQuery.tsx index 58660b329d0636db46778cc877db8adafdb3bd11..d4ce7f803b67819cc0f87786697f204578615551 100644 --- a/webui/src/pages/list/ListQuery.tsx +++ b/webui/src/pages/list/ListQuery.tsx @@ -6,7 +6,7 @@ import { Button } from '@material-ui/core'; import IconButton from '@material-ui/core/IconButton'; import InputBase from '@material-ui/core/InputBase'; import Paper from '@material-ui/core/Paper'; -import { fade, makeStyles, Theme } from '@material-ui/core/styles'; +import { makeStyles, Theme } from '@material-ui/core/styles'; import ErrorOutline from '@material-ui/icons/ErrorOutline'; import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft'; import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight'; @@ -56,10 +56,11 @@ const useStyles = makeStyles((theme) => ({ }, search: { borderRadius: theme.shape.borderRadius, + color: theme.palette.text.secondary, borderColor: theme.palette.divider, borderStyle: 'solid', borderWidth: '1px', - backgroundColor: fade(theme.palette.text.hint, 0.05), + backgroundColor: theme.palette.primary.light, padding: theme.spacing(0, 1), width: ({ searching }) => (searching ? '20rem' : '15rem'), transition: theme.transitions.create([ @@ -69,13 +70,11 @@ const useStyles = makeStyles((theme) => ({ ]), }, searchFocused: { - borderColor: fade(theme.palette.primary.main, 0.4), backgroundColor: theme.palette.background.paper, - width: '20rem!important', }, placeholderRow: { padding: theme.spacing(1), - borderBottomColor: theme.palette.grey['300'], + borderBottomColor: theme.palette.divider, borderBottomWidth: '1px', borderBottomStyle: 'solid', display: 'flex', @@ -91,7 +90,8 @@ const useStyles = makeStyles((theme) => ({ ...theme.typography.h5, padding: theme.spacing(8), textAlign: 'center', - borderBottomColor: theme.palette.grey['300'], + color: theme.palette.text.hint, + borderBottomColor: theme.palette.divider, borderBottomWidth: '1px', borderBottomStyle: 'solid', '& > p': { @@ -99,12 +99,15 @@ const useStyles = makeStyles((theme) => ({ }, }, errorBox: { - color: theme.palette.error.main, + color: theme.palette.error.dark, '& > pre': { fontSize: '1rem', textAlign: 'left', - backgroundColor: theme.palette.grey['900'], - color: theme.palette.common.white, + borderColor: theme.palette.divider, + borderWidth: '1px', + borderRadius: theme.shape.borderRadius, + borderStyle: 'solid', + color: theme.palette.text.primary, marginTop: theme.spacing(4), padding: theme.spacing(2, 3), }, diff --git a/webui/src/themes/DefaultDark.ts b/webui/src/themes/DefaultDark.ts index 8d961caa8809b43565ec89f10b927a94609c0708..fe31f2117eb06aa94781c28fdfe4bd8d9b92228a 100644 --- a/webui/src/themes/DefaultDark.ts +++ b/webui/src/themes/DefaultDark.ts @@ -5,6 +5,14 @@ const defaultDarkTheme = createMuiTheme({ type: 'dark', primary: { main: '#263238', + light: '#525252', + }, + error: { + main: '#f44336', + dark: '#ff4949', + }, + info: { + main: '#2a393e', }, }, }); diff --git a/webui/src/themes/DefaultLight.ts b/webui/src/themes/DefaultLight.ts index 3a404fd5aa4a71c781980a7956353d84a0f25ee5..898bd0bc1a6a7ea53c506d042d0522866f077350 100644 --- a/webui/src/themes/DefaultLight.ts +++ b/webui/src/themes/DefaultLight.ts @@ -5,6 +5,13 @@ const defaultLightTheme = createMuiTheme({ type: 'light', primary: { main: '#263238', + light: '#f5f5f5', + }, + info: { + main: '#e2f1ff', + }, + text: { + secondary: '#555', }, }, }); From c8a5330bdeabbf2c83890ca096281cd09159cc10 Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 3 Mar 2021 13:02:27 +0100 Subject: [PATCH 051/157] Fix padding around issue comments --- webui/src/pages/bug/Message.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx index f2f62b52b771380cd479d672b3059140cb619206..d27f7a3da75011e11ebe2d8b7ff62b3b0b0e7198 100644 --- a/webui/src/pages/bug/Message.tsx +++ b/webui/src/pages/bug/Message.tsx @@ -49,7 +49,7 @@ const useStyles = makeStyles((theme) => ({ }, body: { ...theme.typography.body2, - padding: '0 1rem', + padding: '0.5rem', }, })); From b918c9facd157d1dfb24282f6309782a10e02123 Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 3 Mar 2021 14:28:45 +0100 Subject: [PATCH 052/157] Revert restructuring between App.tsx and index.tsx This should prevent unnecessary merge conflicts. --- webui/src/index.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/webui/src/index.tsx b/webui/src/index.tsx index a7f52448a1dd8091ccb51cf961e65d31389c1faa..d3591e1a7d629573b200fc2e8d4625e5b2832f5e 100644 --- a/webui/src/index.tsx +++ b/webui/src/index.tsx @@ -1,13 +1,20 @@ import { ApolloProvider } from '@apollo/client'; import React from 'react'; import ReactDOM from 'react-dom'; +import { BrowserRouter } from 'react-router-dom'; import App from './App'; import apolloClient from './apollo'; +import Themer from './components/Themer'; +import { defaultLightTheme, defaultDarkTheme } from './themes/index'; ReactDOM.render( - + + + + + , document.getElementById('root') ); From c24b7370f484e89662283a52b66e34940f972cf6 Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 3 Mar 2021 16:26:57 +0100 Subject: [PATCH 053/157] Add Icon to "Close Issue"-button --- .../src/components/CloseBugButton/CloseBugButton.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/webui/src/components/CloseBugButton/CloseBugButton.tsx b/webui/src/components/CloseBugButton/CloseBugButton.tsx index 19f56cab6c9df8a57b66ff85c73489a09bd1c3a9..8d397c23d60b8fcd46f5135b45d0646345456411 100644 --- a/webui/src/components/CloseBugButton/CloseBugButton.tsx +++ b/webui/src/components/CloseBugButton/CloseBugButton.tsx @@ -1,12 +1,21 @@ import React from 'react'; import Button from '@material-ui/core/Button'; +import { makeStyles, Theme } from '@material-ui/core/styles'; +import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'; import { BugFragment } from 'src/pages/bug/Bug.generated'; import { TimelineDocument } from 'src/pages/bug/TimelineQuery.generated'; import { useCloseBugMutation } from './CloseBug.generated'; +const useStyles = makeStyles((theme: Theme) => ({ + closeIssueIcon: { + color: theme.palette.secondary.dark, + paddingTop: '0.1rem', + }, +})); + interface Props { bug: BugFragment; disabled: boolean; @@ -14,6 +23,7 @@ interface Props { function CloseBugButton({ bug, disabled }: Props) { const [closeBug, { loading, error }] = useCloseBugMutation(); + const classes = useStyles(); function closeBugAction() { closeBug({ @@ -45,6 +55,7 @@ function CloseBugButton({ bug, disabled }: Props) { variant="contained" onClick={() => closeBugAction()} disabled={bug.status === 'CLOSED' || disabled} + startIcon={} > Close issue From 8e8ca99bd5f72de60f6e0e1b334f33312cab5388 Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 3 Mar 2021 17:49:53 +0100 Subject: [PATCH 054/157] Use colors from theme palette - Use theme colors for title input. - Remove inputTitle classes as they are not applied to the TextField. This will lead to double borders and artifacts at the field corners. --- .../BugTitleForm/.BugTitleForm.tsx.swp | Bin 0 -> 20480 bytes .../components/BugTitleForm/BugTitleForm.tsx | 24 +++--------------- webui/src/pages/list/ListQuery.tsx | 11 +++----- webui/src/pages/new/NewBugPage.tsx | 23 +++-------------- webui/src/themes/DefaultDark.ts | 5 ++++ webui/src/themes/DefaultLight.ts | 4 +++ 6 files changed, 19 insertions(+), 48 deletions(-) create mode 100644 webui/src/components/BugTitleForm/.BugTitleForm.tsx.swp diff --git a/webui/src/components/BugTitleForm/.BugTitleForm.tsx.swp b/webui/src/components/BugTitleForm/.BugTitleForm.tsx.swp new file mode 100644 index 0000000000000000000000000000000000000000..125bed1102632dc6d3342a125b36283027706373 GIT binary patch literal 20480 zcmeI2dyE}b9mfZYwIYuKm8ijZy2RYO+wHx(tp&W@Tc9n~#lGlPFxCcUXU@HM+PO2s z%$aVlo0VV`B2`i!@f8px7#~5Y2|iGaA`uM&iP33C>L?OZN@0>Hwy~{TG z2Y)a(``LSE&hz&@kD2p3XLjPM>0RvN#s&kQ)rRrdPcFIkcek%G9&Q>&WJXrcq=1+I zh@Rse4ov&yV;RPWREEttKRg@-rp24c-}l`npR=N<8HD~Jepr~{EO$h+>xlKUakn|g zXJe-sg;vw@`+@IqPejeFad*EXT)x8(`-O{!h>k8-jVioS;MJ!HAcD zy-R;@EQPPo*Dd}1$x^t!tiCD*Dg`P9Dg`P9Dg`P9Dg`P9Dg`P9Dg`P9Dg|Bv1!&j) zE2kO8N8f`10Mr7 zfg8bx!L{IG@D6Y~_~k0YI0p8DN$^In2K@VVhVdtG2lzC&9t=PX1h@)Z2x?#rSOK11 zX&Co|Z-cLa52A;<$J<(JZ=}mBjK441KIL&Ixc8)O1HKS;^+2`$f`IAQtZaYFn-KO&o z7cxtl*-yrNZ(Gmwx=38-M{tg%#Y}H7%Gz64#Km-Kr3!4|AWWBw#u3wv`Dor)c_%MR zKCq8ej@KTsxfk)gJ)PKfuXb$FOD$q7mBx>STUKU>t%AFBr?3OU7hv z=J__?oGaF$9QN@Jbf6%qz{-I`Jw{DOeJnXmR3ByY)N7SJXcL@2l6hQ?h)7WHm~O;J zl_|w0L(GO$M~0>)Sf*!jSG9UE_l41h=y9)(yQ=+%m!%71%$zeF(u@nMcV)~&hes_Y z!kCY1l@2C9Q2*>_=>o<(VT6u1IWdusgjTa#jEr#X{C(L$pb*N+8i=v$P=P^r#S;{POv&r`F*}kX6;=ncqXE2&sriK5G7YQ^V^E)h5b~#>G8>4Teh91!O zbC%_PV)RqTY?Zs78ar977z>cGb5vQ{i~FWGoikT<%goS4wxh*mZ>THloDQq6bs{=~ zc9rQmc71ddhEOltjM}DMpx7Hrogg(=c23#7C^1@%adfp*blrPPLrdA0Op06B1iM6b z3D#n{tf-!)gw)S-RV)~xhJedOSxq@xQSC5~Mna889$|RL5uXtQbdtL4tt$9HR^)hb zdOMgTL3gMZ$Bj;!2tCf`nd_T2`o%coVd#f!AxW*E ztkX8znmb-tpQmxb7Fuj?=m(LMZmemrF?MNShGt)e&TyAog4^=K@tEsCp}_3o@ixY` zCFd3yCNddAc2|b8m3SB!&9O1VpeYB%v>1xs5t>~ZPMZzK6FltTprk6Bnr2~CTB>{h z!O^65*Je%Ya5wa0&)(*{ept%_{1CHb>E;|g33MD0nqDL)ILs(L-sg?%PSmhMP8uCR z1sUr*o_vP1*v5?)suFPSlNu;VO6NLpS)UkZ6S{Jxno;Mpd0mwhnCNMOH-gZ?lWWlE z;{lD7jfqW3Vp)4#Xl*h$sw+J!@5_GIVztP}04#(^drzvU4TX~Sp}WO2wMrqPo*D3l zDocfs(X#pE!%fW=x-rrjX_3J-(r(e+Oo3&BNuVgUEFMKlAk+bEA)oX+c?Z+EuC3O7 zQd-mvIrr3?Nuiocr%RN`T6shAn7{zd)iB?-)Ls{azC>q^NZekQZpKj zCjK%*lGh`8iy0J5G7`Bi?W68kp1A9A#@+HmvV*bxX~(7Bm%a|uMb>n5MH4A~t5H6l zNPx$L7GobbEkTb_+5DtnQZfdCB~Tiz|KE%?>szt*rS*TZfByj1_1^+t1joPvpf!I8 zt^-$s?chz|H1IR5?{5GhI0&u)tHFP;hW{hD4+vm`J>Vj+7SQ_sH{b;L4EP8*1onYT zzy@$WI18)-r-BvWajfA_fP27Q;Cj#oXM#V&mhXelg8;lA>;hW>t@Ag779g9?2i2!i zpi-bxpi-bxpi-bxpi+Po;70@e(JQ~rJ7(+(@=eGk5V^eYPES4y?Vp@#!aJV0x(gSK zO*OOk*(3_XXG*@&lT)xgkN1A6B+B;t6wTE-C0n@$y%k}Ye-K1Jn0Ne+>MMf*m61f$CUK6hM-v5 zcUFwIQ#sVI+D#%Ow$f8KCN){EIq5Q66eUKZ0a;;JVo@rJK{wUnB%e-&z(KEEjE1^S z{x6Q%gV(k_()9RTenUgWPq#48ioTKjNUX+O(O}gq!DUkEhXv@wgb? z1AG=72OkH=Kp*S^TfjQ-7VrYr_`e3f06zw__wNG^wt{uwT<`+c`?UA}7w~&-Po+SmK&3#XKv4mD z$f=Kh;Yu!-dd-|MvlzpEE&1LyG(g_WvKbnue5Of7PoplMdifYHb7pF}#7pT!>C!qH zSuV>Q(UK8QDxJ9i*mF?)le5~}lKukiV?>R4F$cuRV|r1%z|Uldfw!m49o u+8aX$6{=%l=t>a+HN;B ({ marginLeft: theme.spacing(2), }, greenButton: { - marginLeft: '8px', - backgroundColor: '#2ea44fd9', - color: '#fff', - '&:hover': { - backgroundColor: '#2ea44f', - }, - }, - titleInput: { - borderRadius: theme.shape.borderRadius, - borderColor: fade(theme.palette.primary.main, 0.2), - borderStyle: 'solid', - borderWidth: '1px', - backgroundColor: fade(theme.palette.primary.main, 0.05), - padding: theme.spacing(0, 0), - minWidth: 336, - transition: theme.transitions.create([ - 'width', - 'borderColor', - 'backgroundColor', - ]), + marginLeft: theme.spacing(1), + backgroundColor: theme.palette.success.main, + color: theme.palette.success.contrastText, }, })); @@ -126,7 +109,6 @@ function BugTitleForm({ bug }: Props) { inputRef={(node) => { issueTitleInput = node; }} - className={classes.titleInput} variant="outlined" fullWidth margin="dense" diff --git a/webui/src/pages/list/ListQuery.tsx b/webui/src/pages/list/ListQuery.tsx index d4ce7f803b67819cc0f87786697f204578615551..021f70b0ce7d343b3dfe1685c6c808a4cdf1a58d 100644 --- a/webui/src/pages/list/ListQuery.tsx +++ b/webui/src/pages/list/ListQuery.tsx @@ -112,12 +112,9 @@ const useStyles = makeStyles((theme) => ({ padding: theme.spacing(2, 3), }, }, - greenButton: { - backgroundColor: '#2ea44fd9', - color: '#fff', - '&:hover': { - backgroundColor: '#2ea44f', - }, + newIssueButton: { + backgroundColor: theme.palette.success.main, + color: theme.palette.success.contrastText, }, })); @@ -318,7 +315,7 @@ function ListQuery() { {() => ( )} diff --git a/webui/src/components/CloseBugButton/CloseBugButton.tsx b/webui/src/components/CloseBugButton/CloseBugButton.tsx index 8d397c23d60b8fcd46f5135b45d0646345456411..9f098483a8fc2450c6e50aea1845ee3e37a3474c 100644 --- a/webui/src/components/CloseBugButton/CloseBugButton.tsx +++ b/webui/src/components/CloseBugButton/CloseBugButton.tsx @@ -57,7 +57,7 @@ function CloseBugButton({ bug, disabled }: Props) { disabled={bug.status === 'CLOSED' || disabled} startIcon={} > - Close issue + Close bug ); diff --git a/webui/src/components/ReopenBugButton/ReopenBugButton.tsx b/webui/src/components/ReopenBugButton/ReopenBugButton.tsx index 195ca5125e6b99be39d32828d0ec269afc171dd4..e3e792fc2e2bc50add8e1fb5c6380d1a1d2ac144 100644 --- a/webui/src/components/ReopenBugButton/ReopenBugButton.tsx +++ b/webui/src/components/ReopenBugButton/ReopenBugButton.tsx @@ -46,7 +46,7 @@ function ReopenBugButton({ bug, disabled }: Props) { onClick={() => openBugAction()} disabled={bug.status === 'OPEN' || disabled} > - Reopen issue + Reopen bug ); diff --git a/webui/src/pages/list/ListQuery.tsx b/webui/src/pages/list/ListQuery.tsx index 3a58f803a94be0f19293ad96b2c70588633959ea..500ccf77dec04992955147f35dc4b1127306b71e 100644 --- a/webui/src/pages/list/ListQuery.tsx +++ b/webui/src/pages/list/ListQuery.tsx @@ -323,7 +323,7 @@ function ListQuery() { variant="contained" href="/new" > - New issue + New bug )} diff --git a/webui/src/pages/new/NewBugPage.tsx b/webui/src/pages/new/NewBugPage.tsx index 4517c6e30d355b56e4ded1856f5b6dfe6de5a7e7..96afb56a8a1d05720f7272e65d36f4429c21e62b 100644 --- a/webui/src/pages/new/NewBugPage.tsx +++ b/webui/src/pages/new/NewBugPage.tsx @@ -94,7 +94,7 @@ function NewBugPage() { type="submit" disabled={isFormValid() ? false : true} > - Submit new issue + Submit new bug 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 068/157] 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 069/157] 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 c223c75c11f3ec903413207dd275152e1ec97fcc Mon Sep 17 00:00:00 2001 From: Lena Date: Sun, 28 Feb 2021 00:18:03 +0100 Subject: [PATCH 070/157] Add back-to-list-button #10 --- webui/src/components/Header/Header.tsx | 7 +++++++ webui/src/pages/bug/CommentForm.tsx | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/webui/src/components/Header/Header.tsx b/webui/src/components/Header/Header.tsx index 3bdb252f4aa0ef308b5ccc2af4f635b1229c874c..cdac0f0e539b5f460f5128dce66fc4f99efcc7a0 100644 --- a/webui/src/components/Header/Header.tsx +++ b/webui/src/components/Header/Header.tsx @@ -29,6 +29,13 @@ const useStyles = makeStyles((theme) => ({ height: '42px', marginRight: theme.spacing(2), }, + greenButton: { + backgroundColor: '#2ea44fd9', + color: '#fff', + '&:hover': { + backgroundColor: '#2ea44f', + }, + }, })); function Header() { diff --git a/webui/src/pages/bug/CommentForm.tsx b/webui/src/pages/bug/CommentForm.tsx index 0b97e133323c732aee7b8d33078d5c0cf6111248..6ec9bf2a10cf6095b0dc19f2f6a92d5a5f12be12 100644 --- a/webui/src/pages/bug/CommentForm.tsx +++ b/webui/src/pages/bug/CommentForm.tsx @@ -28,6 +28,7 @@ const useStyles = makeStyles((theme) => ({ }, actions: { display: 'flex', + gap: '1em', justifyContent: 'flex-end', }, greenButton: { @@ -38,6 +39,13 @@ const useStyles = makeStyles((theme) => ({ backgroundColor: '#2ea44f', }, }, + backButton: { + backgroundColor: '#574142', + color: '#fff', + '&:hover': { + backgroundColor: '#610B0B', + }, + }, })); type Props = { @@ -101,6 +109,9 @@ function CommentForm({ bug }: Props) { onChange={(comment: string) => setIssueComment(comment)} />
+ {bug.status === 'OPEN' ? getCloseButton() : getReopenButton()}
diff --git a/webui/src/pages/bug/CommentForm.tsx b/webui/src/pages/bug/CommentForm.tsx index 6ec9bf2a10cf6095b0dc19f2f6a92d5a5f12be12..e53c7ddd0b163e216f1e1078cd39bf355a4ce863 100644 --- a/webui/src/pages/bug/CommentForm.tsx +++ b/webui/src/pages/bug/CommentForm.tsx @@ -39,13 +39,6 @@ const useStyles = makeStyles((theme) => ({ backgroundColor: '#2ea44f', }, }, - backButton: { - backgroundColor: '#574142', - color: '#fff', - '&:hover': { - backgroundColor: '#610B0B', - }, - }, })); type Props = { @@ -109,9 +102,6 @@ function CommentForm({ bug }: Props) { onChange={(comment: string) => setIssueComment(comment)} />
- {bug.status === 'OPEN' ? getCloseButton() : getReopenButton()} +
@@ -98,8 +118,8 @@ function Bug({ bug }: Props) { )}
-
- Labels +
+ Labels
    {bug.labels.length === 0 && ( None yet @@ -110,15 +130,6 @@ function Bug({ bug }: Props) { ))}
-
diff --git a/webui/src/pages/bug/BugQuery.tsx b/webui/src/pages/bug/BugQuery.tsx index 2a70a2f84f676f29c72837a028cc96f5c9e117b6..ade64e9d015600dfb8498f753e64ed3729475c1e 100644 --- a/webui/src/pages/bug/BugQuery.tsx +++ b/webui/src/pages/bug/BugQuery.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { RouteComponentProps } from 'react-router-dom'; +import { Redirect, RouteComponentProps } from 'react-router-dom'; import CircularProgress from '@material-ui/core/CircularProgress'; @@ -15,8 +15,8 @@ const BugQuery: React.FC = ({ match }: Props) => { variables: { id: match.params.id }, }); if (loading) return ; + if (!data?.repository?.bug) return ; if (error) return

Error: {error}

; - if (!data?.repository?.bug) return

404.

; return ; }; From 46d38aa53fa925a2335fdf6c3b0b6f47d05dce94 Mon Sep 17 00:00:00 2001 From: Lena Date: Tue, 2 Mar 2021 20:15:39 +0100 Subject: [PATCH 075/157] Route instead of Redirect from empty bug to 404 after a hint from GM #10 --- return-404-page.patch | 37 ++++++++++++++++++++++++++++++++ webui/src/App.tsx | 1 - webui/src/pages/bug/BugQuery.tsx | 6 ++++-- 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 return-404-page.patch diff --git a/return-404-page.patch b/return-404-page.patch new file mode 100644 index 0000000000000000000000000000000000000000..e512d6229a18d1cb1a6450023855f62217f679bf --- /dev/null +++ b/return-404-page.patch @@ -0,0 +1,37 @@ +diff --git a/webui/src/App.tsx b/webui/src/App.tsx +index 3e8f71e..4fd0993 100644 +--- a/webui/src/App.tsx ++++ b/webui/src/App.tsx +@@ -11,7 +11,6 @@ export default function App() { + + + +- + + + +diff --git a/webui/src/pages/bug/BugQuery.tsx b/webui/src/pages/bug/BugQuery.tsx +index ade64e9..5d459c4 100644 +--- a/webui/src/pages/bug/BugQuery.tsx ++++ b/webui/src/pages/bug/BugQuery.tsx +@@ -1,8 +1,10 @@ + import React from 'react'; +-import { Redirect, RouteComponentProps } from 'react-router-dom'; ++import { RouteComponentProps } from 'react-router-dom'; + + import CircularProgress from '@material-ui/core/CircularProgress'; + ++import NotFoundPage from '../notfound/NotFoundPage'; ++ + import Bug from './Bug'; + import { useGetBugQuery } from './BugQuery.generated'; + +@@ -15,7 +17,7 @@ const BugQuery: React.FC = ({ match }: Props) => { + variables: { id: match.params.id }, + }); + if (loading) return ; +- if (!data?.repository?.bug) return ; ++ if (!data?.repository?.bug) return ; + if (error) return

Error: {error}

; + return ; + }; diff --git a/webui/src/App.tsx b/webui/src/App.tsx index e0580b1df6fbd7ec7e3051996cbf6a36c9d30a0e..4c81913cbdd5b346f62f64830ae34854af22a2d0 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -13,7 +13,6 @@ export default function App() { - diff --git a/webui/src/pages/bug/BugQuery.tsx b/webui/src/pages/bug/BugQuery.tsx index ade64e9d015600dfb8498f753e64ed3729475c1e..5d459c429a737fc812fe7615c629c2684ade06ca 100644 --- a/webui/src/pages/bug/BugQuery.tsx +++ b/webui/src/pages/bug/BugQuery.tsx @@ -1,8 +1,10 @@ import React from 'react'; -import { Redirect, RouteComponentProps } from 'react-router-dom'; +import { RouteComponentProps } from 'react-router-dom'; import CircularProgress from '@material-ui/core/CircularProgress'; +import NotFoundPage from '../notfound/NotFoundPage'; + import Bug from './Bug'; import { useGetBugQuery } from './BugQuery.generated'; @@ -15,7 +17,7 @@ const BugQuery: React.FC = ({ match }: Props) => { variables: { id: match.params.id }, }); if (loading) return ; - if (!data?.repository?.bug) return ; + if (!data?.repository?.bug) return ; if (error) return

Error: {error}

; return ; }; From de26990afc9f21a9017b039ba9d7546a8d8ea5da Mon Sep 17 00:00:00 2001 From: Sascha Date: Thu, 11 Mar 2021 12:47:09 +0100 Subject: [PATCH 076/157] Adjust BackToList button color for dark/light-mode The AppBar is commonly used for navigation. As the BackToList button is a navigation element, use similar colors as the AppBar. --- webui/src/components/Header/Header.tsx | 8 ++++++-- webui/src/pages/bug/Bug.tsx | 7 ++++--- webui/src/themes/DefaultDark.ts | 3 ++- webui/src/themes/DefaultLight.ts | 4 +++- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/webui/src/components/Header/Header.tsx b/webui/src/components/Header/Header.tsx index 3bdb252f4aa0ef308b5ccc2af4f635b1229c874c..579bf127f01edc6e0ab26df57c74d5884ee5be7d 100644 --- a/webui/src/components/Header/Header.tsx +++ b/webui/src/components/Header/Header.tsx @@ -15,9 +15,13 @@ const useStyles = makeStyles((theme) => ({ filler: { flexGrow: 1, }, + appBar: { + backgroundColor: theme.palette.primary.dark, + color: theme.palette.primary.contrastText, + }, appTitle: { ...theme.typography.h6, - color: 'white', + color: theme.palette.primary.contrastText, textDecoration: 'none', display: 'flex', alignItems: 'center', @@ -36,7 +40,7 @@ function Header() { return ( <> - + git-bug diff --git a/webui/src/pages/bug/Bug.tsx b/webui/src/pages/bug/Bug.tsx index 8b537fb8cef5f5035e6ba8a8cdaf4f623a5f5caf..343721b4e502ea1a0a888a05dae3fe8c2694e4ea 100644 --- a/webui/src/pages/bug/Bug.tsx +++ b/webui/src/pages/bug/Bug.tsx @@ -76,10 +76,11 @@ const useStyles = makeStyles((theme) => ({ backButton: { position: 'sticky', top: '80px', - backgroundColor: '#574142', - color: '#fff', + backgroundColor: theme.palette.primary.dark, + color: theme.palette.primary.contrastText, '&:hover': { - backgroundColor: '#610B0B', + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, }, }, })); diff --git a/webui/src/themes/DefaultDark.ts b/webui/src/themes/DefaultDark.ts index 6a92ec49df522346db64a2228165c7ec8ba760fd..65dd6329b44ed0b334d21c8bce712efc3b715844 100644 --- a/webui/src/themes/DefaultDark.ts +++ b/webui/src/themes/DefaultDark.ts @@ -4,7 +4,8 @@ const defaultDarkTheme = createMuiTheme({ palette: { type: 'dark', primary: { - main: '#263238', + dark: '#263238', + main: '#2a393e', light: '#525252', }, error: { diff --git a/webui/src/themes/DefaultLight.ts b/webui/src/themes/DefaultLight.ts index bc788a986634e35ca9682a05808f6d292da39867..9c57ebe57ce904a8df3f346848c219784ff7d595 100644 --- a/webui/src/themes/DefaultLight.ts +++ b/webui/src/themes/DefaultLight.ts @@ -4,8 +4,10 @@ const defaultLightTheme = createMuiTheme({ palette: { type: 'light', primary: { - main: '#263238', + dark: '#263238', + main: '#5a6b73', light: '#f5f5f5', + contrastText: '#fff', }, info: { main: '#e2f1ff', From 07f3163296b187ddf7069c05ca94f5ebaf43413c Mon Sep 17 00:00:00 2001 From: Lena Date: Thu, 11 Mar 2021 22:27:08 +0100 Subject: [PATCH 077/157] #10 Add redirect to detail page after creating new bug --- webui/src/pages/new/NewBugPage.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/webui/src/pages/new/NewBugPage.tsx b/webui/src/pages/new/NewBugPage.tsx index a46226ad29fbc92059a2a93177bc16e5cac774ed..9ad52ad08ae9d767c43fa79c778e5999a6e915fd 100644 --- a/webui/src/pages/new/NewBugPage.tsx +++ b/webui/src/pages/new/NewBugPage.tsx @@ -1,4 +1,5 @@ import React, { FormEvent, useState } from 'react'; +import { useHistory } from 'react-router-dom'; import { Button } from '@material-ui/core'; import Paper from '@material-ui/core/Paper'; @@ -43,7 +44,9 @@ function NewBugPage() { const [issueTitle, setIssueTitle] = useState(''); const [issueComment, setIssueComment] = useState(''); const classes = useStyles(); + let issueTitleInput: any; + let history = useHistory(); function submitNewIssue(e: FormEvent) { e.preventDefault(); @@ -55,6 +58,9 @@ function NewBugPage() { message: issueComment, }, }, + }).then(function (data) { + const id = data.data?.newBug.bug.humanId; + history.push('/bug/' + id); }); issueTitleInput.value = ''; } From cd02d80ca2458be40d64d2e945670e0aeeb30fcc Mon Sep 17 00:00:00 2001 From: Lena Date: Sat, 13 Mar 2021 23:12:24 +0100 Subject: [PATCH 078/157] Make BackButton a component and Add it to NewBugPage #10 --- webui/src/pages/bug/BackButton.tsx | 36 ++++++++++++ webui/src/pages/bug/Bug.tsx | 23 +------- webui/src/pages/new/NewBugPage.tsx | 90 ++++++++++++++++++++---------- 3 files changed, 97 insertions(+), 52 deletions(-) create mode 100644 webui/src/pages/bug/BackButton.tsx diff --git a/webui/src/pages/bug/BackButton.tsx b/webui/src/pages/bug/BackButton.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4c73dd0aedafa9ce2e2bb6e854a14f3cf785a804 --- /dev/null +++ b/webui/src/pages/bug/BackButton.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import Button from '@material-ui/core/Button'; +import { makeStyles } from '@material-ui/core/styles'; +import ArrowBackIcon from '@material-ui/icons/ArrowBack'; + +const useStyles = makeStyles((theme) => ({ + backButton: { + position: 'sticky', + top: '80px', + backgroundColor: theme.palette.primary.dark, + color: theme.palette.primary.contrastText, + '&:hover': { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + }, + }, +})); + +function BackButton() { + const classes = useStyles(); + + return ( + + ); +} + +export default BackButton; diff --git a/webui/src/pages/bug/Bug.tsx b/webui/src/pages/bug/Bug.tsx index 343721b4e502ea1a0a888a05dae3fe8c2694e4ea..aa6247f2f5594cb872fe00177be9634270d6797f 100644 --- a/webui/src/pages/bug/Bug.tsx +++ b/webui/src/pages/bug/Bug.tsx @@ -1,13 +1,12 @@ import React from 'react'; -import Button from '@material-ui/core/Button'; import { makeStyles } from '@material-ui/core/styles'; -import ArrowBackIcon from '@material-ui/icons/ArrowBack'; import BugTitleForm from 'src/components/BugTitleForm/BugTitleForm'; import IfLoggedIn from 'src/components/IfLoggedIn/IfLoggedIn'; import Label from 'src/components/Label'; +import BackButton from './BackButton'; import { BugFragment } from './Bug.generated'; import CommentForm from './CommentForm'; import TimelineQuery from './TimelineQuery'; @@ -73,16 +72,6 @@ const useStyles = makeStyles((theme) => ({ commentForm: { marginLeft: 48, }, - backButton: { - position: 'sticky', - top: '80px', - backgroundColor: theme.palette.primary.dark, - color: theme.palette.primary.contrastText, - '&:hover': { - backgroundColor: theme.palette.primary.main, - color: theme.palette.primary.contrastText, - }, - }, })); type Props = { @@ -99,15 +88,7 @@ function Bug({ bug }: Props) {
- +
diff --git a/webui/src/pages/new/NewBugPage.tsx b/webui/src/pages/new/NewBugPage.tsx index 9ad52ad08ae9d767c43fa79c778e5999a6e915fd..d04e753a2585bd20abdcb2f9f3819fa402f85054 100644 --- a/webui/src/pages/new/NewBugPage.tsx +++ b/webui/src/pages/new/NewBugPage.tsx @@ -1,12 +1,12 @@ import React, { FormEvent, useState } from 'react'; import { useHistory } from 'react-router-dom'; -import { Button } from '@material-ui/core'; -import Paper from '@material-ui/core/Paper'; +import { Button, Paper } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; import BugTitleInput from '../../components/BugTitleForm/BugTitleInput'; import CommentInput from '../../components/CommentInput/CommentInput'; +import BackButton from '../bug/BackButton'; import { useNewBugMutation } from './NewBug.generated'; @@ -15,12 +15,17 @@ import { useNewBugMutation } from './NewBug.generated'; */ const useStyles = makeStyles((theme: Theme) => ({ main: { - maxWidth: 800, + maxWidth: 1200, margin: 'auto', marginTop: theme.spacing(4), marginBottom: theme.spacing(4), padding: theme.spacing(2), - overflow: 'hidden', + }, + container: { + display: 'flex', + marginBottom: theme.spacing(1), + marginRight: theme.spacing(2), + marginLeft: theme.spacing(2), }, form: { display: 'flex', @@ -34,6 +39,21 @@ const useStyles = makeStyles((theme: Theme) => ({ backgroundColor: theme.palette.success.main, color: theme.palette.success.contrastText, }, + leftSidebar: { + marginTop: theme.spacing(2), + marginRight: theme.spacing(2), + }, + rightSidebar: { + marginTop: theme.spacing(2), + flex: '0 0 200px', + }, + timeline: { + flex: 1, + marginTop: theme.spacing(2), + marginRight: theme.spacing(2), + minWidth: 400, + padding: theme.spacing(1), + }, })); /** @@ -73,34 +93,42 @@ function NewBugPage() { if (error) return
Error
; return ( - -
- { - issueTitleInput = node; - }} - label="Title" - variant="outlined" - fullWidth - margin="dense" - onChange={(event: any) => setIssueTitle(event.target.value)} - /> - setIssueComment(comment)} - /> -
- +
+
+
+
- - + +
+ { + issueTitleInput = node; + }} + label="Title" + variant="outlined" + fullWidth + margin="dense" + onChange={(event: any) => setIssueTitle(event.target.value)} + /> + setIssueComment(comment)} + /> +
+ +
+ +
+
+
+
); } From ac17596c1ab94b6426bb5b48e39ed0dd9038303e Mon Sep 17 00:00:00 2001 From: Lena Date: Mon, 15 Mar 2021 14:52:06 +0100 Subject: [PATCH 079/157] Refactor BackToListButton #10 --- .../BackToListButton/BackToListButton.tsx} | 4 ++-- webui/src/pages/bug/Bug.tsx | 4 ++-- webui/src/pages/new/NewBugPage.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename webui/src/{pages/bug/BackButton.tsx => components/BackToListButton/BackToListButton.tsx} (92%) diff --git a/webui/src/pages/bug/BackButton.tsx b/webui/src/components/BackToListButton/BackToListButton.tsx similarity index 92% rename from webui/src/pages/bug/BackButton.tsx rename to webui/src/components/BackToListButton/BackToListButton.tsx index 4c73dd0aedafa9ce2e2bb6e854a14f3cf785a804..7ca53ad05ae8595c7790500d7322d1a0e9aa0d21 100644 --- a/webui/src/pages/bug/BackButton.tsx +++ b/webui/src/components/BackToListButton/BackToListButton.tsx @@ -17,7 +17,7 @@ const useStyles = makeStyles((theme) => ({ }, })); -function BackButton() { +function BackToListButton() { const classes = useStyles(); return ( @@ -33,4 +33,4 @@ function BackButton() { ); } -export default BackButton; +export default BackToListButton; diff --git a/webui/src/pages/bug/Bug.tsx b/webui/src/pages/bug/Bug.tsx index aa6247f2f5594cb872fe00177be9634270d6797f..9ce2f6a67cb11720f1a5c62c558a02b6487a9a82 100644 --- a/webui/src/pages/bug/Bug.tsx +++ b/webui/src/pages/bug/Bug.tsx @@ -2,11 +2,11 @@ import React from 'react'; import { makeStyles } from '@material-ui/core/styles'; +import BackToListButton from '../../components/BackToListButton/BackToListButton'; import BugTitleForm from 'src/components/BugTitleForm/BugTitleForm'; import IfLoggedIn from 'src/components/IfLoggedIn/IfLoggedIn'; import Label from 'src/components/Label'; -import BackButton from './BackButton'; import { BugFragment } from './Bug.generated'; import CommentForm from './CommentForm'; import TimelineQuery from './TimelineQuery'; @@ -88,7 +88,7 @@ function Bug({ bug }: Props) {
- +
diff --git a/webui/src/pages/new/NewBugPage.tsx b/webui/src/pages/new/NewBugPage.tsx index d04e753a2585bd20abdcb2f9f3819fa402f85054..aa220e04199493179c130a25a4ebebaba5b7faf0 100644 --- a/webui/src/pages/new/NewBugPage.tsx +++ b/webui/src/pages/new/NewBugPage.tsx @@ -4,9 +4,9 @@ import { useHistory } from 'react-router-dom'; import { Button, Paper } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; +import BackToListButton from '../../components/BackToListButton/BackToListButton'; import BugTitleInput from '../../components/BugTitleForm/BugTitleInput'; import CommentInput from '../../components/CommentInput/CommentInput'; -import BackButton from '../bug/BackButton'; import { useNewBugMutation } from './NewBug.generated'; @@ -96,7 +96,7 @@ function NewBugPage() {
- +
From 09fabc98a357454b2c4da4f08ce269bb4106c36a Mon Sep 17 00:00:00 2001 From: Lena Date: Mon, 15 Mar 2021 15:40:25 +0100 Subject: [PATCH 080/157] Adjust Button #10 --- .../components/{BackToListButton => }/BackToListButton.tsx | 0 webui/src/pages/bug/Bug.tsx | 2 +- webui/src/pages/new/NewBugPage.tsx | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename webui/src/components/{BackToListButton => }/BackToListButton.tsx (100%) diff --git a/webui/src/components/BackToListButton/BackToListButton.tsx b/webui/src/components/BackToListButton.tsx similarity index 100% rename from webui/src/components/BackToListButton/BackToListButton.tsx rename to webui/src/components/BackToListButton.tsx diff --git a/webui/src/pages/bug/Bug.tsx b/webui/src/pages/bug/Bug.tsx index 9ce2f6a67cb11720f1a5c62c558a02b6487a9a82..bde8c5dd3e09ac30801d816cd1a2b300eb601328 100644 --- a/webui/src/pages/bug/Bug.tsx +++ b/webui/src/pages/bug/Bug.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { makeStyles } from '@material-ui/core/styles'; -import BackToListButton from '../../components/BackToListButton/BackToListButton'; +import BackToListButton from '../../components/BackToListButton'; import BugTitleForm from 'src/components/BugTitleForm/BugTitleForm'; import IfLoggedIn from 'src/components/IfLoggedIn/IfLoggedIn'; import Label from 'src/components/Label'; diff --git a/webui/src/pages/new/NewBugPage.tsx b/webui/src/pages/new/NewBugPage.tsx index aa220e04199493179c130a25a4ebebaba5b7faf0..39725722933d8cd6601d42651717207b9ac60637 100644 --- a/webui/src/pages/new/NewBugPage.tsx +++ b/webui/src/pages/new/NewBugPage.tsx @@ -4,7 +4,7 @@ import { useHistory } from 'react-router-dom'; import { Button, Paper } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; -import BackToListButton from '../../components/BackToListButton/BackToListButton'; +import BackToListButton from '../../components/BackToListButton'; import BugTitleInput from '../../components/BugTitleForm/BugTitleInput'; import CommentInput from '../../components/CommentInput/CommentInput'; @@ -40,7 +40,7 @@ const useStyles = makeStyles((theme: Theme) => ({ color: theme.palette.success.contrastText, }, leftSidebar: { - marginTop: theme.spacing(2), + marginTop: theme.spacing(4), marginRight: theme.spacing(2), }, rightSidebar: { From 833f020a9b97656941434a5e9bc5248ee0caeb76 Mon Sep 17 00:00:00 2001 From: Sascha Date: Mon, 15 Mar 2021 16:09:40 +0100 Subject: [PATCH 081/157] Use BackToList button on 404-Page --- webui/src/pages/notfound/NotFoundPage.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/webui/src/pages/notfound/NotFoundPage.tsx b/webui/src/pages/notfound/NotFoundPage.tsx index 57b186c59255389564192263353e6c7a0c9d7139..2c6f68540029055944c15cb4ee4e758d3c1c197a 100644 --- a/webui/src/pages/notfound/NotFoundPage.tsx +++ b/webui/src/pages/notfound/NotFoundPage.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { makeStyles } from '@material-ui/core/styles'; -import ArrowBackIcon from '@material-ui/icons/ArrowBack'; + +import BackToListButton from '../../components/BackToListButton'; const useStyles = makeStyles((theme) => ({ main: { @@ -22,8 +23,8 @@ const useStyles = makeStyles((theme) => ({ fontSize: '80px', }, backLink: { - textDecoration: 'none', - color: theme.palette.text.primary, + marginTop: theme.spacing(1), + textAlign: 'center', }, header: { fontSize: '30px', @@ -41,10 +42,9 @@ function NotFoundPage() { className={classes.logo} alt="git-bug Logo" /> - -

Go back to start page

- -
+
+ +
); } From 0b3acaa3211b1c884bd42f18bf58383343b42bc1 Mon Sep 17 00:00:00 2001 From: Sascha Date: Mon, 15 Mar 2021 21:28:47 +0100 Subject: [PATCH 082/157] Remove mistakenly commited patch file --- return-404-page.patch | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 return-404-page.patch diff --git a/return-404-page.patch b/return-404-page.patch deleted file mode 100644 index e512d6229a18d1cb1a6450023855f62217f679bf..0000000000000000000000000000000000000000 --- a/return-404-page.patch +++ /dev/null @@ -1,37 +0,0 @@ -diff --git a/webui/src/App.tsx b/webui/src/App.tsx -index 3e8f71e..4fd0993 100644 ---- a/webui/src/App.tsx -+++ b/webui/src/App.tsx -@@ -11,7 +11,6 @@ export default function App() { - - - -- - - - -diff --git a/webui/src/pages/bug/BugQuery.tsx b/webui/src/pages/bug/BugQuery.tsx -index ade64e9..5d459c4 100644 ---- a/webui/src/pages/bug/BugQuery.tsx -+++ b/webui/src/pages/bug/BugQuery.tsx -@@ -1,8 +1,10 @@ - import React from 'react'; --import { Redirect, RouteComponentProps } from 'react-router-dom'; -+import { RouteComponentProps } from 'react-router-dom'; - - import CircularProgress from '@material-ui/core/CircularProgress'; - -+import NotFoundPage from '../notfound/NotFoundPage'; -+ - import Bug from './Bug'; - import { useGetBugQuery } from './BugQuery.generated'; - -@@ -15,7 +17,7 @@ const BugQuery: React.FC = ({ match }: Props) => { - variables: { id: match.params.id }, - }); - if (loading) return ; -- if (!data?.repository?.bug) return ; -+ if (!data?.repository?.bug) return ; - if (error) return

Error: {error}

; - return ; - }; From 07e1c45cd70554630640bb1ea25968078a36fd6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Fri, 19 Mar 2021 17:04:59 +0100 Subject: [PATCH 083/157] webui: minor code fixes --- webui/src/components/BugTitleForm/BugTitleForm.tsx | 2 +- webui/src/components/Content/PreTag.tsx | 2 +- webui/src/components/Header/Header.tsx | 2 +- webui/src/pages/list/FilterToolbar.tsx | 2 +- webui/src/pages/new/NewBugPage.tsx | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/webui/src/components/BugTitleForm/BugTitleForm.tsx b/webui/src/components/BugTitleForm/BugTitleForm.tsx index c31f8ef7a892aecec8a81aa3d025ec9e2040437d..9a161443c55dfd570e639534ce16dd09877669f1 100644 --- a/webui/src/components/BugTitleForm/BugTitleForm.tsx +++ b/webui/src/components/BugTitleForm/BugTitleForm.tsx @@ -66,7 +66,7 @@ function BugTitleForm({ bug }: Props) { function isFormValid() { if (issueTitleInput) { - return issueTitleInput.value.length > 0 ? true : false; + return issueTitleInput.value.length > 0; } else { return false; } diff --git a/webui/src/components/Content/PreTag.tsx b/webui/src/components/Content/PreTag.tsx index 5256ab12ec8abacd56dded3153569d5136f6dac0..8e352153238c927c73c075e59c0eff61f3879991 100644 --- a/webui/src/components/Content/PreTag.tsx +++ b/webui/src/components/Content/PreTag.tsx @@ -11,7 +11,7 @@ const useStyles = makeStyles({ const PreTag = (props: React.HTMLProps) => { const classes = useStyles(); - return
;
+  return 
;
 };
 
 export default PreTag;
diff --git a/webui/src/components/Header/Header.tsx b/webui/src/components/Header/Header.tsx
index 579bf127f01edc6e0ab26df57c74d5884ee5be7d..3443fcf5997ecd59b5844c67e8062635948b56d6 100644
--- a/webui/src/components/Header/Header.tsx
+++ b/webui/src/components/Header/Header.tsx
@@ -46,7 +46,7 @@ function Header() {
             git-bug
             git-bug
           
-          
+
diff --git a/webui/src/pages/list/FilterToolbar.tsx b/webui/src/pages/list/FilterToolbar.tsx index e4cd8e6a244a338c74d26f073e1c181acb3ac3d8..74eefe4c3184ac27c3b7649576bdccb43d2a16f9 100644 --- a/webui/src/pages/list/FilterToolbar.tsx +++ b/webui/src/pages/list/FilterToolbar.tsx @@ -40,7 +40,7 @@ function CountingFilter({ query, children, ...props }: CountingFilterProps) { variables: { query }, }); - var prefix; + let prefix; if (loading) prefix = '...'; else if (error || !data?.repository) prefix = '???'; // TODO: better prefixes & error handling diff --git a/webui/src/pages/new/NewBugPage.tsx b/webui/src/pages/new/NewBugPage.tsx index 39725722933d8cd6601d42651717207b9ac60637..f313ac24e7c3b513e913e29632a69b5fba3ac280 100644 --- a/webui/src/pages/new/NewBugPage.tsx +++ b/webui/src/pages/new/NewBugPage.tsx @@ -86,7 +86,7 @@ function NewBugPage() { } function isFormValid() { - return issueTitle.length > 0 && issueComment.length > 0 ? true : false; + return issueTitle.length > 0; } if (loading) return
Loading...
; @@ -119,14 +119,14 @@ function NewBugPage() { className={classes.greenButton} variant="contained" type="submit" - disabled={isFormValid() ? false : true} + disabled={!isFormValid()} > Submit new issue
-
+
); From 0dcb48d03aae413d77c7321f461502fa54abe05f Mon Sep 17 00:00:00 2001 From: Sascha Date: Mon, 15 Mar 2021 13:15:06 +0100 Subject: [PATCH 084/157] Add EditButton to bug message --- webui/src/pages/bug/Message.tsx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx index faff5356bf47f541825d15a962ceac22b10a74a6..6b04059f315df15112f70164432974cdc231efdc 100644 --- a/webui/src/pages/bug/Message.tsx +++ b/webui/src/pages/bug/Message.tsx @@ -1,7 +1,10 @@ import React from 'react'; +import IconButton from '@material-ui/core/IconButton'; import Paper from '@material-ui/core/Paper'; +import Tooltip from '@material-ui/core/Tooltip/Tooltip'; import { makeStyles } from '@material-ui/core/styles'; +import EditIcon from '@material-ui/icons/Edit'; import Author, { Avatar } from 'src/components/Author'; import Content from 'src/components/Content'; @@ -51,6 +54,14 @@ const useStyles = makeStyles((theme) => ({ ...theme.typography.body2, padding: '0.5rem', }, + editButton: { + color: theme.palette.info.contrastText, + padding: '0rem', + fontSize: '0.75rem', + '&:hover': { + backgroundColor: 'inherit', + }, + }, })); type Props = { @@ -70,6 +81,15 @@ function Message({ op }: Props) {
{op.edited &&
Edited
} + + + + +
From 79cc9884b3d1b93c2a17b6a1e7d5cc2c7f5f7c0f Mon Sep 17 00:00:00 2001 From: Sascha Date: Tue, 16 Mar 2021 14:11:06 +0100 Subject: [PATCH 085/157] GraphQL: Resolve new EditComment mutation --- api/graphql/resolvers/mutation.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/api/graphql/resolvers/mutation.go b/api/graphql/resolvers/mutation.go index 642a4fb981f604aeedb7ed0e6858b56d985c0ddd..59e93cdddf6ecde65abb1c26fc370a31331350ef 100644 --- a/api/graphql/resolvers/mutation.go +++ b/api/graphql/resolvers/mutation.go @@ -89,6 +89,34 @@ func (r mutationResolver) AddComment(ctx context.Context, input models.AddCommen }, nil } +func (r mutationResolver) EditComment(ctx context.Context, input models.EditCommentInput) (*models.EditCommentPayload, error) { + repo, b, err := r.getBug(input.RepoRef, input.Prefix) + if err != nil { + return nil, err + } + + author, err := auth.UserFromCtx(ctx, repo) + if err != nil { + return nil, err + } + + op, err := b.EditCommentRaw(author, time.Now().Unix(), input.Message, input.Files, nil) + if err != nil { + return nil, err + } + + err = b.Commit() + if err != nil { + return nil, err + } + + return &models.EditCommentPayload{ + ClientMutationID: input.ClientMutationID, + Bug: models.NewLoadedBug(b.Snapshot()), + Operation: op, + }, nil +} + func (r mutationResolver) ChangeLabels(ctx context.Context, input *models.ChangeLabelInput) (*models.ChangeLabelPayload, error) { repo, b, err := r.getBug(input.RepoRef, input.Prefix) if err != nil { From 19a68deabb2a047795436e8e5d0ce9a8618fd01a Mon Sep 17 00:00:00 2001 From: Sascha Date: Tue, 16 Mar 2021 14:13:49 +0100 Subject: [PATCH 086/157] GraphQL: Add EditComment to mutation type --- api/graphql/schema/root.graphql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/graphql/schema/root.graphql b/api/graphql/schema/root.graphql index 94a0b5309af76c11f5b41031c033bd7d65ffa0d9..884fd98db4170774eec7b06bf760e2bf7e91c78e 100644 --- a/api/graphql/schema/root.graphql +++ b/api/graphql/schema/root.graphql @@ -8,6 +8,8 @@ type Mutation { newBug(input: NewBugInput!): NewBugPayload! """Add a new comment to a bug""" addComment(input: AddCommentInput!): AddCommentPayload! + """Change a comment of a bug""" + editComment(input: EditCommentInput!): EditCommentPayload! """Add or remove a set of label on a bug""" changeLabels(input: ChangeLabelInput): ChangeLabelPayload! """Change a bug's status to open""" From 4960448ac7efb13ea8cb6687d05c0ee766c02763 Mon Sep 17 00:00:00 2001 From: Sascha Date: Tue, 16 Mar 2021 14:21:11 +0100 Subject: [PATCH 087/157] GraphQL: Add EditComment mutation to schema --- api/graphql/schema/mutations.graphql | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/api/graphql/schema/mutations.graphql b/api/graphql/schema/mutations.graphql index e6b70fafa57ccd9e8c10d0d11918e2439c26fedc..1544fe6728d75d506a2d6df28b63a7b946583d7e 100644 --- a/api/graphql/schema/mutations.graphql +++ b/api/graphql/schema/mutations.graphql @@ -42,6 +42,28 @@ type AddCommentPayload { operation: AddCommentOperation! } +input EditCommentInput { + """A unique identifier for the client performing the mutation.""" + clientMutationId: String + """"The name of the repository. If not set, the default repository is used.""" + repoRef: String + """The bug ID's prefix.""" + prefix: String! + """The new message to be set.""" + message: String! + """The collection of file's hash required for the first message.""" + files: [Hash!] +} + +type EditCommentPayload { + """A unique identifier for the client performing the mutation.""" + clientMutationId: String + """The affected bug.""" + bug: Bug! + """The resulting operation.""" + operation: EditCommentOperation! +} + input ChangeLabelInput { """A unique identifier for the client performing the mutation.""" clientMutationId: String From 50cd1a9c177120a78e0c7f4e2f4238d2debfbfc6 Mon Sep 17 00:00:00 2001 From: Sascha Date: Tue, 16 Mar 2021 14:31:28 +0100 Subject: [PATCH 088/157] GraphQL: Add EditComment input/payload to gen_models --- api/graphql/models/gen_models.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/api/graphql/models/gen_models.go b/api/graphql/models/gen_models.go index 675c4b6b59f24dfdf570d3d0b5b428cd2a59145c..5bf8ea9ec232a96bf79bc56d67c8f7e41f5d7555 100644 --- a/api/graphql/models/gen_models.go +++ b/api/graphql/models/gen_models.go @@ -38,6 +38,28 @@ type AddCommentPayload struct { Operation *bug.AddCommentOperation `json:"operation"` } +type EditCommentInput struct { + // A unique identifier for the client performing the mutation. + ClientMutationID *string `json:"clientMutationId"` + // "The name of the repository. If not set, the default repository is used. + RepoRef *string `json:"repoRef"` + // The bug ID's prefix. + Prefix string `json:"prefix"` + // The first message of the new bug. + Message string `json:"message"` + // The collection of file's hash required for the first message. + Files []repository.Hash `json:"files"` +} + +type EditCommentPayload struct { + // A unique identifier for the client performing the mutation. + ClientMutationID *string `json:"clientMutationId"` + // The affected bug. + Bug BugWrapper `json:"bug"` + // The resulting operation. + Operation *bug.EditCommentOperation `json:"operation"` +} + // The connection type for Bug. type BugConnection struct { // A list of edges. From c6d15bd52b415b6fbc7a63e6072472402a1491d6 Mon Sep 17 00:00:00 2001 From: Sascha Date: Tue, 16 Mar 2021 15:16:24 +0100 Subject: [PATCH 089/157] Fix compilation errors --- api/graphql/models/gen_models.go | 5 ++++- api/graphql/resolvers/mutation.go | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/api/graphql/models/gen_models.go b/api/graphql/models/gen_models.go index 5bf8ea9ec232a96bf79bc56d67c8f7e41f5d7555..5a120f572deb7d30900bbd4c3a1db6c5541df6ae 100644 --- a/api/graphql/models/gen_models.go +++ b/api/graphql/models/gen_models.go @@ -8,6 +8,7 @@ import ( "strconv" "github.com/MichaelMure/git-bug/bug" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/repository" ) @@ -45,7 +46,9 @@ type EditCommentInput struct { RepoRef *string `json:"repoRef"` // The bug ID's prefix. Prefix string `json:"prefix"` - // The first message of the new bug. + // Target + Target entity.Id `json:"target"` + // The new message to be set. Message string `json:"message"` // The collection of file's hash required for the first message. Files []repository.Hash `json:"files"` diff --git a/api/graphql/resolvers/mutation.go b/api/graphql/resolvers/mutation.go index 59e93cdddf6ecde65abb1c26fc370a31331350ef..60746b8bd58d3f06c9f5796db202f18c892a9a99 100644 --- a/api/graphql/resolvers/mutation.go +++ b/api/graphql/resolvers/mutation.go @@ -100,7 +100,7 @@ func (r mutationResolver) EditComment(ctx context.Context, input models.EditComm return nil, err } - op, err := b.EditCommentRaw(author, time.Now().Unix(), input.Message, input.Files, nil) + op, err := b.EditCommentRaw(author, time.Now().Unix(), input.Target, input.Message, nil) if err != nil { return nil, err } From cc7788ad44bbf6d3a272c15cd8858fb6dc3c1536 Mon Sep 17 00:00:00 2001 From: Sascha Date: Tue, 16 Mar 2021 17:29:56 +0100 Subject: [PATCH 090/157] GraphQL: Add target to EditCommentInput --- api/graphql/resolvers/mutation.go | 3 ++- api/graphql/schema/mutations.graphql | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/api/graphql/resolvers/mutation.go b/api/graphql/resolvers/mutation.go index 60746b8bd58d3f06c9f5796db202f18c892a9a99..9cd936a68e34f29ed2af24caa39f4a858c8f7b28 100644 --- a/api/graphql/resolvers/mutation.go +++ b/api/graphql/resolvers/mutation.go @@ -5,6 +5,7 @@ import ( "time" "github.com/MichaelMure/git-bug/api/auth" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/api/graphql/graph" "github.com/MichaelMure/git-bug/api/graphql/models" "github.com/MichaelMure/git-bug/bug" @@ -100,7 +101,7 @@ func (r mutationResolver) EditComment(ctx context.Context, input models.EditComm return nil, err } - op, err := b.EditCommentRaw(author, time.Now().Unix(), input.Target, input.Message, nil) + op, err := b.EditCommentRaw(author, time.Now().Unix(), entity.Id(input.Target), input.Message, nil) if err != nil { return nil, err } diff --git a/api/graphql/schema/mutations.graphql b/api/graphql/schema/mutations.graphql index 1544fe6728d75d506a2d6df28b63a7b946583d7e..f520991799abeebdf8d8fade3ec73d25258391aa 100644 --- a/api/graphql/schema/mutations.graphql +++ b/api/graphql/schema/mutations.graphql @@ -49,6 +49,8 @@ input EditCommentInput { repoRef: String """The bug ID's prefix.""" prefix: String! + """The target.""" + target: String! """The new message to be set.""" message: String! """The collection of file's hash required for the first message.""" From 2a1c77236c1c601971ab71f076c85529d1d60a72 Mon Sep 17 00:00:00 2001 From: Sascha Date: Tue, 16 Mar 2021 17:41:25 +0100 Subject: [PATCH 091/157] GrapQL: Regenerate the GraphQL-Server --- api/graphql/graph/gen_graph.go | 340 +++++++++++++++++++++++++++++++ api/graphql/models/gen_models.go | 49 +++-- go.sum | 2 + 3 files changed, 366 insertions(+), 25 deletions(-) diff --git a/api/graphql/graph/gen_graph.go b/api/graphql/graph/gen_graph.go index 3ff86c3fd5a49017528a78812350281aea99f17a..b70e70d8e3f83614f452fcbfc1e3415702a82387 100644 --- a/api/graphql/graph/gen_graph.go +++ b/api/graphql/graph/gen_graph.go @@ -193,6 +193,12 @@ type ComplexityRoot struct { Target func(childComplexity int) int } + EditCommentPayload struct { + Bug func(childComplexity int) int + ClientMutationID func(childComplexity int) int + Operation func(childComplexity int) int + } + Identity struct { AvatarUrl func(childComplexity int) int DisplayName func(childComplexity int) int @@ -258,6 +264,7 @@ type ComplexityRoot struct { AddComment func(childComplexity int, input models.AddCommentInput) int ChangeLabels func(childComplexity int, input *models.ChangeLabelInput) int CloseBug func(childComplexity int, input models.CloseBugInput) int + EditComment func(childComplexity int, input models.EditCommentInput) int NewBug func(childComplexity int, input models.NewBugInput) int OpenBug func(childComplexity int, input models.OpenBugInput) int SetTitle func(childComplexity int, input models.SetTitleInput) int @@ -433,6 +440,7 @@ type LabelChangeTimelineItemResolver interface { type MutationResolver interface { NewBug(ctx context.Context, input models.NewBugInput) (*models.NewBugPayload, error) AddComment(ctx context.Context, input models.AddCommentInput) (*models.AddCommentPayload, error) + EditComment(ctx context.Context, input models.EditCommentInput) (*models.EditCommentPayload, error) ChangeLabels(ctx context.Context, input *models.ChangeLabelInput) (*models.ChangeLabelPayload, error) OpenBug(ctx context.Context, input models.OpenBugInput) (*models.OpenBugPayload, error) CloseBug(ctx context.Context, input models.CloseBugInput) (*models.CloseBugPayload, error) @@ -1059,6 +1067,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.EditCommentOperation.Target(childComplexity), true + case "EditCommentPayload.bug": + if e.complexity.EditCommentPayload.Bug == nil { + break + } + + return e.complexity.EditCommentPayload.Bug(childComplexity), true + + case "EditCommentPayload.clientMutationId": + if e.complexity.EditCommentPayload.ClientMutationID == nil { + break + } + + return e.complexity.EditCommentPayload.ClientMutationID(childComplexity), true + + case "EditCommentPayload.operation": + if e.complexity.EditCommentPayload.Operation == nil { + break + } + + return e.complexity.EditCommentPayload.Operation(childComplexity), true + case "Identity.avatarUrl": if e.complexity.Identity.AvatarUrl == nil { break @@ -1333,6 +1362,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.CloseBug(childComplexity, args["input"].(models.CloseBugInput)), true + case "Mutation.editComment": + if e.complexity.Mutation.EditComment == nil { + break + } + + args, err := ec.field_Mutation_editComment_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.EditComment(childComplexity, args["input"].(models.EditCommentInput)), true + case "Mutation.newBug": if e.complexity.Mutation.NewBug == nil { break @@ -2034,6 +2075,30 @@ type AddCommentPayload { operation: AddCommentOperation! } +input EditCommentInput { + """A unique identifier for the client performing the mutation.""" + clientMutationId: String + """"The name of the repository. If not set, the default repository is used.""" + repoRef: String + """The bug ID's prefix.""" + prefix: String! + """The target.""" + target: String! + """The new message to be set.""" + message: String! + """The collection of file's hash required for the first message.""" + files: [Hash!] +} + +type EditCommentPayload { + """A unique identifier for the client performing the mutation.""" + clientMutationId: String + """The affected bug.""" + bug: Bug! + """The resulting operation.""" + operation: EditCommentOperation! +} + input ChangeLabelInput { """A unique identifier for the client performing the mutation.""" clientMutationId: String @@ -2290,6 +2355,8 @@ type Mutation { newBug(input: NewBugInput!): NewBugPayload! """Add a new comment to a bug""" addComment(input: AddCommentInput!): AddCommentPayload! + """Change a comment of a bug""" + editComment(input: EditCommentInput!): EditCommentPayload! """Add or remove a set of label on a bug""" changeLabels(input: ChangeLabelInput): ChangeLabelPayload! """Change a bug's status to open""" @@ -2657,6 +2724,20 @@ func (ec *executionContext) field_Mutation_closeBug_args(ctx context.Context, ra return args, nil } +func (ec *executionContext) field_Mutation_editComment_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 models.EditCommentInput + if tmp, ok := rawArgs["input"]; ok { + arg0, err = ec.unmarshalNEditCommentInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐEditCommentInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_newBug_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -5591,6 +5672,105 @@ func (ec *executionContext) _EditCommentOperation_files(ctx context.Context, fie return ec.marshalNHash2ᚕgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋrepositoryᚐHashᚄ(ctx, field.Selections, res) } +func (ec *executionContext) _EditCommentPayload_clientMutationId(ctx context.Context, field graphql.CollectedField, obj *models.EditCommentPayload) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "EditCommentPayload", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ClientMutationID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) _EditCommentPayload_bug(ctx context.Context, field graphql.CollectedField, obj *models.EditCommentPayload) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "EditCommentPayload", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Bug, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(models.BugWrapper) + fc.Result = res + return ec.marshalNBug2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐBugWrapper(ctx, field.Selections, res) +} + +func (ec *executionContext) _EditCommentPayload_operation(ctx context.Context, field graphql.CollectedField, obj *models.EditCommentPayload) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "EditCommentPayload", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Operation, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*bug.EditCommentOperation) + fc.Result = res + return ec.marshalNEditCommentOperation2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋbugᚐEditCommentOperation(ctx, field.Selections, res) +} + func (ec *executionContext) _Identity_id(ctx context.Context, field graphql.CollectedField, obj models.IdentityWrapper) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -6817,6 +6997,47 @@ func (ec *executionContext) _Mutation_addComment(ctx context.Context, field grap return ec.marshalNAddCommentPayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐAddCommentPayload(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_editComment(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_editComment_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().EditComment(rctx, args["input"].(models.EditCommentInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*models.EditCommentPayload) + fc.Result = res + return ec.marshalNEditCommentPayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐEditCommentPayload(ctx, field.Selections, res) +} + func (ec *executionContext) _Mutation_changeLabels(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -9971,6 +10192,54 @@ func (ec *executionContext) unmarshalInputCloseBugInput(ctx context.Context, obj return it, nil } +func (ec *executionContext) unmarshalInputEditCommentInput(ctx context.Context, obj interface{}) (models.EditCommentInput, error) { + var it models.EditCommentInput + var asMap = obj.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "clientMutationId": + var err error + it.ClientMutationID, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + case "repoRef": + var err error + it.RepoRef, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + case "prefix": + var err error + it.Prefix, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + case "target": + var err error + it.Target, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + case "message": + var err error + it.Message, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + case "files": + var err error + it.Files, err = ec.unmarshalOHash2ᚕgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋrepositoryᚐHashᚄ(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputNewBugInput(ctx context.Context, obj interface{}) (models.NewBugInput, error) { var it models.NewBugInput var asMap = obj.(map[string]interface{}) @@ -11254,6 +11523,40 @@ func (ec *executionContext) _EditCommentOperation(ctx context.Context, sel ast.S return out } +var editCommentPayloadImplementors = []string{"EditCommentPayload"} + +func (ec *executionContext) _EditCommentPayload(ctx context.Context, sel ast.SelectionSet, obj *models.EditCommentPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, editCommentPayloadImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("EditCommentPayload") + case "clientMutationId": + out.Values[i] = ec._EditCommentPayload_clientMutationId(ctx, field, obj) + case "bug": + out.Values[i] = ec._EditCommentPayload_bug(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "operation": + out.Values[i] = ec._EditCommentPayload_operation(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var identityImplementors = []string{"Identity"} func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet, obj models.IdentityWrapper) graphql.Marshaler { @@ -11734,6 +12037,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "editComment": + out.Values[i] = ec._Mutation_editComment(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "changeLabels": out.Values[i] = ec._Mutation_changeLabels(ctx, field) if out.Values[i] == graphql.Null { @@ -13130,6 +13438,38 @@ func (ec *executionContext) marshalNCreateOperation2ᚖgithubᚗcomᚋMichaelMur return ec._CreateOperation(ctx, sel, v) } +func (ec *executionContext) unmarshalNEditCommentInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐEditCommentInput(ctx context.Context, v interface{}) (models.EditCommentInput, error) { + return ec.unmarshalInputEditCommentInput(ctx, v) +} + +func (ec *executionContext) marshalNEditCommentOperation2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋbugᚐEditCommentOperation(ctx context.Context, sel ast.SelectionSet, v bug.EditCommentOperation) graphql.Marshaler { + return ec._EditCommentOperation(ctx, sel, &v) +} + +func (ec *executionContext) marshalNEditCommentOperation2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋbugᚐEditCommentOperation(ctx context.Context, sel ast.SelectionSet, v *bug.EditCommentOperation) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._EditCommentOperation(ctx, sel, v) +} + +func (ec *executionContext) marshalNEditCommentPayload2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐEditCommentPayload(ctx context.Context, sel ast.SelectionSet, v models.EditCommentPayload) graphql.Marshaler { + return ec._EditCommentPayload(ctx, sel, &v) +} + +func (ec *executionContext) marshalNEditCommentPayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐEditCommentPayload(ctx context.Context, sel ast.SelectionSet, v *models.EditCommentPayload) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._EditCommentPayload(ctx, sel, v) +} + func (ec *executionContext) unmarshalNHash2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋrepositoryᚐHash(ctx context.Context, v interface{}) (repository.Hash, error) { var res repository.Hash return res, res.UnmarshalGQL(v) diff --git a/api/graphql/models/gen_models.go b/api/graphql/models/gen_models.go index 5a120f572deb7d30900bbd4c3a1db6c5541df6ae..1046d11a4d66540ee0ca89cf394f3cc29a392779 100644 --- a/api/graphql/models/gen_models.go +++ b/api/graphql/models/gen_models.go @@ -8,7 +8,6 @@ import ( "strconv" "github.com/MichaelMure/git-bug/bug" - "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/repository" ) @@ -39,30 +38,6 @@ type AddCommentPayload struct { Operation *bug.AddCommentOperation `json:"operation"` } -type EditCommentInput struct { - // A unique identifier for the client performing the mutation. - ClientMutationID *string `json:"clientMutationId"` - // "The name of the repository. If not set, the default repository is used. - RepoRef *string `json:"repoRef"` - // The bug ID's prefix. - Prefix string `json:"prefix"` - // Target - Target entity.Id `json:"target"` - // The new message to be set. - Message string `json:"message"` - // The collection of file's hash required for the first message. - Files []repository.Hash `json:"files"` -} - -type EditCommentPayload struct { - // A unique identifier for the client performing the mutation. - ClientMutationID *string `json:"clientMutationId"` - // The affected bug. - Bug BugWrapper `json:"bug"` - // The resulting operation. - Operation *bug.EditCommentOperation `json:"operation"` -} - // The connection type for Bug. type BugConnection struct { // A list of edges. @@ -136,6 +111,30 @@ type CommentEdge struct { Node *bug.Comment `json:"node"` } +type EditCommentInput struct { + // A unique identifier for the client performing the mutation. + ClientMutationID *string `json:"clientMutationId"` + // "The name of the repository. If not set, the default repository is used. + RepoRef *string `json:"repoRef"` + // The bug ID's prefix. + Prefix string `json:"prefix"` + // The target. + Target string `json:"target"` + // The new message to be set. + Message string `json:"message"` + // The collection of file's hash required for the first message. + Files []repository.Hash `json:"files"` +} + +type EditCommentPayload struct { + // A unique identifier for the client performing the mutation. + ClientMutationID *string `json:"clientMutationId"` + // The affected bug. + Bug BugWrapper `json:"bug"` + // The resulting operation. + Operation *bug.EditCommentOperation `json:"operation"` +} + type IdentityConnection struct { Edges []*IdentityEdge `json:"edges"` Nodes []IdentityWrapper `json:"nodes"` diff --git a/go.sum b/go.sum index 960409299663ba87d32e3532ce61d7a5f23397a5..84a5b59dac52eb7c3f226ae68c5bddc6dc9e00c0 100644 --- a/go.sum +++ b/go.sum @@ -535,6 +535,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= @@ -685,6 +686,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= From 26ad5fc379749d7effc324ae36e778ee540053a7 Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 17 Mar 2021 11:37:04 +0100 Subject: [PATCH 092/157] Add onClick handler to edit button --- webui/src/pages/bug/Message.tsx | 6 ++++++ webui/src/pages/bug/MessageCommentFragment.graphql | 1 + webui/src/pages/bug/MessageCreateFragment.graphql | 1 + 3 files changed, 8 insertions(+) diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx index 6b04059f315df15112f70164432974cdc231efdc..390f369e5829ec5a41c7a6dfb8aef670344b1299 100644 --- a/webui/src/pages/bug/Message.tsx +++ b/webui/src/pages/bug/Message.tsx @@ -70,6 +70,11 @@ type Props = { function Message({ op }: Props) { const classes = useStyles(); + + const editComment = (id: String) => { + console.log(id); + }; + return (
@@ -86,6 +91,7 @@ function Message({ op }: Props) { disableRipple className={classes.editButton} aria-label="edit message" + onClick={() => editComment(op.id)} > diff --git a/webui/src/pages/bug/MessageCommentFragment.graphql b/webui/src/pages/bug/MessageCommentFragment.graphql index 00f8342d749d97256fa9d6624e1dfe3ab582f711..337eade0967384dcc0db81d3249d59245aef2a29 100644 --- a/webui/src/pages/bug/MessageCommentFragment.graphql +++ b/webui/src/pages/bug/MessageCommentFragment.graphql @@ -1,6 +1,7 @@ #import "../../components/fragments.graphql" fragment AddComment on AddCommentTimelineItem { + id createdAt ...authored edited diff --git a/webui/src/pages/bug/MessageCreateFragment.graphql b/webui/src/pages/bug/MessageCreateFragment.graphql index 4cae819db1a0131edafed4a9e7b95b3e2acfbe96..81f80afb6b659743de1c48fae64ae2ea0b947562 100644 --- a/webui/src/pages/bug/MessageCreateFragment.graphql +++ b/webui/src/pages/bug/MessageCreateFragment.graphql @@ -1,6 +1,7 @@ #import "../../components/fragments.graphql" fragment Create on CreateTimelineItem { + id createdAt ...authored edited From cd4b1adebbb009caba47b7dc4f543c4d951841f2 Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 17 Mar 2021 12:28:45 +0100 Subject: [PATCH 093/157] Pass BugFragment as prop to EditComment --- webui/src/pages/bug/.Bug.tsx.swp | Bin 0 -> 12288 bytes webui/src/pages/bug/.TimelineQuery.tsx.swp | Bin 0 -> 12288 bytes webui/src/pages/bug/Bug.tsx | 2 +- webui/src/pages/bug/Message.tsx | 13 ++++++++++++- webui/src/pages/bug/Timeline.tsx | 8 +++++--- webui/src/pages/bug/TimelineQuery.tsx | 9 +++++---- 6 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 webui/src/pages/bug/.Bug.tsx.swp create mode 100644 webui/src/pages/bug/.TimelineQuery.tsx.swp diff --git a/webui/src/pages/bug/.Bug.tsx.swp b/webui/src/pages/bug/.Bug.tsx.swp new file mode 100644 index 0000000000000000000000000000000000000000..4a312e0daab61c565eabd8c7b582a50e81add559 GIT binary patch literal 12288 zcmeI2&x;#n7{}j=sIAsY6+MXXZiP*<+L`RONXbqXZA)Qg`@6OWMV(~cOr|?C?=bUD zHz7p5i3ckPDqiZv_GCR-5kx4I1-*!15j}WOD%Ajevzxtc8GFqR)r(R@Dg|)RKYCR z32tsDwE ze4d2eUpm5lM!QTHx-^|Wr^|d>v}wIwr&T&TTbS60-AQvKYg%z4(a*azriBza7HgQ{ zDMz@hzm$X*jYnl5i<73x#^xp(OSsJNa%+X#HnWz;mbp>2;>|Kp*f_vFkNM&d4?T)> z)MZX4rFcq^&C&2&pwfGlZbXUAg}b|*(MO(c;G9Zo*Yu*rRa+F>Cq zNxJ$mISrIslBj7~Omp34rj_`WZVg$XlhP^ACsBq>#G#Ly4PhXvNTk**qIDWa?4;V;6;+=!?*NG>Ig#VM!M}wQ#?jVUFE~PNfESt;9B8&qZ^WbzoRU zBUz|cgHE>QG@c0ySV&fwxhr_`9RG}Ci8j(x&6bgRIgtiaR4Vl|P;=@;l(v!p=cO)# znki13yDv7I;x?k#Jg1|ZLxz;-G_1`D5#7do>CIdWdQXKu0RiQ5)AB61=I2=0a`_rE z)^;q5`L{~f-og!uVP-7&a8M^p*nBn3m@IWUUs~P6nnpQ#=IWrp%Spg#k3`kY>3|-H zB8v9$>`9uo0iw^%;d_pk{990LldX_@w4~{Uv!%s>alWq)OmpW(hP{0EpnWmIQbCEtmIB}w2uI#QN*l0Kc7`5XxsKsk;NU#_qg^CKTt{x literal 0 HcmV?d00001 diff --git a/webui/src/pages/bug/.TimelineQuery.tsx.swp b/webui/src/pages/bug/.TimelineQuery.tsx.swp new file mode 100644 index 0000000000000000000000000000000000000000..0ad00f67d95d7c71c00c040dcd1cfd134b06d80a GIT binary patch literal 12288 zcmeI2%Wl&^6o#j)DXjpB?I|qUNUfcgO_ekaqJRVvh$1d45EFainmV?b8PislVaE~{ z>{x-|5fCdLf`#6jQjuPT|UC`Y~^twrm=nlse{OMHTwIuM zmLx8mpQ1CT?;Ud}JU$a(0!)AjFaajO1egF5I9>#7IwoFVl@eO(py@Q@Yx1dRA z0us>nDItDAKcO$sODKapXcf8vU4d4hbI>+={DwY3AE7tUYv>gup-0d|sPysx;)@9| z0Vco%m;e)C0!)AjFaaj;4-rtCX`(IpsxPz1(!PVqZ6S)bAkxZaI;N%3CMBuqG-CML9r-s(Ci z*mdmil(bXDRmICXO6x>7c3h>)SIdPK<<&;9`mJ(UC-m!;XHti<7b%0zNUA5wY1nd>DmSy$Y*b*# z%%~B->e%;8F6x|ps|cLQ%RW@J2rg68pw2Qn)UN^?ExMcq*R%`6S*+wEzGB literal 0 HcmV?d00001 diff --git a/webui/src/pages/bug/Bug.tsx b/webui/src/pages/bug/Bug.tsx index d85c52966139658fe79ef1d1d9ac7fcdc6a06401..46a443d5649b4604653488980af86d08beb0ca4f 100644 --- a/webui/src/pages/bug/Bug.tsx +++ b/webui/src/pages/bug/Bug.tsx @@ -78,7 +78,7 @@ function Bug({ bug }: Props) {
- + {() => (
diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx index 390f369e5829ec5a41c7a6dfb8aef670344b1299..3993b5f71d64ee2e53f32901fc6a803a04638fee 100644 --- a/webui/src/pages/bug/Message.tsx +++ b/webui/src/pages/bug/Message.tsx @@ -9,7 +9,10 @@ import EditIcon from '@material-ui/icons/Edit'; import Author, { Avatar } from 'src/components/Author'; import Content from 'src/components/Content'; import Date from 'src/components/Date'; +import IfLoggedIn from 'src/components/IfLoggedIn/IfLoggedIn'; +import { BugFragment } from './Bug.generated'; +import CommentForm from './CommentForm'; import { AddCommentFragment } from './MessageCommentFragment.generated'; import { CreateFragment } from './MessageCreateFragment.generated'; @@ -65,10 +68,11 @@ const useStyles = makeStyles((theme) => ({ })); type Props = { + bug: BugFragment; op: AddCommentFragment | CreateFragment; }; -function Message({ op }: Props) { +function Message({ bug, op }: Props) { const classes = useStyles(); const editComment = (id: String) => { @@ -101,6 +105,13 @@ function Message({ op }: Props) {
+ + {() => ( +
+ +
+ )} +
); } diff --git a/webui/src/pages/bug/Timeline.tsx b/webui/src/pages/bug/Timeline.tsx index 6e1d242e074f6abdd4608029300ccd82c2956e90..60459a532505375c8d8a50a6b8e84dbc165beb39 100644 --- a/webui/src/pages/bug/Timeline.tsx +++ b/webui/src/pages/bug/Timeline.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { makeStyles } from '@material-ui/core/styles'; +import { BugFragment } from './Bug.generated'; import LabelChange from './LabelChange'; import Message from './Message'; import SetStatus from './SetStatus'; @@ -18,9 +19,10 @@ const useStyles = makeStyles((theme) => ({ type Props = { ops: Array; + bug: BugFragment; }; -function Timeline({ ops }: Props) { +function Timeline({ bug, ops }: Props) { const classes = useStyles(); return ( @@ -28,9 +30,9 @@ function Timeline({ ops }: Props) { {ops.map((op, index) => { switch (op.__typename) { case 'CreateTimelineItem': - return ; + return ; case 'AddCommentTimelineItem': - return ; + return ; case 'LabelChangeTimelineItem': return ; case 'SetTitleTimelineItem': diff --git a/webui/src/pages/bug/TimelineQuery.tsx b/webui/src/pages/bug/TimelineQuery.tsx index 74eed52b213d12c7166515f2b6b7408a0fdaff8b..d66c665b83b2f019fcbe8c58df781d3fd7e63ad3 100644 --- a/webui/src/pages/bug/TimelineQuery.tsx +++ b/webui/src/pages/bug/TimelineQuery.tsx @@ -2,17 +2,18 @@ import React from 'react'; import CircularProgress from '@material-ui/core/CircularProgress'; +import { BugFragment } from './Bug.generated'; import Timeline from './Timeline'; import { useTimelineQuery } from './TimelineQuery.generated'; type Props = { - id: string; + bug: BugFragment; }; -const TimelineQuery = ({ id }: Props) => { +const TimelineQuery = ({ bug }: Props) => { const { loading, error, data } = useTimelineQuery({ variables: { - id, + id: bug.id, first: 100, }, }); @@ -25,7 +26,7 @@ const TimelineQuery = ({ id }: Props) => { return null; } - return ; + return ; }; export default TimelineQuery; From d82c481e00a729c9736ac3f297347b23201a4080 Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 17 Mar 2021 12:30:38 +0100 Subject: [PATCH 094/157] Fix remove .swp files --- webui/src/pages/bug/.Bug.tsx.swp | Bin 12288 -> 0 bytes webui/src/pages/bug/.TimelineQuery.tsx.swp | Bin 12288 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 webui/src/pages/bug/.Bug.tsx.swp delete mode 100644 webui/src/pages/bug/.TimelineQuery.tsx.swp diff --git a/webui/src/pages/bug/.Bug.tsx.swp b/webui/src/pages/bug/.Bug.tsx.swp deleted file mode 100644 index 4a312e0daab61c565eabd8c7b582a50e81add559..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI2&x;#n7{}j=sIAsY6+MXXZiP*<+L`RONXbqXZA)Qg`@6OWMV(~cOr|?C?=bUD zHz7p5i3ckPDqiZv_GCR-5kx4I1-*!15j}WOD%Ajevzxtc8GFqR)r(R@Dg|)RKYCR z32tsDwE ze4d2eUpm5lM!QTHx-^|Wr^|d>v}wIwr&T&TTbS60-AQvKYg%z4(a*azriBza7HgQ{ zDMz@hzm$X*jYnl5i<73x#^xp(OSsJNa%+X#HnWz;mbp>2;>|Kp*f_vFkNM&d4?T)> z)MZX4rFcq^&C&2&pwfGlZbXUAg}b|*(MO(c;G9Zo*Yu*rRa+F>Cq zNxJ$mISrIslBj7~Omp34rj_`WZVg$XlhP^ACsBq>#G#Ly4PhXvNTk**qIDWa?4;V;6;+=!?*NG>Ig#VM!M}wQ#?jVUFE~PNfESt;9B8&qZ^WbzoRU zBUz|cgHE>QG@c0ySV&fwxhr_`9RG}Ci8j(x&6bgRIgtiaR4Vl|P;=@;l(v!p=cO)# znki13yDv7I;x?k#Jg1|ZLxz;-G_1`D5#7do>CIdWdQXKu0RiQ5)AB61=I2=0a`_rE z)^;q5`L{~f-og!uVP-7&a8M^p*nBn3m@IWUUs~P6nnpQ#=IWrp%Spg#k3`kY>3|-H zB8v9$>`9uo0iw^%;d_pk{990LldX_@w4~{Uv!%s>alWq)OmpW(hP{0EpnWmIQbCEtmIB}w2uI#QN*l0Kc7`5XxsKsk;NU#_qg^CKTt{x diff --git a/webui/src/pages/bug/.TimelineQuery.tsx.swp b/webui/src/pages/bug/.TimelineQuery.tsx.swp deleted file mode 100644 index 0ad00f67d95d7c71c00c040dcd1cfd134b06d80a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI2%Wl&^6o#j)DXjpB?I|qUNUfcgO_ekaqJRVvh$1d45EFainmV?b8PislVaE~{ z>{x-|5fCdLf`#6jQjuPT|UC`Y~^twrm=nlse{OMHTwIuM zmLx8mpQ1CT?;Ud}JU$a(0!)AjFaajO1egF5I9>#7IwoFVl@eO(py@Q@Yx1dRA z0us>nDItDAKcO$sODKapXcf8vU4d4hbI>+={DwY3AE7tUYv>gup-0d|sPysx;)@9| z0Vco%m;e)C0!)AjFaaj;4-rtCX`(IpsxPz1(!PVqZ6S)bAkxZaI;N%3CMBuqG-CML9r-s(Ci z*mdmil(bXDRmICXO6x>7c3h>)SIdPK<<&;9`mJ(UC-m!;XHti<7b%0zNUA5wY1nd>DmSy$Y*b*# z%%~B->e%;8F6x|ps|cLQ%RW@J2rg68pw2Qn)UN^?ExMcq*R%`6S*+wEzGB From 2483b2729602b5ab544a9d0e88f47eba233e8e82 Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 17 Mar 2021 13:04:24 +0100 Subject: [PATCH 095/157] Display comment form on edit click --- webui/src/pages/bug/Message.tsx | 55 ++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx index 3993b5f71d64ee2e53f32901fc6a803a04638fee..4ad68b44202b4a54475a0ae5cd225c518052916b 100644 --- a/webui/src/pages/bug/Message.tsx +++ b/webui/src/pages/bug/Message.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import IconButton from '@material-ui/core/IconButton'; import Paper from '@material-ui/core/Paper'; @@ -74,14 +74,15 @@ type Props = { function Message({ bug, op }: Props) { const classes = useStyles(); + const [editMode, switchToEditMode] = useState(false); const editComment = (id: String) => { + switchToEditMode(true); console.log(id); }; - return ( -
- + function readMessageView() { + return (
@@ -90,28 +91,40 @@ function Message({ bug, op }: Props) {
{op.edited &&
Edited
} - - editComment(op.id)} - > - - - + + {() => ( + + editComment(op.id)} + > + + + + )} +
- - {() => ( -
- -
- )} -
+ ); + } + + function editMessageView() { + return ( +
+ +
+ ); + } + + return ( +
+ + {editMode ? editMessageView() : readMessageView()}
); } From 0cd5c84d59d00141bb997346f538b165644d233c Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 17 Mar 2021 13:14:07 +0100 Subject: [PATCH 096/157] Fix CommentForm margin --- webui/src/pages/bug/Bug.tsx | 1 + webui/src/pages/bug/CommentForm.tsx | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/webui/src/pages/bug/Bug.tsx b/webui/src/pages/bug/Bug.tsx index 46a443d5649b4604653488980af86d08beb0ca4f..83785a37fe9490d85cf61ce64658f61b9040f0eb 100644 --- a/webui/src/pages/bug/Bug.tsx +++ b/webui/src/pages/bug/Bug.tsx @@ -59,6 +59,7 @@ const useStyles = makeStyles((theme) => ({ ...theme.typography.body2, }, commentForm: { + marginTop: theme.spacing(2), marginLeft: 48, }, })); diff --git a/webui/src/pages/bug/CommentForm.tsx b/webui/src/pages/bug/CommentForm.tsx index 773e7d0ecd8f6fb40d4176a204078db16240ad2e..fe9536ac6fb6631869e903bc40b862f5633acc69 100644 --- a/webui/src/pages/bug/CommentForm.tsx +++ b/webui/src/pages/bug/CommentForm.tsx @@ -15,7 +15,6 @@ import { TimelineDocument } from './TimelineQuery.generated'; type StyleProps = { loading: boolean }; const useStyles = makeStyles((theme) => ({ container: { - margin: theme.spacing(2, 0), padding: theme.spacing(0, 2, 2, 2), }, textarea: {}, From c874d111f50dc129a1ac8210beff626eea2f2186 Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 17 Mar 2021 16:04:34 +0100 Subject: [PATCH 097/157] Create EditCommentForm and handle cancle button --- webui/src/pages/bug/EditCommentForm.tsx | 119 ++++++++++++++++++++++++ webui/src/pages/bug/Message.tsx | 12 ++- 2 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 webui/src/pages/bug/EditCommentForm.tsx diff --git a/webui/src/pages/bug/EditCommentForm.tsx b/webui/src/pages/bug/EditCommentForm.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fb192a0207503f75b102d85e58e5deab190de7d3 --- /dev/null +++ b/webui/src/pages/bug/EditCommentForm.tsx @@ -0,0 +1,119 @@ +import React, { useState, useRef } from 'react'; + +import Button from '@material-ui/core/Button'; +import Paper from '@material-ui/core/Paper'; +import { makeStyles, Theme } from '@material-ui/core/styles'; + +import CommentInput from '../../components/CommentInput/CommentInput'; + +import { BugFragment } from './Bug.generated'; +import { useAddCommentMutation } from './CommentForm.generated'; +import { TimelineDocument } from './TimelineQuery.generated'; + +type StyleProps = { loading: boolean }; +const useStyles = makeStyles((theme) => ({ + container: { + padding: theme.spacing(0, 2, 2, 2), + }, + textarea: {}, + tabContent: { + margin: theme.spacing(2, 0), + }, + preview: { + borderBottom: `solid 3px ${theme.palette.grey['200']}`, + minHeight: '5rem', + }, + actions: { + display: 'flex', + justifyContent: 'flex-end', + }, + greenButton: { + marginLeft: '8px', + backgroundColor: '#2ea44fd9', + color: '#fff', + '&:hover': { + backgroundColor: '#2ea44f', + }, + }, +})); + +type Props = { + bug: BugFragment; + commentId: string; + onCancleClick?: () => void; +}; + +function EditCommentForm({ bug, commentId, onCancleClick }: Props) { + const [addComment, { loading }] = useAddCommentMutation(); + const [issueComment, setIssueComment] = useState(''); + const [inputProp, setInputProp] = useState(''); + const classes = useStyles({ loading }); + const form = useRef(null); + + const submit = () => { + addComment({ + variables: { + input: { + prefix: bug.id, + message: issueComment, + }, + }, + refetchQueries: [ + // TODO: update the cache instead of refetching + { + query: TimelineDocument, + variables: { + id: bug.id, + first: 100, + }, + }, + ], + awaitRefetchQueries: true, + }).then(() => resetForm()); + }; + + function resetForm() { + setInputProp({ + value: '', + }); + } + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (issueComment.length > 0) submit(); + }; + + function getCancleButton() { + return ( + + ); + } + + return ( + +
+ setIssueComment(comment)} + /> +
+ {onCancleClick ? getCancleButton() : ''} + +
+ +
+ ); +} + +export default EditCommentForm; diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx index 4ad68b44202b4a54475a0ae5cd225c518052916b..928e256f9192156a6f0b42256092eb590ebc635b 100644 --- a/webui/src/pages/bug/Message.tsx +++ b/webui/src/pages/bug/Message.tsx @@ -12,7 +12,7 @@ import Date from 'src/components/Date'; import IfLoggedIn from 'src/components/IfLoggedIn/IfLoggedIn'; import { BugFragment } from './Bug.generated'; -import CommentForm from './CommentForm'; +import EditCommentForm from './EditCommentForm'; import { AddCommentFragment } from './MessageCommentFragment.generated'; import { CreateFragment } from './MessageCreateFragment.generated'; @@ -114,9 +114,17 @@ function Message({ bug, op }: Props) { } function editMessageView() { + const cancleEdition = () => { + switchToEditMode(false); + }; + return (
- +
); } From 9f6c045f8b6e44e47300cec181217906f48d8261 Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 17 Mar 2021 17:54:49 +0100 Subject: [PATCH 098/157] Several fixes - Fix misspelling of cancel... - Fix flickering of green "update comment" button - Fill input with comment text - Close edit view after submit --- .../components/CommentInput/CommentInput.tsx | 5 ++- webui/src/pages/bug/EditCommentForm.tsx | 43 +++++++------------ webui/src/pages/bug/Message.tsx | 22 +++++----- 3 files changed, 30 insertions(+), 40 deletions(-) diff --git a/webui/src/components/CommentInput/CommentInput.tsx b/webui/src/components/CommentInput/CommentInput.tsx index 86cc7dbbdf8fa885668c4b3e7f6ef25510fd7e09..c574538e886f220675bf839dc91c6ef7ac25c4b8 100644 --- a/webui/src/components/CommentInput/CommentInput.tsx +++ b/webui/src/components/CommentInput/CommentInput.tsx @@ -51,6 +51,7 @@ const a11yProps = (index: number) => ({ type Props = { inputProps?: any; + inputText?: string; loading: boolean; onChange: (comment: string) => void; }; @@ -62,8 +63,8 @@ type Props = { * @param loading Disable input when component not ready yet * @param onChange Callback to return input value changes */ -function CommentInput({ inputProps, loading, onChange }: Props) { - const [input, setInput] = useState(''); +function CommentInput({ inputProps, inputText, loading, onChange }: Props) { + const [input, setInput] = useState(inputText ? inputText : ''); const [tab, setTab] = useState(0); const classes = useStyles(); diff --git a/webui/src/pages/bug/EditCommentForm.tsx b/webui/src/pages/bug/EditCommentForm.tsx index fb192a0207503f75b102d85e58e5deab190de7d3..46cf1e1f014f904f9cac76ee35b3aec7f97feeb7 100644 --- a/webui/src/pages/bug/EditCommentForm.tsx +++ b/webui/src/pages/bug/EditCommentForm.tsx @@ -8,7 +8,8 @@ import CommentInput from '../../components/CommentInput/CommentInput'; import { BugFragment } from './Bug.generated'; import { useAddCommentMutation } from './CommentForm.generated'; -import { TimelineDocument } from './TimelineQuery.generated'; +import { AddCommentFragment } from './MessageCommentFragment.generated'; +import { CreateFragment } from './MessageCreateFragment.generated'; type StyleProps = { loading: boolean }; const useStyles = makeStyles((theme) => ({ @@ -39,37 +40,22 @@ const useStyles = makeStyles((theme) => ({ type Props = { bug: BugFragment; - commentId: string; - onCancleClick?: () => void; + comment: AddCommentFragment | CreateFragment; + onCancelClick?: () => void; + onPostSubmit?: () => void; }; -function EditCommentForm({ bug, commentId, onCancleClick }: Props) { +function EditCommentForm({ bug, comment, onCancelClick, onPostSubmit }: Props) { const [addComment, { loading }] = useAddCommentMutation(); - const [issueComment, setIssueComment] = useState(''); + const [issueComment, setIssueComment] = useState(comment.message); const [inputProp, setInputProp] = useState(''); const classes = useStyles({ loading }); const form = useRef(null); const submit = () => { - addComment({ - variables: { - input: { - prefix: bug.id, - message: issueComment, - }, - }, - refetchQueries: [ - // TODO: update the cache instead of refetching - { - query: TimelineDocument, - variables: { - id: bug.id, - first: 100, - }, - }, - ], - awaitRefetchQueries: true, - }).then(() => resetForm()); + console.log('submit: ' + issueComment); + resetForm(); + if (onPostSubmit) onPostSubmit(); }; function resetForm() { @@ -83,10 +69,10 @@ function EditCommentForm({ bug, commentId, onCancleClick }: Props) { if (issueComment.length > 0) submit(); }; - function getCancleButton() { + function getCancelButton() { return ( - ); } @@ -98,9 +84,10 @@ function EditCommentForm({ bug, commentId, onCancleClick }: Props) { inputProps={inputProp} loading={loading} onChange={(comment: string) => setIssueComment(comment)} + inputText={comment.message} />
- {onCancleClick ? getCancleButton() : ''} + {onCancelClick ? getCancelButton() : ''} diff --git a/webui/src/pages/bug/EditCommentform.graphql b/webui/src/pages/bug/EditCommentform.graphql new file mode 100644 index 0000000000000000000000000000000000000000..c7047e6ecd4d32b4c00a894b4e6bf07cb89f78c3 --- /dev/null +++ b/webui/src/pages/bug/EditCommentform.graphql @@ -0,0 +1,7 @@ +mutation EditComment($input: EditCommentInput!) { + editComment(input: $input) { + bug { + id + } + } +} From d4f96fa91faa6f56a790ebc7fd041af705ed77b0 Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 17 Mar 2021 18:21:52 +0100 Subject: [PATCH 100/157] Use theme colors for submit button --- webui/src/pages/bug/EditCommentForm.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/webui/src/pages/bug/EditCommentForm.tsx b/webui/src/pages/bug/EditCommentForm.tsx index f50400642fefa9fa8478c21401efea211b7d79da..ca627c276c6acfab7d434d8a86be02d3d9f7cdfb 100644 --- a/webui/src/pages/bug/EditCommentForm.tsx +++ b/webui/src/pages/bug/EditCommentForm.tsx @@ -30,10 +30,11 @@ const useStyles = makeStyles((theme) => ({ }, greenButton: { marginLeft: '8px', - backgroundColor: '#2ea44fd9', - color: '#fff', + backgroundColor: theme.palette.success.main, + color: theme.palette.success.contrastText, '&:hover': { - backgroundColor: '#2ea44f', + backgroundColor: theme.palette.success.dark, + color: theme.palette.success.contrastText, }, }, })); From d6c3ffa984c57a546d437d9be989077d824fac46 Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 17 Mar 2021 21:21:20 +0100 Subject: [PATCH 101/157] Fix graphql filename capitalization --- .../bug/{EditCommentform.graphql => EditCommentForm.graphql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename webui/src/pages/bug/{EditCommentform.graphql => EditCommentForm.graphql} (100%) diff --git a/webui/src/pages/bug/EditCommentform.graphql b/webui/src/pages/bug/EditCommentForm.graphql similarity index 100% rename from webui/src/pages/bug/EditCommentform.graphql rename to webui/src/pages/bug/EditCommentForm.graphql From 9fb033ef191a23b1338e0fdfe8ab1f462165b99d Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 17 Mar 2021 22:28:45 +0100 Subject: [PATCH 102/157] Return of new comment works... ...but the types are quite hacky --- webui/src/pages/bug/EditCommentForm.graphql | 9 +++++++++ webui/src/pages/bug/EditCommentForm.tsx | 11 +++++++---- webui/src/pages/bug/Message.tsx | 8 ++++++-- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/webui/src/pages/bug/EditCommentForm.graphql b/webui/src/pages/bug/EditCommentForm.graphql index c7047e6ecd4d32b4c00a894b4e6bf07cb89f78c3..4765b75caaa79609f51fe4195c6f556a7180aa23 100644 --- a/webui/src/pages/bug/EditCommentForm.graphql +++ b/webui/src/pages/bug/EditCommentForm.graphql @@ -1,7 +1,16 @@ +#import "./MessageCommentFragment.graphql" +#import "./MessageCreateFragment.graphql" + mutation EditComment($input: EditCommentInput!) { editComment(input: $input) { bug { id + timeline { + comments: nodes { + ...Create + ...AddComment + } + } } } } diff --git a/webui/src/pages/bug/EditCommentForm.tsx b/webui/src/pages/bug/EditCommentForm.tsx index ca627c276c6acfab7d434d8a86be02d3d9f7cdfb..7823d75efa11d619d568f83b9a4042adca90a20f 100644 --- a/webui/src/pages/bug/EditCommentForm.tsx +++ b/webui/src/pages/bug/EditCommentForm.tsx @@ -7,7 +7,7 @@ import { makeStyles, Theme } from '@material-ui/core/styles'; import CommentInput from '../../components/CommentInput/CommentInput'; import { BugFragment } from './Bug.generated'; -import { useEditCommentMutation } from './EditCommentform.generated'; +import { useEditCommentMutation } from './EditCommentForm.generated'; import { AddCommentFragment } from './MessageCommentFragment.generated'; import { CreateFragment } from './MessageCreateFragment.generated'; @@ -43,7 +43,7 @@ type Props = { bug: BugFragment; comment: AddCommentFragment | CreateFragment; onCancelClick?: () => void; - onPostSubmit?: () => void; + onPostSubmit?: (comments: any) => void; }; function EditCommentForm({ bug, comment, onCancelClick, onPostSubmit }: Props) { @@ -54,7 +54,6 @@ function EditCommentForm({ bug, comment, onCancelClick, onPostSubmit }: Props) { const form = useRef(null); const submit = () => { - console.log('submit: ' + message + '\nTo: ' + comment.id); editComment({ variables: { input: { @@ -63,9 +62,13 @@ function EditCommentForm({ bug, comment, onCancelClick, onPostSubmit }: Props) { target: comment.id, }, }, + }).then((result) => { + const comments = result.data?.editComment.bug.timeline.comments; + const coms = comments as (AddCommentFragment | CreateFragment)[]; + const res = coms.find((elem) => elem.id === comment.id); + if (onPostSubmit) onPostSubmit(res); }); resetForm(); - if (onPostSubmit) onPostSubmit(); }; function resetForm() { diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx index 08a55dc6d44c55ea63a5f5ee13e3eba65276623a..7455104bf95c2af6ff3e34101ac00b5dc12ef3c7 100644 --- a/webui/src/pages/bug/Message.tsx +++ b/webui/src/pages/bug/Message.tsx @@ -78,7 +78,6 @@ function Message({ bug, op: comment }: Props) { const editComment = (id: String) => { switchToEditMode(true); - console.log(id); }; function readMessageView() { @@ -118,13 +117,18 @@ function Message({ bug, op: comment }: Props) { switchToEditMode(false); }; + const onPostSubmit = (comments: AddCommentFragment | CreateFragment) => { + console.log('posted: ' + comments.message); + switchToEditMode(false); + }; + return (
From b06f7c781620c967e939577fc92e1265cdff6485 Mon Sep 17 00:00:00 2001 From: Sascha Date: Thu, 18 Mar 2021 12:07:09 +0100 Subject: [PATCH 103/157] Reduced arbitary variable names --- webui/src/pages/bug/EditCommentForm.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/webui/src/pages/bug/EditCommentForm.tsx b/webui/src/pages/bug/EditCommentForm.tsx index 7823d75efa11d619d568f83b9a4042adca90a20f..0794c3efeb76e53a30d3302b415768456ab470ab 100644 --- a/webui/src/pages/bug/EditCommentForm.tsx +++ b/webui/src/pages/bug/EditCommentForm.tsx @@ -63,10 +63,14 @@ function EditCommentForm({ bug, comment, onCancelClick, onPostSubmit }: Props) { }, }, }).then((result) => { - const comments = result.data?.editComment.bug.timeline.comments; - const coms = comments as (AddCommentFragment | CreateFragment)[]; - const res = coms.find((elem) => elem.id === comment.id); - if (onPostSubmit) onPostSubmit(res); + const comments = result.data?.editComment.bug.timeline.comments as ( + | AddCommentFragment + | CreateFragment + )[]; + // NOTE Searching for the changed comment could be dropped if GraphQL get + // filter by id argument for timelineitems + const modifiedComment = comments.find((elem) => elem.id === comment.id); + if (onPostSubmit) onPostSubmit(modifiedComment); }); resetForm(); }; From 142adfd2b15dda3a7b9353c046f5858496012876 Mon Sep 17 00:00:00 2001 From: Sascha Date: Thu, 18 Mar 2021 12:08:19 +0100 Subject: [PATCH 104/157] Update message view after editing --- webui/src/pages/bug/Message.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx index 7455104bf95c2af6ff3e34101ac00b5dc12ef3c7..adb3057c17ed6de8a63120717348d809a5010e87 100644 --- a/webui/src/pages/bug/Message.tsx +++ b/webui/src/pages/bug/Message.tsx @@ -72,9 +72,10 @@ type Props = { op: AddCommentFragment | CreateFragment; }; -function Message({ bug, op: comment }: Props) { +function Message({ bug, op }: Props) { const classes = useStyles(); const [editMode, switchToEditMode] = useState(false); + const [comment, setComment] = useState(op); const editComment = (id: String) => { switchToEditMode(true); @@ -117,8 +118,8 @@ function Message({ bug, op: comment }: Props) { switchToEditMode(false); }; - const onPostSubmit = (comments: AddCommentFragment | CreateFragment) => { - console.log('posted: ' + comments.message); + const onPostSubmit = (comment: AddCommentFragment | CreateFragment) => { + setComment(comment); switchToEditMode(false); }; From 58e124056002e7e8e9dc9ac38f672a90b005eebd Mon Sep 17 00:00:00 2001 From: Sascha Date: Thu, 18 Mar 2021 12:13:03 +0100 Subject: [PATCH 105/157] Add space between edit button and edited indicator --- webui/src/pages/bug/Message.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx index adb3057c17ed6de8a63120717348d809a5010e87..26bbb1aa4be838c4319bb760ce27248ead41fdce 100644 --- a/webui/src/pages/bug/Message.tsx +++ b/webui/src/pages/bug/Message.tsx @@ -60,6 +60,7 @@ const useStyles = makeStyles((theme) => ({ editButton: { color: theme.palette.info.contrastText, padding: '0rem', + marginLeft: theme.spacing(1), fontSize: '0.75rem', '&:hover': { backgroundColor: 'inherit', From 25d3aca9adac3441da14d53f489b2609fefac21f Mon Sep 17 00:00:00 2001 From: Sascha Date: Thu, 18 Mar 2021 14:11:41 +0100 Subject: [PATCH 106/157] Add test menu for edit history --- webui/src/pages/bug/EditHistoryMenu.tsx | 80 +++++++++++++++++++++++++ webui/src/pages/bug/Message.tsx | 13 ++-- 2 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 webui/src/pages/bug/EditHistoryMenu.tsx diff --git a/webui/src/pages/bug/EditHistoryMenu.tsx b/webui/src/pages/bug/EditHistoryMenu.tsx new file mode 100644 index 0000000000000000000000000000000000000000..10b66f8faa03c688dc79b49212e4524a7847349e --- /dev/null +++ b/webui/src/pages/bug/EditHistoryMenu.tsx @@ -0,0 +1,80 @@ +import React from 'react'; + +import IconButton, { IconButtonProps } from '@material-ui/core/IconButton'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import HistoryIcon from '@material-ui/icons/History'; + +const options = [ + 'None', + 'Atria', + 'Callisto', + 'Dione', + 'Ganymede', + 'Hangouts Call', + 'Luna', + 'Oberon', + 'Phobos', + 'Pyxis', + 'Sedna', + 'Titania', + 'Triton', + 'Umbriel', +]; + +const ITEM_HEIGHT = 48; + +type Props = { + iconBtnProps?: IconButtonProps; +}; +function EditHistoryMenu({ iconBtnProps }: Props) { + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + return ( +
+ + + + + {options.map((option) => ( + + {option} + + ))} + +
+ ); +} + +export default EditHistoryMenu; diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx index 26bbb1aa4be838c4319bb760ce27248ead41fdce..d36b104440f6676e3951a72ac0300e344b75bf51 100644 --- a/webui/src/pages/bug/Message.tsx +++ b/webui/src/pages/bug/Message.tsx @@ -13,6 +13,7 @@ import IfLoggedIn from 'src/components/IfLoggedIn/IfLoggedIn'; import { BugFragment } from './Bug.generated'; import EditCommentForm from './EditCommentForm'; +import EditHistoryMenu from './EditHistoryMenu'; import { AddCommentFragment } from './MessageCommentFragment.generated'; import { CreateFragment } from './MessageCreateFragment.generated'; @@ -57,7 +58,8 @@ const useStyles = makeStyles((theme) => ({ ...theme.typography.body2, padding: '0.5rem', }, - editButton: { + headerActions2: {}, + headerActions: { color: theme.palette.info.contrastText, padding: '0rem', marginLeft: theme.spacing(1), @@ -91,13 +93,17 @@ function Message({ bug, op }: Props) { commented
- {comment.edited &&
Edited
} + {comment.edited && ( + + )} {() => ( editComment(comment.id)} > @@ -129,7 +135,6 @@ function Message({ bug, op }: Props) { From defd1ae00cccce0b46207e03084fe6af32096610 Mon Sep 17 00:00:00 2001 From: Sascha Date: Thu, 18 Mar 2021 15:45:20 +0100 Subject: [PATCH 107/157] Populate history menu with edit steps --- webui/src/pages/bug/EditHistoryMenu.tsx | 54 ++++++++++--------- webui/src/pages/bug/Message.tsx | 2 + .../pages/bug/MessageCommentFragment.graphql | 4 ++ .../pages/bug/MessageCreateFragment.graphql | 4 ++ .../src/pages/bug/MessageEditHistory.graphql | 15 ++++++ 5 files changed, 55 insertions(+), 24 deletions(-) create mode 100644 webui/src/pages/bug/MessageEditHistory.graphql diff --git a/webui/src/pages/bug/EditHistoryMenu.tsx b/webui/src/pages/bug/EditHistoryMenu.tsx index 10b66f8faa03c688dc79b49212e4524a7847349e..a916a1a89183722750751b6b067ac362c1b71d03 100644 --- a/webui/src/pages/bug/EditHistoryMenu.tsx +++ b/webui/src/pages/bug/EditHistoryMenu.tsx @@ -1,36 +1,43 @@ import React from 'react'; +import CircularProgress from '@material-ui/core/CircularProgress'; import IconButton, { IconButtonProps } from '@material-ui/core/IconButton'; import Menu from '@material-ui/core/Menu'; import MenuItem from '@material-ui/core/MenuItem'; import HistoryIcon from '@material-ui/icons/History'; -const options = [ - 'None', - 'Atria', - 'Callisto', - 'Dione', - 'Ganymede', - 'Hangouts Call', - 'Luna', - 'Oberon', - 'Phobos', - 'Pyxis', - 'Sedna', - 'Titania', - 'Triton', - 'Umbriel', -]; +import Date from 'src/components/Date'; + +import { AddCommentFragment } from './MessageCommentFragment.generated'; +import { CreateFragment } from './MessageCreateFragment.generated'; +import { useMessageEditHistoryQuery } from './MessageEditHistory.generated'; const ITEM_HEIGHT = 48; type Props = { + bugId: string; + commentId: string; iconBtnProps?: IconButtonProps; }; -function EditHistoryMenu({ iconBtnProps }: Props) { +function EditHistoryMenu({ iconBtnProps, bugId, commentId }: Props) { const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); + const { loading, error, data } = useMessageEditHistoryQuery({ + variables: { bugIdPrefix: bugId }, + }); + if (loading) return ; + if (error) return

Error: {error}

; + + const comments = data?.repository?.bug?.timeline.comments as ( + | AddCommentFragment + | CreateFragment + )[]; + // NOTE Searching for the changed comment could be dropped if GraphQL get + // filter by id argument for timelineitems + const comment = comments.find((elem) => elem.id === commentId); + const history = comment?.history; + const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; @@ -63,13 +70,12 @@ function EditHistoryMenu({ iconBtnProps }: Props) { }, }} > - {options.map((option) => ( - - {option} + + Edited {history?.length} times. + + {history?.map((edit, index) => ( + + ))} diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx index d36b104440f6676e3951a72ac0300e344b75bf51..b117c103b2b6995f4b13bd812cb637c380acfaf0 100644 --- a/webui/src/pages/bug/Message.tsx +++ b/webui/src/pages/bug/Message.tsx @@ -96,6 +96,8 @@ function Message({ bug, op }: Props) { {comment.edited && ( )} diff --git a/webui/src/pages/bug/MessageCommentFragment.graphql b/webui/src/pages/bug/MessageCommentFragment.graphql index 337eade0967384dcc0db81d3249d59245aef2a29..c852b4b0bf3685e38efc5c39ee923acfc67fd9af 100644 --- a/webui/src/pages/bug/MessageCommentFragment.graphql +++ b/webui/src/pages/bug/MessageCommentFragment.graphql @@ -6,4 +6,8 @@ fragment AddComment on AddCommentTimelineItem { ...authored edited message + history { + message + date + } } diff --git a/webui/src/pages/bug/MessageCreateFragment.graphql b/webui/src/pages/bug/MessageCreateFragment.graphql index 81f80afb6b659743de1c48fae64ae2ea0b947562..1f4647b65205a22c934ccc37bd54ed0f302ce4ca 100644 --- a/webui/src/pages/bug/MessageCreateFragment.graphql +++ b/webui/src/pages/bug/MessageCreateFragment.graphql @@ -6,4 +6,8 @@ fragment Create on CreateTimelineItem { ...authored edited message + history { + message + date + } } diff --git a/webui/src/pages/bug/MessageEditHistory.graphql b/webui/src/pages/bug/MessageEditHistory.graphql new file mode 100644 index 0000000000000000000000000000000000000000..6fde8d452772caac3143ff89fd1214a61d00dab9 --- /dev/null +++ b/webui/src/pages/bug/MessageEditHistory.graphql @@ -0,0 +1,15 @@ +#import "./MessageCommentFragment.graphql" +#import "./MessageCreateFragment.graphql" + +query MessageEditHistory($bugIdPrefix: String!) { + repository { + bug(prefix: $bugIdPrefix) { + timeline { + comments: nodes { + ...Create + ...AddComment + } + } + } + } +} From d453abdeedcac5b7593f72d63a5641f9a3e99da0 Mon Sep 17 00:00:00 2001 From: Sascha Date: Fri, 19 Mar 2021 11:21:18 +0100 Subject: [PATCH 108/157] Move toggle button out of history menu --- webui/src/pages/bug/EditHistoryMenu.tsx | 33 ++++----------- webui/src/pages/bug/Message.tsx | 54 +++++++++++++++++++++---- 2 files changed, 54 insertions(+), 33 deletions(-) diff --git a/webui/src/pages/bug/EditHistoryMenu.tsx b/webui/src/pages/bug/EditHistoryMenu.tsx index a916a1a89183722750751b6b067ac362c1b71d03..da2ed0cd3b8722ff9894515f3a5310abbebc97b3 100644 --- a/webui/src/pages/bug/EditHistoryMenu.tsx +++ b/webui/src/pages/bug/EditHistoryMenu.tsx @@ -1,10 +1,8 @@ import React from 'react'; import CircularProgress from '@material-ui/core/CircularProgress'; -import IconButton, { IconButtonProps } from '@material-ui/core/IconButton'; import Menu from '@material-ui/core/Menu'; import MenuItem from '@material-ui/core/MenuItem'; -import HistoryIcon from '@material-ui/icons/History'; import Date from 'src/components/Date'; @@ -15,13 +13,13 @@ import { useMessageEditHistoryQuery } from './MessageEditHistory.generated'; const ITEM_HEIGHT = 48; type Props = { + anchor: null | HTMLElement; bugId: string; commentId: string; - iconBtnProps?: IconButtonProps; + onClose: () => void; }; -function EditHistoryMenu({ iconBtnProps, bugId, commentId }: Props) { - const [anchorEl, setAnchorEl] = React.useState(null); - const open = Boolean(anchorEl); +function EditHistoryMenu({ anchor, bugId, commentId, onClose }: Props) { + const open = Boolean(anchor); const { loading, error, data } = useMessageEditHistoryQuery({ variables: { bugIdPrefix: bugId }, @@ -38,31 +36,14 @@ function EditHistoryMenu({ iconBtnProps, bugId, commentId }: Props) { const comment = comments.find((elem) => elem.id === commentId); const history = comment?.history; - const handleClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - return (
- - - {history?.map((edit, index) => ( - + ))} diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx index b117c103b2b6995f4b13bd812cb637c380acfaf0..bf3fb6da239055c87e529540c6f697c82b899b3d 100644 --- a/webui/src/pages/bug/Message.tsx +++ b/webui/src/pages/bug/Message.tsx @@ -5,6 +5,7 @@ import Paper from '@material-ui/core/Paper'; import Tooltip from '@material-ui/core/Tooltip/Tooltip'; import { makeStyles } from '@material-ui/core/styles'; import EditIcon from '@material-ui/icons/Edit'; +import HistoryIcon from '@material-ui/icons/History'; import Author, { Avatar } from 'src/components/Author'; import Content from 'src/components/Content'; @@ -58,7 +59,6 @@ const useStyles = makeStyles((theme) => ({ ...theme.typography.body2, padding: '0.5rem', }, - headerActions2: {}, headerActions: { color: theme.palette.info.contrastText, padding: '0rem', @@ -70,11 +70,55 @@ const useStyles = makeStyles((theme) => ({ }, })); +//TODO move button out of this component and let only menu as component with +//query. Then the query won't execute unless button click renders menu with +//query. +//TODO Fix display of load button spinner. +//TODO Move this button and menu in separate component directory +//TODO fix failing pipeline due to eslint error +type HistBtnProps = { + bugId: string; + commentId: string; +}; +function HistoryMenuToggleButton({ bugId, commentId }: HistBtnProps) { + const classes = useStyles(); + const [anchorEl, setAnchorEl] = React.useState(null); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + return ( +
+ + + + {anchorEl && ( + + )} +
+ ); +} + type Props = { bug: BugFragment; op: AddCommentFragment | CreateFragment; }; - function Message({ bug, op }: Props) { const classes = useStyles(); const [editMode, switchToEditMode] = useState(false); @@ -94,11 +138,7 @@ function Message({ bug, op }: Props) {
{comment.edited && ( - + )} {() => ( From 155b37e81fb3a5f463ddcc0c39790ea9b755d57b Mon Sep 17 00:00:00 2001 From: Sascha Date: Fri, 19 Mar 2021 14:20:54 +0100 Subject: [PATCH 109/157] Use dialog with accordion for message history menu --- webui/src/pages/bug/EditHistoryMenu.tsx | 67 ------ webui/src/pages/bug/Message.tsx | 36 ++-- webui/src/pages/bug/MessageHistoryDialog.tsx | 215 +++++++++++++++++++ 3 files changed, 233 insertions(+), 85 deletions(-) delete mode 100644 webui/src/pages/bug/EditHistoryMenu.tsx create mode 100644 webui/src/pages/bug/MessageHistoryDialog.tsx diff --git a/webui/src/pages/bug/EditHistoryMenu.tsx b/webui/src/pages/bug/EditHistoryMenu.tsx deleted file mode 100644 index da2ed0cd3b8722ff9894515f3a5310abbebc97b3..0000000000000000000000000000000000000000 --- a/webui/src/pages/bug/EditHistoryMenu.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; - -import CircularProgress from '@material-ui/core/CircularProgress'; -import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; - -import Date from 'src/components/Date'; - -import { AddCommentFragment } from './MessageCommentFragment.generated'; -import { CreateFragment } from './MessageCreateFragment.generated'; -import { useMessageEditHistoryQuery } from './MessageEditHistory.generated'; - -const ITEM_HEIGHT = 48; - -type Props = { - anchor: null | HTMLElement; - bugId: string; - commentId: string; - onClose: () => void; -}; -function EditHistoryMenu({ anchor, bugId, commentId, onClose }: Props) { - const open = Boolean(anchor); - - const { loading, error, data } = useMessageEditHistoryQuery({ - variables: { bugIdPrefix: bugId }, - }); - if (loading) return ; - if (error) return

Error: {error}

; - - const comments = data?.repository?.bug?.timeline.comments as ( - | AddCommentFragment - | CreateFragment - )[]; - // NOTE Searching for the changed comment could be dropped if GraphQL get - // filter by id argument for timelineitems - const comment = comments.find((elem) => elem.id === commentId); - const history = comment?.history; - - return ( -
- - - Edited {history?.length} times. - - {history?.map((edit, index) => ( - - - - ))} - -
- ); -} - -export default EditHistoryMenu; diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx index bf3fb6da239055c87e529540c6f697c82b899b3d..51f45a4b1c11c18ad60ce9a52610e087f7810842 100644 --- a/webui/src/pages/bug/Message.tsx +++ b/webui/src/pages/bug/Message.tsx @@ -14,9 +14,9 @@ import IfLoggedIn from 'src/components/IfLoggedIn/IfLoggedIn'; import { BugFragment } from './Bug.generated'; import EditCommentForm from './EditCommentForm'; -import EditHistoryMenu from './EditHistoryMenu'; import { AddCommentFragment } from './MessageCommentFragment.generated'; import { CreateFragment } from './MessageCreateFragment.generated'; +import MessageHistoryDialog from './MessageHistoryDialog'; const useStyles = makeStyles((theme) => ({ author: { @@ -70,10 +70,6 @@ const useStyles = makeStyles((theme) => ({ }, })); -//TODO move button out of this component and let only menu as component with -//query. Then the query won't execute unless button click renders menu with -//query. -//TODO Fix display of load button spinner. //TODO Move this button and menu in separate component directory //TODO fix failing pipeline due to eslint error type HistBtnProps = { @@ -82,14 +78,14 @@ type HistBtnProps = { }; function HistoryMenuToggleButton({ bugId, commentId }: HistBtnProps) { const classes = useStyles(); - const [anchorEl, setAnchorEl] = React.useState(null); + const [open, setOpen] = React.useState(false); - const handleClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); + const handleClickOpen = () => { + setOpen(true); }; const handleClose = () => { - setAnchorEl(null); + setOpen(false); }; return ( @@ -98,19 +94,23 @@ function HistoryMenuToggleButton({ bugId, commentId }: HistBtnProps) { aria-label="more" aria-controls="long-menu" aria-haspopup="true" - onClick={handleClick} + onClick={handleClickOpen} className={classes.headerActions} >
- {anchorEl && ( - - )} + { + // Render CustomizedDialogs on open to prevent fetching the history + // before opening the history menu. + open && ( + + ) + }
); } diff --git a/webui/src/pages/bug/MessageHistoryDialog.tsx b/webui/src/pages/bug/MessageHistoryDialog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c49ac6614d07341fe7e8e68dbc240343010ebd8b --- /dev/null +++ b/webui/src/pages/bug/MessageHistoryDialog.tsx @@ -0,0 +1,215 @@ +import moment from 'moment'; +import React from 'react'; +import Moment from 'react-moment'; + +import MuiAccordion from '@material-ui/core/Accordion'; +import MuiAccordionDetails from '@material-ui/core/AccordionDetails'; +import MuiAccordionSummary from '@material-ui/core/AccordionSummary'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import Dialog from '@material-ui/core/Dialog'; +import MuiDialogContent from '@material-ui/core/DialogContent'; +import MuiDialogTitle from '@material-ui/core/DialogTitle'; +import Grid from '@material-ui/core/Grid'; +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip/Tooltip'; +import Typography from '@material-ui/core/Typography'; +import { + createStyles, + Theme, + withStyles, + WithStyles, +} from '@material-ui/core/styles'; +import CloseIcon from '@material-ui/icons/Close'; + +import { AddCommentFragment } from './MessageCommentFragment.generated'; +import { CreateFragment } from './MessageCreateFragment.generated'; +import { useMessageEditHistoryQuery } from './MessageEditHistory.generated'; + +const styles = (theme: Theme) => + createStyles({ + root: { + margin: 0, + padding: theme.spacing(2), + }, + closeButton: { + position: 'absolute', + right: theme.spacing(1), + top: theme.spacing(1), + }, + }); + +export interface DialogTitleProps extends WithStyles { + id: string; + children: React.ReactNode; + onClose: () => void; +} + +const DialogTitle = withStyles(styles)((props: DialogTitleProps) => { + const { children, classes, onClose, ...other } = props; + return ( + + {children} + {onClose ? ( + + + + ) : null} + + ); +}); + +const DialogContent = withStyles((theme: Theme) => ({ + root: { + padding: theme.spacing(2), + }, +}))(MuiDialogContent); + +const Accordion = withStyles({ + root: { + border: '1px solid rgba(0, 0, 0, .125)', + boxShadow: 'none', + '&:not(:last-child)': { + borderBottom: 0, + }, + '&:before': { + display: 'none', + }, + '&$expanded': { + margin: 'auto', + }, + }, + expanded: {}, +})(MuiAccordion); + +const AccordionSummary = withStyles((theme) => ({ + root: { + backgroundColor: theme.palette.primary.light, + borderBottomWidth: '1px', + borderBottomStyle: 'solid', + borderBottomColor: theme.palette.divider, + marginBottom: -1, + minHeight: 56, + '&$expanded': { + minHeight: 56, + }, + }, + content: { + '&$expanded': { + margin: '12px 0', + }, + }, + expanded: {}, +}))(MuiAccordionSummary); + +const AccordionDetails = withStyles((theme) => ({ + root: { + padding: theme.spacing(2), + }, +}))(MuiAccordionDetails); + +type Props = { + bugId: string; + commentId: string; + open: boolean; + onClose: () => void; +}; +function MessageHistoryDialog({ bugId, commentId, open, onClose }: Props) { + const [expanded, setExpanded] = React.useState('panel0'); + + const { loading, error, data } = useMessageEditHistoryQuery({ + variables: { bugIdPrefix: bugId }, + }); + if (loading) { + return ( + + + Loading... + + + + + + + + ); + } + if (error) { + return ( + + + Something went wrong... + + +

Error: {error}

+
+
+ ); + } + + const comments = data?.repository?.bug?.timeline.comments as ( + | AddCommentFragment + | CreateFragment + )[]; + // NOTE Searching for the changed comment could be dropped if GraphQL get + // filter by id argument for timelineitems + const comment = comments.find((elem) => elem.id === commentId); + const history = comment?.history; + + const handleChange = (panel: string) => ( + event: React.ChangeEvent<{}>, + newExpanded: boolean + ) => { + setExpanded(newExpanded ? panel : false); + }; + + return ( + + + Edited {history?.length} times. + + + {history?.map((edit, index) => ( + + + + + + + {edit.message} + + ))} + + + ); +} + +export default MessageHistoryDialog; From dfb039a9361a1a0d19a31e25130c89f70828ef00 Mon Sep 17 00:00:00 2001 From: Sascha Date: Fri, 19 Mar 2021 14:25:42 +0100 Subject: [PATCH 110/157] Rename MessageEditHistoryQuery --- .../{MessageEditHistory.graphql => MessageHistory.graphql} | 2 +- webui/src/pages/bug/MessageHistoryDialog.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename webui/src/pages/bug/{MessageEditHistory.graphql => MessageHistory.graphql} (83%) diff --git a/webui/src/pages/bug/MessageEditHistory.graphql b/webui/src/pages/bug/MessageHistory.graphql similarity index 83% rename from webui/src/pages/bug/MessageEditHistory.graphql rename to webui/src/pages/bug/MessageHistory.graphql index 6fde8d452772caac3143ff89fd1214a61d00dab9..e90eb45958280f519690b148859e4c53943064b4 100644 --- a/webui/src/pages/bug/MessageEditHistory.graphql +++ b/webui/src/pages/bug/MessageHistory.graphql @@ -1,7 +1,7 @@ #import "./MessageCommentFragment.graphql" #import "./MessageCreateFragment.graphql" -query MessageEditHistory($bugIdPrefix: String!) { +query MessageHistory($bugIdPrefix: String!) { repository { bug(prefix: $bugIdPrefix) { timeline { diff --git a/webui/src/pages/bug/MessageHistoryDialog.tsx b/webui/src/pages/bug/MessageHistoryDialog.tsx index c49ac6614d07341fe7e8e68dbc240343010ebd8b..9857f272912a7222e73490544a93074cb218ecf7 100644 --- a/webui/src/pages/bug/MessageHistoryDialog.tsx +++ b/webui/src/pages/bug/MessageHistoryDialog.tsx @@ -23,7 +23,7 @@ import CloseIcon from '@material-ui/icons/Close'; import { AddCommentFragment } from './MessageCommentFragment.generated'; import { CreateFragment } from './MessageCreateFragment.generated'; -import { useMessageEditHistoryQuery } from './MessageEditHistory.generated'; +import { useMessageHistoryQuery } from './MessageHistory.generated'; const styles = (theme: Theme) => createStyles({ @@ -120,7 +120,7 @@ type Props = { function MessageHistoryDialog({ bugId, commentId, open, onClose }: Props) { const [expanded, setExpanded] = React.useState('panel0'); - const { loading, error, data } = useMessageEditHistoryQuery({ + const { loading, error, data } = useMessageHistoryQuery({ variables: { bugIdPrefix: bugId }, }); if (loading) { From de9dd698917f905946c5b0e19039e4202375721d Mon Sep 17 00:00:00 2001 From: Sascha Date: Fri, 19 Mar 2021 14:57:11 +0100 Subject: [PATCH 111/157] Sort history from most recent edit to old --- webui/src/pages/bug/MessageHistoryDialog.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/webui/src/pages/bug/MessageHistoryDialog.tsx b/webui/src/pages/bug/MessageHistoryDialog.tsx index 9857f272912a7222e73490544a93074cb218ecf7..dc0e09cb53fe55c9c8d310b1b1b58b6379fb702c 100644 --- a/webui/src/pages/bug/MessageHistoryDialog.tsx +++ b/webui/src/pages/bug/MessageHistoryDialog.tsx @@ -169,7 +169,9 @@ function MessageHistoryDialog({ bugId, commentId, open, onClose }: Props) { // NOTE Searching for the changed comment could be dropped if GraphQL get // filter by id argument for timelineitems const comment = comments.find((elem) => elem.id === commentId); - const history = comment?.history; + // Sort by most recent edit. Must create a copy of constant history as + // reverse() modifies inplace. + const history = comment?.history.slice().reverse(); const handleChange = (panel: string) => ( event: React.ChangeEvent<{}>, @@ -203,6 +205,7 @@ function MessageHistoryDialog({ bugId, commentId, open, onClose }: Props) { + {index === 0 && '• (most recent edit)'} {edit.message} From bd316ef9efc5485513d8f2ceeca3938eb0c5b30f Mon Sep 17 00:00:00 2001 From: Sascha Date: Fri, 19 Mar 2021 15:03:27 +0100 Subject: [PATCH 112/157] Improve readability of accordion summary --- webui/src/pages/bug/MessageHistoryDialog.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/webui/src/pages/bug/MessageHistoryDialog.tsx b/webui/src/pages/bug/MessageHistoryDialog.tsx index dc0e09cb53fe55c9c8d310b1b1b58b6379fb702c..c359873b993836e20d6d8bd48a425472e7cf5123 100644 --- a/webui/src/pages/bug/MessageHistoryDialog.tsx +++ b/webui/src/pages/bug/MessageHistoryDialog.tsx @@ -202,10 +202,12 @@ function MessageHistoryDialog({ bugId, commentId, open, onClose }: Props) { aria-controls="panel1d-content" id="panel1d-header" > - - - - {index === 0 && '• (most recent edit)'} + + + + + {index === 0 && '• (most recent edit)'} + {edit.message} From 5b562559e36d07888e888d014db5130a80be4519 Mon Sep 17 00:00:00 2001 From: Sascha Date: Fri, 19 Mar 2021 16:06:41 +0100 Subject: [PATCH 113/157] Fix eslint pipeline fail hopefully --- webui/src/pages/bug/EditCommentForm.tsx | 8 ++++---- webui/src/pages/bug/Message.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/webui/src/pages/bug/EditCommentForm.tsx b/webui/src/pages/bug/EditCommentForm.tsx index 0794c3efeb76e53a30d3302b415768456ab470ab..8fa659b3bf2b65866b32ef0a05da64b24246b94e 100644 --- a/webui/src/pages/bug/EditCommentForm.tsx +++ b/webui/src/pages/bug/EditCommentForm.tsx @@ -42,11 +42,11 @@ const useStyles = makeStyles((theme) => ({ type Props = { bug: BugFragment; comment: AddCommentFragment | CreateFragment; - onCancelClick?: () => void; + onCancel?: () => void; onPostSubmit?: (comments: any) => void; }; -function EditCommentForm({ bug, comment, onCancelClick, onPostSubmit }: Props) { +function EditCommentForm({ bug, comment, onCancel, onPostSubmit }: Props) { const [editComment, { loading }] = useEditCommentMutation(); const [message, setMessage] = useState(comment.message); const [inputProp, setInputProp] = useState(''); @@ -88,7 +88,7 @@ function EditCommentForm({ bug, comment, onCancelClick, onPostSubmit }: Props) { function getCancelButton() { return ( - ); @@ -104,7 +104,7 @@ function EditCommentForm({ bug, comment, onCancelClick, onPostSubmit }: Props) { inputText={comment.message} />
- {onCancelClick ? getCancelButton() : ''} + {onCancel && getCancelButton()}
+ + + + + + Count + + ); } From 688cc33ccca5fd6653ee6f2ee2946dc30a9052ae Mon Sep 17 00:00:00 2001 From: Sascha Date: Fri, 19 Mar 2021 20:36:09 +0100 Subject: [PATCH 116/157] Display real number of comment --- webui/src/pages/list/BugRow.graphql | 3 +++ webui/src/pages/list/BugRow.tsx | 20 +++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/webui/src/pages/list/BugRow.graphql b/webui/src/pages/list/BugRow.graphql index 547c09d8ef8cf91c966f50788eab8a075ea906ed..e4e2760c23379585d5963dc5dc4d263c22f8e25f 100644 --- a/webui/src/pages/list/BugRow.graphql +++ b/webui/src/pages/list/BugRow.graphql @@ -9,5 +9,8 @@ fragment BugRow on Bug { labels { ...Label } + comments { + totalCount + } ...authored } diff --git a/webui/src/pages/list/BugRow.tsx b/webui/src/pages/list/BugRow.tsx index b06097ada6cae4fe835e29ef327abc3948f7c3a1..f9a50487a085ca04bebdeb2723979376605a65e5 100644 --- a/webui/src/pages/list/BugRow.tsx +++ b/webui/src/pages/list/BugRow.tsx @@ -76,6 +76,10 @@ const useStyles = makeStyles((theme) => ({ display: 'inline-block', }, }, + commentCount: { + fontSize: '1rem', + marginLeft: theme.spacing(0.5), + }, })); type Props = { @@ -84,6 +88,8 @@ type Props = { function BugRow({ bug }: Props) { const classes = useStyles(); + // Subtract 1 from totalCount as 1 comment is the bug description + const commentCount = bug.comments.totalCount - 1; return ( @@ -109,12 +115,16 @@ function BugRow({ bug }: Props) {
- - - + {commentCount > 0 && ( + + + + + + {commentCount} + - Count - + )} ); From 4c5a0f37f7979d29fa077c0cfcd9be936c8fc0bd Mon Sep 17 00:00:00 2001 From: Sascha Date: Sat, 20 Mar 2021 11:04:17 +0100 Subject: [PATCH 117/157] Improve message history accordions - Add unfold icon to edit history accordions - Change order of edit number. Most recent should have highest id and initial description should always have "#1 Edit" - Add "(Initial description)' to title of the first comment version --- webui/src/pages/bug/MessageHistoryDialog.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/webui/src/pages/bug/MessageHistoryDialog.tsx b/webui/src/pages/bug/MessageHistoryDialog.tsx index 9d1578b28ae741365ccd0ae3d2825e511e0badda..139eb4a599b4146490ccbd6b6a7d33f8e118f781 100644 --- a/webui/src/pages/bug/MessageHistoryDialog.tsx +++ b/webui/src/pages/bug/MessageHistoryDialog.tsx @@ -20,6 +20,7 @@ import { WithStyles, } from '@material-ui/core/styles'; import CloseIcon from '@material-ui/icons/Close'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { AddCommentFragment } from './MessageCommentFragment.generated'; import { CreateFragment } from './MessageCreateFragment.generated'; @@ -199,15 +200,17 @@ function MessageHistoryDialog({ bugId, commentId, open, onClose }: Props) { onChange={handleChange('panel' + index)} > } aria-controls="panel1d-content" id="panel1d-header" > - {`#${index + 1} • Edited `} + {`#${history?.length - index} • Edited `} {index === 0 && ' (most recent edit)'} + {index === history?.length - 1 && ' (Initial description)'} {edit.message} From c610d942fc510349302ed408fcc7b11500c2bb4c Mon Sep 17 00:00:00 2001 From: Sascha Date: Sat, 20 Mar 2021 11:07:36 +0100 Subject: [PATCH 118/157] Remove TODO comments --- webui/src/pages/bug/Message.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx index 22cce9b18fdce3356d2622e90850470475816df0..2f4cbc592ee6f3016426eec371899c3efcd43b99 100644 --- a/webui/src/pages/bug/Message.tsx +++ b/webui/src/pages/bug/Message.tsx @@ -70,8 +70,6 @@ const useStyles = makeStyles((theme) => ({ }, })); -//TODO Move this button and menu in separate component directory -//TODO fix failing pipeline due to eslint error type HistBtnProps = { bugId: string; commentId: string; From 4a2e04df61d6bf1264a7f353af621e8e6207653d Mon Sep 17 00:00:00 2001 From: Sascha Date: Sat, 20 Mar 2021 11:11:39 +0100 Subject: [PATCH 119/157] Improve description of target-field --- api/graphql/schema/mutations.graphql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/graphql/schema/mutations.graphql b/api/graphql/schema/mutations.graphql index f520991799abeebdf8d8fade3ec73d25258391aa..d7adde1e76070fac833d592e06a2dedb87ab6244 100644 --- a/api/graphql/schema/mutations.graphql +++ b/api/graphql/schema/mutations.graphql @@ -49,7 +49,7 @@ input EditCommentInput { repoRef: String """The bug ID's prefix.""" prefix: String! - """The target.""" + """The ID of the comment to be changed.""" target: String! """The new message to be set.""" message: String! From b7ddb22558bd4b429e810157b343ee41a6575945 Mon Sep 17 00:00:00 2001 From: Sascha Date: Sat, 20 Mar 2021 14:30:10 +0100 Subject: [PATCH 120/157] Fixate message counter to end of bug row Previously the counter has shifted around depending on the length of the bug titles. --- webui/src/pages/list/BugRow.tsx | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/webui/src/pages/list/BugRow.tsx b/webui/src/pages/list/BugRow.tsx index f9a50487a085ca04bebdeb2723979376605a65e5..1f5d22aae2626eea582b8b311a59d4b374959c0b 100644 --- a/webui/src/pages/list/BugRow.tsx +++ b/webui/src/pages/list/BugRow.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import Grid from '@material-ui/core/Grid'; import TableCell from '@material-ui/core/TableCell/TableCell'; import TableRow from '@material-ui/core/TableRow/TableRow'; import Tooltip from '@material-ui/core/Tooltip/Tooltip'; @@ -80,6 +79,9 @@ const useStyles = makeStyles((theme) => ({ fontSize: '1rem', marginLeft: theme.spacing(0.5), }, + commentCountCell: { + display: 'inline-flex', + }, })); type Props = { @@ -113,17 +115,11 @@ function BugRow({ bug }: Props) {  by {bug.author.displayName} - - {commentCount > 0 && ( - - - - - - {commentCount} - - + + + {commentCount} + )} From 8d8eb2942f73213b175529f47af980889cd080d4 Mon Sep 17 00:00:00 2001 From: Sascha Date: Sat, 20 Mar 2021 10:37:49 +0100 Subject: [PATCH 121/157] Add test navbar --- webui/src/components/Header/Header.tsx | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/webui/src/components/Header/Header.tsx b/webui/src/components/Header/Header.tsx index 3443fcf5997ecd59b5844c67e8062635948b56d6..5a968a29ef6b9f2c2728b59d03a929e6d443380d 100644 --- a/webui/src/components/Header/Header.tsx +++ b/webui/src/components/Header/Header.tsx @@ -2,6 +2,8 @@ import React from 'react'; import { Link } from 'react-router-dom'; import AppBar from '@material-ui/core/AppBar'; +import Tab from '@material-ui/core/Tab'; +import Tabs from '@material-ui/core/Tabs'; import Toolbar from '@material-ui/core/Toolbar'; import { makeStyles } from '@material-ui/core/styles'; @@ -35,6 +37,45 @@ const useStyles = makeStyles((theme) => ({ }, })); +function a11yProps(index: any) { + return { + id: `nav-tab-${index}`, + 'aria-controls': `nav-tabpanel-${index}`, + }; +} + +function NavTabs() { + const [value, setValue] = React.useState(0); + + //TODO page refresh resets state. Must parse url to determine which tab is + //highlighted + const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { + setValue(newValue); + }; + + + return ( + + + + + + + + + ); +} + function Header() { const classes = useStyles(); @@ -54,6 +95,7 @@ function Header() {
+ ); } From 1e411f5abfffa3955504f700b5edbe7f74007ae2 Mon Sep 17 00:00:00 2001 From: Sascha Date: Sat, 20 Mar 2021 14:53:23 +0100 Subject: [PATCH 122/157] Fix eslint empty line error --- webui/src/components/Header/Header.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/webui/src/components/Header/Header.tsx b/webui/src/components/Header/Header.tsx index 5a968a29ef6b9f2c2728b59d03a929e6d443380d..8e9b6697fb2e83af1c8f9b5f4bf8ff55b37e9f27 100644 --- a/webui/src/components/Header/Header.tsx +++ b/webui/src/components/Header/Header.tsx @@ -53,7 +53,6 @@ function NavTabs() { setValue(newValue); }; - return ( Date: Sun, 21 Mar 2021 13:32:08 +0100 Subject: [PATCH 123/157] Center navbar and disable unresolved navigations --- webui/src/components/Header/Header.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/webui/src/components/Header/Header.tsx b/webui/src/components/Header/Header.tsx index 8e9b6697fb2e83af1c8f9b5f4bf8ff55b37e9f27..1fe7504fe8c1f28e3d5dfdbe873c02d52252089d 100644 --- a/webui/src/components/Header/Header.tsx +++ b/webui/src/components/Header/Header.tsx @@ -55,22 +55,27 @@ function NavTabs() { return ( - + - - - + ); } From 9fa40d3d0819aaa27a055ebb51ebdccd83bf1dfd Mon Sep 17 00:00:00 2001 From: Sascha Date: Sun, 21 Mar 2021 13:46:49 +0100 Subject: [PATCH 124/157] Add tooltip to unimplemented navigations --- webui/src/components/Header/Header.tsx | 55 +++++++++++++++++++------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/webui/src/components/Header/Header.tsx b/webui/src/components/Header/Header.tsx index 1fe7504fe8c1f28e3d5dfdbe873c02d52252089d..dc649cb4695a305c6d785d4613cd73f201dc0511 100644 --- a/webui/src/components/Header/Header.tsx +++ b/webui/src/components/Header/Header.tsx @@ -5,6 +5,7 @@ import AppBar from '@material-ui/core/AppBar'; import Tab from '@material-ui/core/Tab'; import Tabs from '@material-ui/core/Tabs'; import Toolbar from '@material-ui/core/Toolbar'; +import Tooltip from '@material-ui/core/Tooltip/Tooltip'; import { makeStyles } from '@material-ui/core/styles'; import { LightSwitch } from '../../components/Themer'; @@ -53,6 +54,12 @@ function NavTabs() { setValue(newValue); }; + const tooltipMsg = `This feature doesn't exist yet. Come help us build it.`; + + /*The span elements around disabled tabs are needed, as the tooltip + * won't be triggered by disabled elements. + * See: https://material-ui.com/components/tooltips/#disabled-elements + */ return ( - + + + + + - - + + + + + + + + + + ); } From 00fbd0a2c861cfa873aad28ad78f69caf81e0e3c Mon Sep 17 00:00:00 2001 From: Sascha Date: Sun, 21 Mar 2021 16:43:37 +0100 Subject: [PATCH 125/157] Fix highlighting of tabs and error in console --- webui/src/components/Header/Header.tsx | 102 +++++++++++-------------- 1 file changed, 45 insertions(+), 57 deletions(-) diff --git a/webui/src/components/Header/Header.tsx b/webui/src/components/Header/Header.tsx index dc649cb4695a305c6d785d4613cd73f201dc0511..975944d74022df63d7fa8bf7d07fe8b9dfff6e7c 100644 --- a/webui/src/components/Header/Header.tsx +++ b/webui/src/components/Header/Header.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import AppBar from '@material-ui/core/AppBar'; -import Tab from '@material-ui/core/Tab'; +import Tab, { TabProps } from '@material-ui/core/Tab'; import Tabs from '@material-ui/core/Tabs'; import Toolbar from '@material-ui/core/Toolbar'; import Tooltip from '@material-ui/core/Tooltip/Tooltip'; @@ -45,75 +45,45 @@ function a11yProps(index: any) { }; } -function NavTabs() { - const [value, setValue] = React.useState(0); - - //TODO page refresh resets state. Must parse url to determine which tab is - //highlighted - const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { - setValue(newValue); - }; - - const tooltipMsg = `This feature doesn't exist yet. Come help us build it.`; - +const DisabledTabWithTooltip = (props: TabProps) => { /*The span elements around disabled tabs are needed, as the tooltip * won't be triggered by disabled elements. * See: https://material-ui.com/components/tooltips/#disabled-elements + * This must be done in a wrapper component, otherwise the TabS component + * cannot pass it styles down to the Tab component. Resulting in (console) + * warnings. This wrapper acceps the passed down TabProps and pass it around + * the span element to the Tab component. */ + const msg = `This feature doesn't exist yet. Come help us build it.`; + console.log(props); return ( - - - - - - - - - - - - - - - - - - + + + + + ); -} +}; function Header() { const classes = useStyles(); + const location = useLocation(); + const [selectedTab, setTab] = React.useState(location.pathname); + console.log(location.pathname); + + const handleTabClick = ( + event: React.ChangeEvent<{}>, + newTabValue: string + ) => { + setTab(newTabValue); + }; return ( <> - git-bug + git-bug logo git-bug
@@ -124,7 +94,25 @@ function Header() {
- + + + + + + ); } From f752dd54806122ea9d77b6807246897eb2656f0e Mon Sep 17 00:00:00 2001 From: Sascha Date: Sun, 21 Mar 2021 16:53:00 +0100 Subject: [PATCH 126/157] Remove BackToList button from BugPage --- webui/src/pages/bug/Bug.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/webui/src/pages/bug/Bug.tsx b/webui/src/pages/bug/Bug.tsx index bde8c5dd3e09ac30801d816cd1a2b300eb601328..3b6b61e0afcfb4f703937fc53fcd6222e6a47410 100644 --- a/webui/src/pages/bug/Bug.tsx +++ b/webui/src/pages/bug/Bug.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { makeStyles } from '@material-ui/core/styles'; -import BackToListButton from '../../components/BackToListButton'; import BugTitleForm from 'src/components/BugTitleForm/BugTitleForm'; import IfLoggedIn from 'src/components/IfLoggedIn/IfLoggedIn'; import Label from 'src/components/Label'; @@ -16,13 +15,13 @@ import TimelineQuery from './TimelineQuery'; */ const useStyles = makeStyles((theme) => ({ main: { - maxWidth: 1200, + maxWidth: 1000, margin: 'auto', marginTop: theme.spacing(4), }, header: { marginRight: theme.spacing(2), - marginLeft: theme.spacing(3) + 205, + marginLeft: theme.spacing(3) + 40, }, title: { ...theme.typography.h5, @@ -43,10 +42,6 @@ const useStyles = makeStyles((theme) => ({ marginRight: theme.spacing(2), minWidth: 400, }, - leftSidebar: { - marginTop: theme.spacing(2), - marginRight: theme.spacing(2), - }, rightSidebar: { marginTop: theme.spacing(2), flex: '0 0 200px', @@ -87,9 +82,6 @@ function Bug({ bug }: Props) {
-
- -
From 50f146a42a3b59341531b5f478217b10d7033ead Mon Sep 17 00:00:00 2001 From: Sascha Date: Sun, 21 Mar 2021 17:01:21 +0100 Subject: [PATCH 127/157] Remove BackToList button from NewBugPage --- webui/src/pages/new/NewBugPage.tsx | 87 ++++++++++-------------------- 1 file changed, 29 insertions(+), 58 deletions(-) diff --git a/webui/src/pages/new/NewBugPage.tsx b/webui/src/pages/new/NewBugPage.tsx index f313ac24e7c3b513e913e29632a69b5fba3ac280..2181d44c99731467dd753f3386945348c1d72e63 100644 --- a/webui/src/pages/new/NewBugPage.tsx +++ b/webui/src/pages/new/NewBugPage.tsx @@ -4,7 +4,6 @@ import { useHistory } from 'react-router-dom'; import { Button, Paper } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; -import BackToListButton from '../../components/BackToListButton'; import BugTitleInput from '../../components/BugTitleForm/BugTitleInput'; import CommentInput from '../../components/CommentInput/CommentInput'; @@ -15,17 +14,12 @@ import { useNewBugMutation } from './NewBug.generated'; */ const useStyles = makeStyles((theme: Theme) => ({ main: { - maxWidth: 1200, + maxWidth: 800, margin: 'auto', marginTop: theme.spacing(4), marginBottom: theme.spacing(4), padding: theme.spacing(2), - }, - container: { - display: 'flex', - marginBottom: theme.spacing(1), - marginRight: theme.spacing(2), - marginLeft: theme.spacing(2), + overflow: 'hidden', }, form: { display: 'flex', @@ -39,21 +33,6 @@ const useStyles = makeStyles((theme: Theme) => ({ backgroundColor: theme.palette.success.main, color: theme.palette.success.contrastText, }, - leftSidebar: { - marginTop: theme.spacing(4), - marginRight: theme.spacing(2), - }, - rightSidebar: { - marginTop: theme.spacing(2), - flex: '0 0 200px', - }, - timeline: { - flex: 1, - marginTop: theme.spacing(2), - marginRight: theme.spacing(2), - minWidth: 400, - padding: theme.spacing(1), - }, })); /** @@ -93,42 +72,34 @@ function NewBugPage() { if (error) return
Error
; return ( -
-
-
- + +
+ { + issueTitleInput = node; + }} + label="Title" + variant="outlined" + fullWidth + margin="dense" + onChange={(event: any) => setIssueTitle(event.target.value)} + /> + setIssueComment(comment)} + /> +
+
- - - { - issueTitleInput = node; - }} - label="Title" - variant="outlined" - fullWidth - margin="dense" - onChange={(event: any) => setIssueTitle(event.target.value)} - /> - setIssueComment(comment)} - /> -
- -
- -
-
-
-
+ + ); } From aa91f39cdf98217cb0b26c70f344148275617220 Mon Sep 17 00:00:00 2001 From: Sascha Date: Sun, 21 Mar 2021 17:34:57 +0100 Subject: [PATCH 128/157] Fix (hoepfully) eslint error for pipeline Unfortunatly will this result in error for eslint on local machine... *sigh* --- webui/src/components/Header/Header.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/webui/src/components/Header/Header.tsx b/webui/src/components/Header/Header.tsx index 975944d74022df63d7fa8bf7d07fe8b9dfff6e7c..70b8c9ff9f9149155ab7341dbe188e0d8916c6dc 100644 --- a/webui/src/components/Header/Header.tsx +++ b/webui/src/components/Header/Header.tsx @@ -83,7 +83,11 @@ function Header() { - git-bug logo + git-bug logo git-bug
@@ -100,7 +104,11 @@ function Header() { onChange={handleTabClick} aria-label="nav tabs" > - + Date: Sun, 21 Mar 2021 18:22:04 +0100 Subject: [PATCH 129/157] 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 fbf7c48b9ec53f94a2f56a3405b381f598c38f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 21 Mar 2021 19:45:26 +0100 Subject: [PATCH 130/157] webui: fix eslint? --- webui/src/components/Header/Header.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/webui/src/components/Header/Header.tsx b/webui/src/components/Header/Header.tsx index 70b8c9ff9f9149155ab7341dbe188e0d8916c6dc..975944d74022df63d7fa8bf7d07fe8b9dfff6e7c 100644 --- a/webui/src/components/Header/Header.tsx +++ b/webui/src/components/Header/Header.tsx @@ -83,11 +83,7 @@ function Header() { - git-bug logo + git-bug logo git-bug
@@ -104,11 +100,7 @@ function Header() { onChange={handleTabClick} aria-label="nav tabs" > - + Date: Sun, 21 Mar 2021 21:28:02 +0100 Subject: [PATCH 131/157] webui: stay within the SPA when redirecting from the header --- webui/src/components/Header/Header.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/webui/src/components/Header/Header.tsx b/webui/src/components/Header/Header.tsx index 975944d74022df63d7fa8bf7d07fe8b9dfff6e7c..3064f6e458d8a0ac3ebad6ae83ab52aaf22b2d33 100644 --- a/webui/src/components/Header/Header.tsx +++ b/webui/src/components/Header/Header.tsx @@ -8,8 +8,8 @@ import Toolbar from '@material-ui/core/Toolbar'; import Tooltip from '@material-ui/core/Tooltip/Tooltip'; import { makeStyles } from '@material-ui/core/styles'; -import { LightSwitch } from '../../components/Themer'; import CurrentIdentity from '../CurrentIdentity/CurrentIdentity'; +import { LightSwitch } from '../Themer'; const useStyles = makeStyles((theme) => ({ offset: { @@ -55,7 +55,6 @@ const DisabledTabWithTooltip = (props: TabProps) => { * the span element to the Tab component. */ const msg = `This feature doesn't exist yet. Come help us build it.`; - console.log(props); return ( @@ -69,7 +68,6 @@ function Header() { const classes = useStyles(); const location = useLocation(); const [selectedTab, setTab] = React.useState(location.pathname); - console.log(location.pathname); const handleTabClick = ( event: React.ChangeEvent<{}>, @@ -101,7 +99,7 @@ function Header() { aria-label="nav tabs" > - + Date: Sun, 21 Mar 2021 22:37:19 +0100 Subject: [PATCH 132/157] repo: fix security issue that could lead to arbitrary code execution see https://blog.golang.org/path-security for details --- go.mod | 2 +- go.sum | 2 ++ repository/git_cli.go | 5 +++-- repository/gogit.go | 4 ++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 1f8ea2303493789fe3502c8ea5db6e80f31b8c38..8f3d418ea5b104985aa0bcd272d0606b2af7302d 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( golang.org/x/net v0.0.0-20201024042810-be3efd7ff127 // indirect golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 - golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a // indirect + golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 golang.org/x/text v0.3.5 golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index 960409299663ba87d32e3532ce61d7a5f23397a5..57d1a0a3a5404f7e9151a3e4ee5b9544a0b53fe3 100644 --- a/go.sum +++ b/go.sum @@ -628,6 +628,8 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a h1:e3IU37lwO4aq3uoRKINC7JikojFmE5gO7xhfxs8VC34= golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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= diff --git a/repository/git_cli.go b/repository/git_cli.go index 085b1cda2dd8575a6bb6c540f0a39ad1575aa888..21cc40e24a8b5960597c323996ee711e1c5a1b53 100644 --- a/repository/git_cli.go +++ b/repository/git_cli.go @@ -4,8 +4,9 @@ import ( "bytes" "fmt" "io" - "os/exec" "strings" + + "golang.org/x/sys/execabs" ) // gitCli is a helper to launch CLI git commands @@ -21,7 +22,7 @@ func (cli gitCli) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, // fmt.Printf("[%s] Running git %s\n", path, strings.Join(args, " ")) - cmd := exec.Command("git", args...) + cmd := execabs.Command("git", args...) cmd.Dir = path cmd.Stdin = stdin cmd.Stdout = stdout diff --git a/repository/gogit.go b/repository/gogit.go index bdac259de85ebda97275ee7fc402c9e9c0023782..f2d2b57e716182bc2e005d2ef89eb03292fcb5e7 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -5,7 +5,6 @@ import ( "fmt" "io/ioutil" "os" - "os/exec" "path/filepath" "sort" "strings" @@ -20,6 +19,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/sys/execabs" "github.com/MichaelMure/git-bug/util/lamport" ) @@ -261,7 +261,7 @@ func (repo *GoGitRepo) GetCoreEditor() (string, error) { } for _, cmd := range priorities { - if _, err = exec.LookPath(cmd); err == nil { + if _, err = execabs.LookPath(cmd); err == nil { return cmd, nil } From bd6159a25b548f8f939f137b70dcf77a722e70dc Mon Sep 17 00:00:00 2001 From: Sascha Date: Mon, 22 Mar 2021 13:47:44 +0100 Subject: [PATCH 133/157] Treat description not as edit --- webui/src/pages/bug/MessageHistoryDialog.tsx | 29 ++++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/webui/src/pages/bug/MessageHistoryDialog.tsx b/webui/src/pages/bug/MessageHistoryDialog.tsx index 139eb4a599b4146490ccbd6b6a7d33f8e118f781..0ed33642cec6366e438457e2a5184d687b6018e1 100644 --- a/webui/src/pages/bug/MessageHistoryDialog.tsx +++ b/webui/src/pages/bug/MessageHistoryDialog.tsx @@ -173,6 +173,7 @@ function MessageHistoryDialog({ bugId, commentId, open, onClose }: Props) { // Sort by most recent edit. Must create a copy of constant history as // reverse() modifies inplace. const history = comment?.history.slice().reverse(); + const editCount = history?.length === undefined ? 0 : history?.length - 1; const handleChange = (panel: string) => ( event: React.ChangeEvent<{}>, @@ -181,6 +182,23 @@ function MessageHistoryDialog({ bugId, commentId, open, onClose }: Props) { setExpanded(newExpanded ? panel : false); }; + const getSummary = (index: number, date: Date) => { + const desc = + index === editCount ? 'Created ' : `#${editCount - index} • Edited `; + const mostRecent = index === 0 ? ' (most recent)' : ''; + return ( + <> + + + {desc} + + {mostRecent} + + + + ); + }; + return ( - Edited {history?.length} times. + {`Edited ${editCount} ${editCount > 1 ? 'times' : 'time'}.`} {history?.map((edit, index) => ( @@ -204,14 +222,7 @@ function MessageHistoryDialog({ bugId, commentId, open, onClose }: Props) { aria-controls="panel1d-content" id="panel1d-header" > - - {`#${history?.length - index} • Edited `} - - - - {index === 0 && ' (most recent edit)'} - {index === history?.length - 1 && ' (Initial description)'} - + {getSummary(index, edit.date)} {edit.message} From 890c014d919f705eac624547031c79205a71321b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 28 Mar 2021 22:18:01 +0200 Subject: [PATCH 134/157] repo: fix various config issues around case insentivity --- bridge/core/auth/credential.go | 6 ++++-- bug/bug_test.go | 5 +---- repository/config_mem.go | 19 ++++++++++++++--- repository/config_testing.go | 39 ++++++++++++++++++++++++++++++++++ repository/gogit_config.go | 2 +- 5 files changed, 61 insertions(+), 10 deletions(-) diff --git a/bridge/core/auth/credential.go b/bridge/core/auth/credential.go index 2327a6fc48b840a6020ae2923674b1d5f76753a7..20343c9c437742a0ef5520eb03c40643fdf25acc 100644 --- a/bridge/core/auth/credential.go +++ b/bridge/core/auth/credential.go @@ -3,12 +3,13 @@ package auth import ( "encoding/base64" "encoding/json" - "errors" "fmt" "strconv" "strings" "time" + "github.com/pkg/errors" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/repository" ) @@ -159,7 +160,8 @@ func List(repo repository.RepoKeyring, opts ...ListOption) ([]Credential, error) item, err := repo.Keyring().Get(key) if err != nil { - return nil, err + // skip unreadable items, nothing much we can do for them anyway + continue } cred, err := decode(item) diff --git a/bug/bug_test.go b/bug/bug_test.go index 6363f4e92db5bf2da5cc6620fea38f8cace203c5..d6ef6fa172c74c991073b2749a22721a13782699 100644 --- a/bug/bug_test.go +++ b/bug/bug_test.go @@ -25,10 +25,7 @@ func TestBugId(t *testing.T) { bug1.Append(createOp) err = bug1.Commit(mockRepo) - - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) bug1.Id() } diff --git a/repository/config_mem.go b/repository/config_mem.go index 9725e8d59a13ea550a5a473baa258d5b6ecd8124..019bc11167f1d2d939ac569ff4be0a6edfe234b5 100644 --- a/repository/config_mem.go +++ b/repository/config_mem.go @@ -20,6 +20,7 @@ func NewMemConfig() *MemConfig { } func (mc *MemConfig) StoreString(key, value string) error { + key = normalizeKey(key) mc.config[key] = value return nil } @@ -33,6 +34,7 @@ func (mc *MemConfig) StoreTimestamp(key string, value time.Time) error { } func (mc *MemConfig) ReadAll(keyPrefix string) (map[string]string, error) { + keyPrefix = normalizeKey(keyPrefix) result := make(map[string]string) for key, val := range mc.config { if strings.HasPrefix(key, keyPrefix) { @@ -44,6 +46,7 @@ func (mc *MemConfig) ReadAll(keyPrefix string) (map[string]string, error) { func (mc *MemConfig) ReadString(key string) (string, error) { // unlike git, the mock can only store one value for the same key + key = normalizeKey(key) val, ok := mc.config[key] if !ok { return "", ErrNoConfigEntry @@ -54,9 +57,9 @@ func (mc *MemConfig) ReadString(key string) (string, error) { func (mc *MemConfig) ReadBool(key string) (bool, error) { // unlike git, the mock can only store one value for the same key - val, ok := mc.config[key] - if !ok { - return false, ErrNoConfigEntry + val, err := mc.ReadString(key) + if err != nil { + return false, err } return strconv.ParseBool(val) @@ -78,6 +81,7 @@ func (mc *MemConfig) ReadTimestamp(key string) (time.Time, error) { // RmConfigs remove all key/value pair matching the key prefix func (mc *MemConfig) RemoveAll(keyPrefix string) error { + keyPrefix = normalizeKey(keyPrefix) found := false for key := range mc.config { if strings.HasPrefix(key, keyPrefix) { @@ -92,3 +96,12 @@ func (mc *MemConfig) RemoveAll(keyPrefix string) error { return nil } + +func normalizeKey(key string) string { + // this feels so wrong, but that's apparently how git behave. + // only section and final segment are case insensitive, subsection in between are not. + s := strings.Split(key, ".") + s[0] = strings.ToLower(s[0]) + s[len(s)-1] = strings.ToLower(s[len(s)-1]) + return strings.Join(s, ".") +} diff --git a/repository/config_testing.go b/repository/config_testing.go index 445f8721f1eb18b9e05c32e8044722ab783862ca..f8a2762b6eecde560264b4f0d9382db4007dac1d 100644 --- a/repository/config_testing.go +++ b/repository/config_testing.go @@ -113,4 +113,43 @@ func testConfig(t *testing.T, config Config) { "section.subsection.subsection.opt1": "foo5", "section.subsection.subsection.opt2": "foo6", }, all) + + // missing section + case insensitive + val, err = config.ReadString("section2.opt1") + require.Error(t, err) + + val, err = config.ReadString("section.opt1") + require.NoError(t, err) + require.Equal(t, "foo", val) + + val, err = config.ReadString("SECTION.OPT1") + require.NoError(t, err) + require.Equal(t, "foo", val) + + _, err = config.ReadString("SECTION2.OPT3") + require.Error(t, err) + + // missing subsection + case insensitive + val, err = config.ReadString("section.subsection.opt1") + require.NoError(t, err) + require.Equal(t, "foo3", val) + + // for some weird reason, subsection ARE case sensitive + _, err = config.ReadString("SECTION.SUBSECTION.OPT1") + require.Error(t, err) + + _, err = config.ReadString("SECTION.SUBSECTION1.OPT1") + require.Error(t, err) + + // missing sub-subsection + case insensitive + val, err = config.ReadString("section.subsection.subsection.opt1") + require.NoError(t, err) + require.Equal(t, "foo5", val) + + // for some weird reason, subsection ARE case sensitive + _, err = config.ReadString("SECTION.SUBSECTION.SUBSECTION.OPT1") + require.Error(t, err) + + _, err = config.ReadString("SECTION.SUBSECTION.SUBSECTION1.OPT1") + require.Error(t, err) } diff --git a/repository/gogit_config.go b/repository/gogit_config.go index ba61adcac651a9217e3c36dccc3a9747c1d68d2e..891e3ffb8525213bd99979ec9ee500204b159f4e 100644 --- a/repository/gogit_config.go +++ b/repository/gogit_config.go @@ -134,7 +134,7 @@ func (cr *goGitConfigReader) ReadString(key string) (string, error) { } return section.Option(optionName), nil default: - subsectionName := strings.Join(split[1:len(split)-2], ".") + subsectionName := strings.Join(split[1:len(split)-1], ".") optionName := split[len(split)-1] if !section.HasSubsection(subsectionName) { return "", ErrNoConfigEntry From 32958b5ca1901e8062bc67c9a03675ffd8ef4fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 28 Mar 2021 23:26:58 +0200 Subject: [PATCH 135/157] cache: only FTS index token < 100 characters --- cache/repo_cache_bug.go | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/cache/repo_cache_bug.go b/cache/repo_cache_bug.go index 1701f66d09fb6ae9c1ded3c0d9ead16bd2a65bf4..8d9914e3782faee7e6369cf277a79c040ebdd70a 100644 --- a/cache/repo_cache_bug.go +++ b/cache/repo_cache_bug.go @@ -8,12 +8,14 @@ import ( "sort" "strings" "time" + "unicode/utf8" + + "github.com/blevesearch/bleve" "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/query" "github.com/MichaelMure/git-bug/repository" - "github.com/blevesearch/bleve" ) const ( @@ -479,11 +481,24 @@ func (c *RepoCache) addBugToSearchIndex(snap *bug.Snapshot) error { Text []string }{} + // See https://github.com/blevesearch/bleve/issues/1576 + var sb strings.Builder + normalize := func(text string) string { + sb.Reset() + for _, field := range strings.Fields(text) { + if utf8.RuneCountInString(field) < 100 { + sb.WriteString(field) + sb.WriteRune(' ') + } + } + return sb.String() + } + for _, comment := range snap.Comments { - searchableBug.Text = append(searchableBug.Text, comment.Message) + searchableBug.Text = append(searchableBug.Text, normalize(comment.Message)) } - searchableBug.Text = append(searchableBug.Text, snap.Title) + searchableBug.Text = append(searchableBug.Text, normalize(snap.Title)) index, err := c.repo.GetBleveIndex("bug") if err != nil { From e985653701e8438e27ee5f925fd0aa7c0eef09fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Mon, 29 Mar 2021 10:08:57 +0200 Subject: [PATCH 136/157] cache: test for FTS bub with long description --- cache/repo_cache_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/cache/repo_cache_test.go b/cache/repo_cache_test.go index bd06e84db518c4ec7fc476fb7398d255d62189d5..7e648ea9045fa15f966e4d1308262a38df49c39c 100644 --- a/cache/repo_cache_test.go +++ b/cache/repo_cache_test.go @@ -1,7 +1,9 @@ package cache import ( + "strings" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -274,3 +276,21 @@ func checkBugPresence(t *testing.T, cache *RepoCache, bug *BugCache, presence bo require.Equal(t, bug, b) } } + +func TestLongDescription(t *testing.T) { + // See https://github.com/MichaelMure/git-bug/issues/606 + + text := strings.Repeat("x", 65536) + + repo := repository.CreateGoGitTestRepo(false) + defer repository.CleanupTestRepos(repo) + + backend, err := NewRepoCache(repo) + require.NoError(t, err) + + i, err := backend.NewIdentity("René Descartes", "rene@descartes.fr") + require.NoError(t, err) + + _, _, err = backend.NewBugRaw(i, time.Now().Unix(), text, text, nil, nil) + require.NoError(t, err) +} From f7dec7e96cff20e039d7f0d6f96d853157a6027f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Mon, 29 Mar 2021 11:13:40 +0200 Subject: [PATCH 137/157] cache: fix no-label filter not properly wired --- cache/filter.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cache/filter.go b/cache/filter.go index c167fe71a1f464e551ea952fa70db54403beb84f..7ea4067369aa80be2e22dbac93e9ad4d74efb0d2 100644 --- a/cache/filter.go +++ b/cache/filter.go @@ -153,6 +153,9 @@ func compileMatcher(filters query.Filters) *Matcher { for _, value := range filters.Title { result.Title = append(result.Title, TitleFilter(value)) } + if filters.NoLabel { + result.NoFilters = append(result.NoFilters, NoLabelFilter()) + } return result } 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 138/157] 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 } From cc3b7c328dd4e4ad51de15919962d62f1146ca51 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 16:06:52 +0000 Subject: [PATCH 139/157] Create Dependabot config file --- .github/dependabot.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000000000000000000000000000000000..bd1922620bdd3bfd47e2868fac27398cdab9161c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: +- package-ecosystem: gomod + directory: "/" + schedule: + interval: daily + time: "04:00" + open-pull-requests-limit: 10 From c61fd2a94fe9bc0e113ea58dd4da834eb3c2b0dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 16:17:43 +0000 Subject: [PATCH 140/157] Bump github.com/go-git/go-billy/v5 from 5.0.0 to 5.1.0 Bumps [github.com/go-git/go-billy/v5](https://github.com/go-git/go-billy) from 5.0.0 to 5.1.0. - [Release notes](https://github.com/go-git/go-billy/releases) - [Commits](https://github.com/go-git/go-billy/compare/v5.0.0...v5.1.0) Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 14 ++------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 357901c2aafa03494b741b60e6d868d049fc970a..6991af4d6c54ccdbb25d716f86c00b370a981295 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/corpix/uarand v0.1.1 // indirect github.com/dustin/go-humanize v1.0.0 github.com/fatih/color v1.10.0 - github.com/go-git/go-billy/v5 v5.0.0 + github.com/go-git/go-billy/v5 v5.1.0 github.com/go-git/go-git/v5 v5.2.0 github.com/golang/protobuf v1.4.3 // indirect github.com/gorilla/mux v1.8.0 diff --git a/go.sum b/go.sum index 4f7c33308b4809ea0a82f77f2478781c129758ba..c5a8da4f270c2d72ff6c958e9d20262aa07d0ddf 100644 --- a/go.sum +++ b/go.sum @@ -74,8 +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.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= @@ -184,8 +182,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/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-billy/v5 v5.1.0 h1:4pl5BV4o7ZG/lterP4S6WzJ6xr49Ba5ET9ygheTYahk= +github.com/go-git/go-billy/v5 v5.1.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= @@ -427,11 +426,9 @@ github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7 h1:Vk3RiBQpF0Ja+ github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk= github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= -github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 h1:SWV2fHctRpRrp49VXJ6UZja7gU9QLHwRpIPBN89SKEo= github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0 h1:JJV9CsgM9EC9w2iVkwuz+sMx8yRFe89PJRUrv6hPCIA= github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= @@ -480,8 +477,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.40.1 h1:jHueLh5Inzv20TL5Yki+CaLmyvtw3Yq7blbWx7GmglQ= -github.com/xanzy/go-gitlab v0.40.1/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= github.com/xanzy/go-gitlab v0.44.0 h1:cEiGhqu7EpFGuei2a2etAwB+x6403E5CvpLn35y+GPs= github.com/xanzy/go-gitlab v0.44.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= @@ -545,7 +540,6 @@ 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= @@ -637,8 +631,6 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a h1:e3IU37lwO4aq3uoRKINC7JikojFmE5gO7xhfxs8VC34= -golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -698,7 +690,6 @@ 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= @@ -801,7 +792,6 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= From 779b2654adb8ea2261d74192781fda6b636de6c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 16:18:05 +0000 Subject: [PATCH 141/157] Bump golang.org/x/text from 0.3.5 to 0.3.6 Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.5 to 0.3.6. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.3.5...v0.3.6) Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 15 ++------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 357901c2aafa03494b741b60e6d868d049fc970a..d41cdddeeb24b35582eb5661988dfbfe39438888 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 - golang.org/x/text v0.3.5 + golang.org/x/text v0.3.6 golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect google.golang.org/appengine v1.6.7 // indirect ) diff --git a/go.sum b/go.sum index 4f7c33308b4809ea0a82f77f2478781c129758ba..ef067e699d0cfcc64903f142107d8287469827ac 100644 --- a/go.sum +++ b/go.sum @@ -74,8 +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.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= @@ -427,11 +425,9 @@ github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7 h1:Vk3RiBQpF0Ja+ github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk= github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= -github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 h1:SWV2fHctRpRrp49VXJ6UZja7gU9QLHwRpIPBN89SKEo= github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0 h1:JJV9CsgM9EC9w2iVkwuz+sMx8yRFe89PJRUrv6hPCIA= github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= @@ -480,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.40.1 h1:jHueLh5Inzv20TL5Yki+CaLmyvtw3Yq7blbWx7GmglQ= -github.com/xanzy/go-gitlab v0.40.1/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= github.com/xanzy/go-gitlab v0.44.0 h1:cEiGhqu7EpFGuei2a2etAwB+x6403E5CvpLn35y+GPs= github.com/xanzy/go-gitlab v0.44.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= @@ -545,7 +539,6 @@ 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= @@ -637,8 +630,6 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a h1:e3IU37lwO4aq3uoRKINC7JikojFmE5gO7xhfxs8VC34= -golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -646,8 +637,8 @@ 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.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/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= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -698,7 +689,6 @@ 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= @@ -801,7 +791,6 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= From 54c5b66271f4f3d95881a186c8124e7e5dcdb6d4 Mon Sep 17 00:00:00 2001 From: Aien Saidi Date: Thu, 18 Mar 2021 18:00:16 +0100 Subject: [PATCH 142/157] feat: use author to filter the list --- webui/.eslintrc.js | 1 + webui/package.json | 2 +- webui/src/pages/list/FilterToolbar.tsx | 28 +++++++++++++++++++-- webui/src/pages/list/ListIdentities.graphql | 13 ++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 webui/src/pages/list/ListIdentities.graphql diff --git a/webui/.eslintrc.js b/webui/.eslintrc.js index 2dfa7543657db5d31cc4dcbae3b250f5461a2501..125fe8015b46fc85170d43c9c0d115c38a5c4558 100644 --- a/webui/.eslintrc.js +++ b/webui/.eslintrc.js @@ -38,4 +38,5 @@ module.exports = { settings: { 'import/internal-regex': '^src/', }, + ignorePatterns: ['**/*.generated.tsx'], }; diff --git a/webui/package.json b/webui/package.json index 39696a25918f7fe4b2bd1f8748f1e83097ab9b69..a69473d870aeb7493eb5fc748c33c550368d299b 100644 --- a/webui/package.json +++ b/webui/package.json @@ -46,7 +46,7 @@ "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject", "generate": "graphql-codegen", - "lint": "eslint src --ext .ts --ext .tsx --ext .js --ext .jsx --ext .graphql", + "lint": "eslint --fix src --ext .ts --ext .tsx --ext .js --ext .jsx --ext .graphql", "clean": "rimraf src/**.generated.* src/schema.json src/gqlTypes.* src/fragmentTypes.*" }, "proxy": "http://localhost:3001", diff --git a/webui/src/pages/list/FilterToolbar.tsx b/webui/src/pages/list/FilterToolbar.tsx index 74eefe4c3184ac27c3b7649576bdccb43d2a16f9..92154ad240b0f83c7a4f81a7ffa94490c8dbf9a0 100644 --- a/webui/src/pages/list/FilterToolbar.tsx +++ b/webui/src/pages/list/FilterToolbar.tsx @@ -8,14 +8,15 @@ import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline'; import ErrorOutline from '@material-ui/icons/ErrorOutline'; import { + Filter, FilterDropdown, FilterProps, - Filter, parse, - stringify, Query, + stringify, } from './Filter'; import { useBugCountQuery } from './FilterToolbar.generated'; +import { useListIdentitiesQuery } from './ListIdentities.generated'; const useStyles = makeStyles((theme) => ({ toolbar: { @@ -35,6 +36,7 @@ type CountingFilterProps = { query: string; // the query used as a source to count the number of element children: React.ReactNode; } & FilterProps; + function CountingFilter({ query, children, ...props }: CountingFilterProps) { const { data, loading, error } = useBugCountQuery({ variables: { query }, @@ -57,9 +59,24 @@ type Props = { query: string; queryLocation: (query: string) => LocationDescriptor; }; + function FilterToolbar({ query, queryLocation }: Props) { const classes = useStyles(); const params: Query = parse(query); + const { data } = useListIdentitiesQuery(); + + let identities: any = []; + + if ( + data?.repository && + data.repository.allIdentities && + data.repository.allIdentities.nodes + ) { + identities = data.repository.allIdentities.nodes.map((node) => [ + node.name, + node.name, + ]); + } const hasKey = (key: string): boolean => params[key] && params[key].length > 0; @@ -115,6 +132,13 @@ function FilterToolbar({ query, queryLocation }: Props) { Author Label */} + hasValue('author', key)} + to={(key) => pipe(replaceParam('author', key), loc)(params)} + > + Author + Date: Thu, 18 Mar 2021 18:09:03 +0100 Subject: [PATCH 143/157] feat: add filter by label --- webui/src/pages/list/FilterToolbar.tsx | 31 +++++++++++++++++++++---- webui/src/pages/list/ListLabels.graphql | 9 +++++++ 2 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 webui/src/pages/list/ListLabels.graphql diff --git a/webui/src/pages/list/FilterToolbar.tsx b/webui/src/pages/list/FilterToolbar.tsx index 92154ad240b0f83c7a4f81a7ffa94490c8dbf9a0..37f63b98f8d3f92cbd49fd7214f7b707db31f4cf 100644 --- a/webui/src/pages/list/FilterToolbar.tsx +++ b/webui/src/pages/list/FilterToolbar.tsx @@ -17,6 +17,7 @@ import { } from './Filter'; import { useBugCountQuery } from './FilterToolbar.generated'; import { useListIdentitiesQuery } from './ListIdentities.generated'; +import { useListLabelsQuery } from './ListLabels.generated'; const useStyles = makeStyles((theme) => ({ toolbar: { @@ -63,16 +64,29 @@ type Props = { function FilterToolbar({ query, queryLocation }: Props) { const classes = useStyles(); const params: Query = parse(query); - const { data } = useListIdentitiesQuery(); + const { data: identitiesData } = useListIdentitiesQuery(); + const { data: labelsData } = useListLabelsQuery() let identities: any = []; + let labels: any = []; if ( - data?.repository && - data.repository.allIdentities && - data.repository.allIdentities.nodes + identitiesData?.repository && + identitiesData.repository.allIdentities && + identitiesData.repository.allIdentities.nodes ) { - identities = data.repository.allIdentities.nodes.map((node) => [ + identities = identitiesData.repository.allIdentities.nodes.map((node) => [ + node.name, + node.name, + ]); + } + + if ( + labelsData?.repository && + labelsData.repository.validLabels && + labelsData.repository.validLabels.nodes + ) { + labels = labelsData.repository.validLabels.nodes.map((node) => [ node.name, node.name, ]); @@ -139,6 +153,13 @@ function FilterToolbar({ query, queryLocation }: Props) { > Author + hasValue('label', key)} + to={(key) => pipe(replaceParam('label', key), loc)(params)} + > + Label + Date: Thu, 18 Mar 2021 18:17:18 +0100 Subject: [PATCH 144/157] feat: check if there are labels --- webui/src/pages/list/FilterToolbar.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/webui/src/pages/list/FilterToolbar.tsx b/webui/src/pages/list/FilterToolbar.tsx index 37f63b98f8d3f92cbd49fd7214f7b707db31f4cf..3046d9d88bf6f1cc311676bd112cdb6d1ce45c38 100644 --- a/webui/src/pages/list/FilterToolbar.tsx +++ b/webui/src/pages/list/FilterToolbar.tsx @@ -65,7 +65,7 @@ function FilterToolbar({ query, queryLocation }: Props) { const classes = useStyles(); const params: Query = parse(query); const { data: identitiesData } = useListIdentitiesQuery(); - const { data: labelsData } = useListLabelsQuery() + const { data: labelsData } = useListLabelsQuery(); let identities: any = []; let labels: any = []; @@ -153,13 +153,15 @@ function FilterToolbar({ query, queryLocation }: Props) { > Author - hasValue('label', key)} - to={(key) => pipe(replaceParam('label', key), loc)(params)} - > - Label - + {labels.length ? ( + hasValue('label', key)} + to={(key) => pipe(replaceParam('label', key), loc)(params)} + > + Label + + ) : null} Date: Wed, 24 Mar 2021 00:37:04 +0100 Subject: [PATCH 145/157] feat: multiple label filter - add search functionality in menu items --- webui/src/pages/list/Filter.tsx | 60 ++++++++++++++++++++------ webui/src/pages/list/FilterToolbar.tsx | 34 ++++++++++----- 2 files changed, 71 insertions(+), 23 deletions(-) diff --git a/webui/src/pages/list/Filter.tsx b/webui/src/pages/list/Filter.tsx index 667020786fbdf72720df1d38c8e90a3fdd1fa7bd..154d0f94e1479b028f7625575ef530a57779a605 100644 --- a/webui/src/pages/list/Filter.tsx +++ b/webui/src/pages/list/Filter.tsx @@ -1,14 +1,26 @@ import clsx from 'clsx'; import { LocationDescriptor } from 'history'; -import React, { useState, useRef } from 'react'; +import React, { useRef, useState } from 'react'; import { Link } from 'react-router-dom'; import Menu from '@material-ui/core/Menu'; import MenuItem from '@material-ui/core/MenuItem'; import { SvgIconProps } from '@material-ui/core/SvgIcon'; -import { makeStyles } from '@material-ui/core/styles'; +import TextField from '@material-ui/core/TextField'; +import { makeStyles, withStyles } from '@material-ui/core/styles'; import ArrowDropDown from '@material-ui/icons/ArrowDropDown'; +const CustomTextField = withStyles({ + root: { + margin: '0 8px 12px 8px', + '& label.Mui-focused': { + margin: '0 2px', + }, + }, +})(TextField); + +const ITEM_HEIGHT = 48; + export type Query = { [key: string]: string[] }; function parse(query: string): Query { @@ -90,6 +102,7 @@ type FilterDropdownProps = { itemActive: (key: string) => boolean; icon?: React.ComponentType; to: (key: string) => LocationDescriptor; + hasFilter?: boolean; } & React.ButtonHTMLAttributes; function FilterDropdown({ @@ -98,9 +111,11 @@ function FilterDropdown({ itemActive, icon: Icon, to, + hasFilter, ...props }: FilterDropdownProps) { const [open, setOpen] = useState(false); + const [filter, setFilter] = useState(''); const buttonRef = useRef(null); const classes = useStyles({ active: false }); @@ -135,18 +150,36 @@ function FilterDropdown({ open={open} onClose={() => setOpen(false)} anchorEl={buttonRef.current} + PaperProps={{ + style: { + maxHeight: ITEM_HEIGHT * 4.5, + width: '20ch', + }, + }} > - {dropdown.map(([key, value]) => ( - setOpen(false)} - key={key} - > - {value} - - ))} + {hasFilter && ( + { + const { value } = e.target; + setFilter(value); + }} + value={filter} + label={`Filter ${children}`} + /> + )} + {dropdown + .filter((d) => d[1].toLowerCase().includes(filter.toLowerCase())) + .map(([key, value]) => ( + setOpen(false)} + key={key} + > + {value} + + ))} ); @@ -158,6 +191,7 @@ export type FilterProps = { icon?: React.ComponentType; children: React.ReactNode; }; + function Filter({ active, to, children, icon: Icon }: FilterProps) { const classes = useStyles(); diff --git a/webui/src/pages/list/FilterToolbar.tsx b/webui/src/pages/list/FilterToolbar.tsx index 3046d9d88bf6f1cc311676bd112cdb6d1ce45c38..1af96d0f7498d24ec11ac98025da7804f02a58f5 100644 --- a/webui/src/pages/list/FilterToolbar.tsx +++ b/webui/src/pages/list/FilterToolbar.tsx @@ -109,6 +109,20 @@ function FilterToolbar({ query, queryLocation }: Props) { ...params, [key]: params[key] && params[key].includes(value) ? [] : [value], }); + const toggleOrAddParam = (key: string, value: string) => ( + params: Query + ): Query => { + const values = params[key]; + return { + ...params, + [key]: + params[key] && params[key].includes(value) + ? values.filter((v) => v !== value) + : values + ? [...values, value] + : [value], + }; + }; const clearParam = (key: string) => (params: Query): Query => ({ ...params, [key]: [], @@ -150,18 +164,18 @@ function FilterToolbar({ query, queryLocation }: Props) { dropdown={identities} itemActive={(key) => hasValue('author', key)} to={(key) => pipe(replaceParam('author', key), loc)(params)} + hasFilter > Author - {labels.length ? ( - hasValue('label', key)} - to={(key) => pipe(replaceParam('label', key), loc)(params)} - > - Label - - ) : null} + hasValue('label', key)} + to={(key) => pipe(toggleOrAddParam('label', key), loc)(params)} + hasFilter + > + Label + hasValue('sort', key)} - to={(key) => pipe(replaceParam('sort', key), loc)(params)} + to={(key) => pipe(toggleParam('sort', key), loc)(params)} > Sort From f82071a3d7140a1e21c02b27f03871794116af8a Mon Sep 17 00:00:00 2001 From: Aien Saidi Date: Wed, 24 Mar 2021 01:16:51 +0100 Subject: [PATCH 146/157] feat: use predefined filters --- webui/src/pages/list/Filter.tsx | 2 +- webui/src/pages/list/ListQuery.tsx | 65 ++++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/webui/src/pages/list/Filter.tsx b/webui/src/pages/list/Filter.tsx index 154d0f94e1479b028f7625575ef530a57779a605..54b2262b8d5ebef3d1c3874e1d0db17a22935580 100644 --- a/webui/src/pages/list/Filter.tsx +++ b/webui/src/pages/list/Filter.tsx @@ -153,7 +153,7 @@ function FilterDropdown({ PaperProps={{ style: { maxHeight: ITEM_HEIGHT * 4.5, - width: '20ch', + width: '25ch', }, }} > diff --git a/webui/src/pages/list/ListQuery.tsx b/webui/src/pages/list/ListQuery.tsx index 500ccf77dec04992955147f35dc4b1127306b71e..58cc75199bbaab6973894835500925859c251871 100644 --- a/webui/src/pages/list/ListQuery.tsx +++ b/webui/src/pages/list/ListQuery.tsx @@ -1,19 +1,23 @@ import { ApolloError } from '@apollo/client'; +import { pipe } from '@arrows/composition'; import React, { useState, useEffect, useRef } from 'react'; import { useLocation, useHistory, Link } from 'react-router-dom'; -import { Button } from '@material-ui/core'; +import { Button, FormControl, Menu, MenuItem } from '@material-ui/core'; import IconButton from '@material-ui/core/IconButton'; import InputBase from '@material-ui/core/InputBase'; import Paper from '@material-ui/core/Paper'; import { makeStyles, Theme } from '@material-ui/core/styles'; +import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; import ErrorOutline from '@material-ui/icons/ErrorOutline'; import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft'; import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight'; import Skeleton from '@material-ui/lab/Skeleton'; +import { useCurrentIdentityQuery } from '../../components/CurrentIdentity/CurrentIdentity.generated'; import IfLoggedIn from 'src/components/IfLoggedIn/IfLoggedIn'; +import { parse, Query, stringify } from './Filter'; import FilterToolbar from './FilterToolbar'; import List from './List'; import { useListBugsQuery } from './ListQuery.generated'; @@ -192,6 +196,8 @@ function ListQuery() { const query = params.has('q') ? params.get('q') || '' : 'status:open'; const [input, setInput] = useState(query); + const [filterMenuIsOpen, setFilterMenuIsOpen] = useState(false); + const filterButtonRef = useRef(null); const classes = useStyles({ searching: !!input }); @@ -293,14 +299,65 @@ function ListQuery() { history.push(queryLocation(input)); }; + const { + loading: ciqLoading, + error: ciqError, + data: ciqData, + } = useCurrentIdentityQuery(); + if (ciqError || ciqLoading || !ciqData?.repository?.userIdentity) { + return null; + } + const user = ciqData.repository.userIdentity; + + const loc = pipe(stringify, queryLocation); + const qparams: Query = parse(query); + const replaceParam = (key: string, value: string) => ( + params: Query + ): Query => ({ + ...params, + [key]: [value], + }); + return (
- + + + setFilterMenuIsOpen(false)} + getContentAnchorEl={null} + anchorEl={filterButtonRef.current} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'left', + }} + > + setFilterMenuIsOpen(false)} + > + Your newest issues + + + Date: Wed, 24 Mar 2021 17:13:20 +0100 Subject: [PATCH 147/157] fix: issue with toggling the author --- webui/src/pages/list/FilterToolbar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webui/src/pages/list/FilterToolbar.tsx b/webui/src/pages/list/FilterToolbar.tsx index 1af96d0f7498d24ec11ac98025da7804f02a58f5..59a09226a0a80605ceaeda07b024cf30bf0d321e 100644 --- a/webui/src/pages/list/FilterToolbar.tsx +++ b/webui/src/pages/list/FilterToolbar.tsx @@ -163,7 +163,7 @@ function FilterToolbar({ query, queryLocation }: Props) { hasValue('author', key)} - to={(key) => pipe(replaceParam('author', key), loc)(params)} + to={(key) => pipe(toggleParam('author', key), loc)(params)} hasFilter > Author @@ -174,7 +174,7 @@ function FilterToolbar({ query, queryLocation }: Props) { to={(key) => pipe(toggleOrAddParam('label', key), loc)(params)} hasFilter > - Label + Labels Date: Wed, 24 Mar 2021 18:12:05 +0100 Subject: [PATCH 148/157] fix: issue with regex --- webui/src/pages/list/Filter.tsx | 6 +++--- webui/src/pages/list/FilterToolbar.tsx | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/webui/src/pages/list/Filter.tsx b/webui/src/pages/list/Filter.tsx index 54b2262b8d5ebef3d1c3874e1d0db17a22935580..2ba6ffafa82aea493483287a183af2609946f92d 100644 --- a/webui/src/pages/list/Filter.tsx +++ b/webui/src/pages/list/Filter.tsx @@ -28,7 +28,7 @@ function parse(query: string): Query { const params: Query = {}; // TODO: support escaping without quotes - const re = /(\w+):([A-Za-z0-9-]+|(["'])(([^\3]|\\.)*)\3)+/g; + const re = /(\w+):([A-Za-z0-9-]+|"([^"]*)")/g; let matches; while ((matches = re.exec(query)) !== null) { if (!params[matches[1]]) { @@ -36,8 +36,8 @@ function parse(query: string): Query { } let value; - if (matches[4]) { - value = matches[4]; + if (matches[3]) { + value = matches[3]; } else { value = matches[2]; } diff --git a/webui/src/pages/list/FilterToolbar.tsx b/webui/src/pages/list/FilterToolbar.tsx index 59a09226a0a80605ceaeda07b024cf30bf0d321e..cf5994abe233bcbbeb1b2d6c21fc4a16313a7aa1 100644 --- a/webui/src/pages/list/FilterToolbar.tsx +++ b/webui/src/pages/list/FilterToolbar.tsx @@ -96,6 +96,8 @@ function FilterToolbar({ query, queryLocation }: Props) { params[key] && params[key].length > 0; const hasValue = (key: string, value: string): boolean => hasKey(key) && params[key].includes(value); + const containsValue = (key: string, value: string): boolean => + hasKey(key) && params[key].indexOf(value) !== -1; const loc = pipe(stringify, queryLocation); const replaceParam = (key: string, value: string) => ( params: Query @@ -170,7 +172,10 @@ function FilterToolbar({ query, queryLocation }: Props) { hasValue('label', key)} + itemActive={(key) => { + console.log(params, params[key], key); + return containsValue('label', key); + }} to={(key) => pipe(toggleOrAddParam('label', key), loc)(params)} hasFilter > From 1216fb1e27919c58c3df81950adaf556bdb73594 Mon Sep 17 00:00:00 2001 From: Aien Saidi Date: Wed, 24 Mar 2021 18:13:05 +0100 Subject: [PATCH 149/157] chore: returning value --- webui/src/pages/list/FilterToolbar.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/webui/src/pages/list/FilterToolbar.tsx b/webui/src/pages/list/FilterToolbar.tsx index cf5994abe233bcbbeb1b2d6c21fc4a16313a7aa1..cfb93dc2e53fc10dbee0fe7aac632ff97e794b41 100644 --- a/webui/src/pages/list/FilterToolbar.tsx +++ b/webui/src/pages/list/FilterToolbar.tsx @@ -172,10 +172,7 @@ function FilterToolbar({ query, queryLocation }: Props) { { - console.log(params, params[key], key); - return containsValue('label', key); - }} + itemActive={(key) => containsValue('label', key)} to={(key) => pipe(toggleOrAddParam('label', key), loc)(params)} hasFilter > From 72fc0ef73e6c8063075a77bcf37fc8bd7e4c8baf Mon Sep 17 00:00:00 2001 From: Aien Saidi Date: Wed, 24 Mar 2021 18:49:17 +0100 Subject: [PATCH 150/157] fix: issue with keyDown propagation --- webui/src/pages/list/Filter.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/webui/src/pages/list/Filter.tsx b/webui/src/pages/list/Filter.tsx index 2ba6ffafa82aea493483287a183af2609946f92d..782c2041f63f390754d9bb455caea50266695761 100644 --- a/webui/src/pages/list/Filter.tsx +++ b/webui/src/pages/list/Filter.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx'; import { LocationDescriptor } from 'history'; -import React, { useRef, useState } from 'react'; +import React, { useRef, useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import Menu from '@material-ui/core/Menu'; @@ -117,8 +117,13 @@ function FilterDropdown({ const [open, setOpen] = useState(false); const [filter, setFilter] = useState(''); const buttonRef = useRef(null); + const searchRef = useRef(null); const classes = useStyles({ active: false }); + useEffect(() => { + searchRef && searchRef.current && searchRef.current.focus(); + }, [filter]); + const content = ( <> {Icon && } @@ -139,6 +144,7 @@ function FilterDropdown({ e.stopPropagation()} value={filter} label={`Filter ${children}`} /> From d2845605c2152738e37d13b7067fedac91ccf225 Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 24 Mar 2021 20:16:25 +0100 Subject: [PATCH 151/157] Fix readability of filter input field in dark mode --- webui/src/pages/list/Filter.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/webui/src/pages/list/Filter.tsx b/webui/src/pages/list/Filter.tsx index 782c2041f63f390754d9bb455caea50266695761..42375dea7ce21ff9e953f14344c0c43cf7efe963 100644 --- a/webui/src/pages/list/Filter.tsx +++ b/webui/src/pages/list/Filter.tsx @@ -10,14 +10,21 @@ import TextField from '@material-ui/core/TextField'; import { makeStyles, withStyles } from '@material-ui/core/styles'; import ArrowDropDown from '@material-ui/icons/ArrowDropDown'; -const CustomTextField = withStyles({ +const CustomTextField = withStyles((theme) => ({ root: { margin: '0 8px 12px 8px', '& label.Mui-focused': { margin: '0 2px', + color: theme.palette.text.secondary, + }, + '& .MuiInput-underline::before': { + borderBottomColor: theme.palette.divider, + }, + '& .MuiInput-underline::after': { + borderBottomColor: theme.palette.divider, }, }, -})(TextField); +}))(TextField); const ITEM_HEIGHT = 48; From 41ee97a4f6e6b6c5c76d6b3f61fdeda5fbabe053 Mon Sep 17 00:00:00 2001 From: Aien Saidi Date: Tue, 30 Mar 2021 19:10:23 +0200 Subject: [PATCH 152/157] fix: regex issue --- webui/src/pages/list/Filter.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webui/src/pages/list/Filter.tsx b/webui/src/pages/list/Filter.tsx index 42375dea7ce21ff9e953f14344c0c43cf7efe963..2e99eedf08dcfa78f3151e5d6cb2f57eaea17f49 100644 --- a/webui/src/pages/list/Filter.tsx +++ b/webui/src/pages/list/Filter.tsx @@ -35,7 +35,7 @@ function parse(query: string): Query { const params: Query = {}; // TODO: support escaping without quotes - const re = /(\w+):([A-Za-z0-9-]+|"([^"]*)")/g; + const re = /(\w+):([A-Za-z0-9-]+|(["'])(([^\3]|\\.)*)\3)+/g; let matches; while ((matches = re.exec(query)) !== null) { if (!params[matches[1]]) { @@ -43,8 +43,8 @@ function parse(query: string): Query { } let value; - if (matches[3]) { - value = matches[3]; + if (matches[4]) { + value = matches[4]; } else { value = matches[2]; } From 3380135182d5c9b1abd6188eb85382db45c7f4ff Mon Sep 17 00:00:00 2001 From: Sascha Date: Fri, 26 Mar 2021 12:52:55 +0100 Subject: [PATCH 153/157] Make filter/search input full width --- webui/src/pages/list/ListQuery.tsx | 117 +++++++++++++---------------- 1 file changed, 54 insertions(+), 63 deletions(-) diff --git a/webui/src/pages/list/ListQuery.tsx b/webui/src/pages/list/ListQuery.tsx index 58cc75199bbaab6973894835500925859c251871..4cd75c8d8527c809a6a33c7e2f440d29ea3d5365 100644 --- a/webui/src/pages/list/ListQuery.tsx +++ b/webui/src/pages/list/ListQuery.tsx @@ -39,24 +39,17 @@ const useStyles = makeStyles((theme) => ({ }, header: { display: 'flex', - padding: theme.spacing(2), - '& > h1': { - ...theme.typography.h6, - margin: theme.spacing(0, 2), - }, - alignItems: 'center', - justifyContent: 'space-between', + padding: theme.spacing(1), }, filterissueLabel: { fontSize: '14px', fontWeight: 'bold', paddingRight: '12px', }, - filterissueContainer: { + form: { display: 'flex', - flexDirection: 'row', - alignItems: 'flex-start', - justifyContents: 'left', + flexGrow: 1, + marginRight: theme.spacing(1), }, search: { borderRadius: theme.shape.borderRadius, @@ -66,7 +59,7 @@ const useStyles = makeStyles((theme) => ({ borderWidth: '1px', backgroundColor: theme.palette.primary.light, padding: theme.spacing(0, 1), - width: ({ searching }) => (searching ? '20rem' : '15rem'), + width: '100%', transition: theme.transitions.create([ 'width', 'borderColor', @@ -321,58 +314,56 @@ function ListQuery() { return (
-
- - - - setFilterMenuIsOpen(false)} - getContentAnchorEl={null} - anchorEl={filterButtonRef.current} - anchorOrigin={{ - vertical: 'bottom', - horizontal: 'left', - }} - transformOrigin={{ - vertical: 'top', - horizontal: 'left', - }} - > - setFilterMenuIsOpen(false)} - > - Your newest issues - - - - setInput(e.target.value)} - classes={{ - root: classes.search, - focused: classes.searchFocused, + + + + setFilterMenuIsOpen(false)} + getContentAnchorEl={null} + anchorEl={filterButtonRef.current} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', }} - /> - - -
+ transformOrigin={{ + vertical: 'top', + horizontal: 'left', + }} + > + setFilterMenuIsOpen(false)} + > + Your newest issues + +
+ + setInput(e.target.value)} + classes={{ + root: classes.search, + focused: classes.searchFocused, + }} + /> + + {() => ( diff --git a/webui/src/pages/list/ListQuery.tsx b/webui/src/pages/list/ListQuery.tsx index 4cd75c8d8527c809a6a33c7e2f440d29ea3d5365..2b46dca50270b5f16bdbe183fa58e077edab76a1 100644 --- a/webui/src/pages/list/ListQuery.tsx +++ b/webui/src/pages/list/ListQuery.tsx @@ -369,7 +369,8 @@ function ListQuery() { From 554992523574684ecce36d38bf5310bff52c8c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 4 Apr 2021 13:28:21 +0200 Subject: [PATCH 157/157] cache: many fixes following the dag entity migration --- bug/operation.go | 2 ++ cache/repo_cache.go | 2 +- cache/repo_cache_test.go | 5 +++-- cache/resolvers.go | 29 +++++++++++++++++++++++++++++ entity/dag/clock.go | 7 ++++--- identity/resolver.go | 35 +++++++++++++++++++++++++++++++++++ repository/gogit.go | 2 +- 7 files changed, 75 insertions(+), 7 deletions(-) diff --git a/bug/operation.go b/bug/operation.go index 8daa2cde9ed0fe657aa69d3f9518c1d6d575bae2..b36103815233777373ccc7454a0d3c62a2bc188b 100644 --- a/bug/operation.go +++ b/bug/operation.go @@ -113,6 +113,8 @@ func operationUnmarshaller(author identity.Interface, raw json.RawMessage) (dag. op.Author_ = author case *CreateOperation: op.Author_ = author + case *EditCommentOperation: + op.Author_ = author case *LabelChangeOperation: op.Author_ = author case *NoOpOperation: diff --git a/cache/repo_cache.go b/cache/repo_cache.go index 58022bdad0ffee890f01531765fd0de986fded3e..14d5f3dbad6c26a8dd81f9495ef69f47ba0b7323 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.ReadAll(c.repo) + allBugs := bug.ReadAllWithResolver(c.repo, newIdentityCacheResolverNoLock(c)) // wipe the index just to be sure err := c.repo.ClearBleveIndex("bug") diff --git a/cache/repo_cache_test.go b/cache/repo_cache_test.go index fab8fff0ad52e2033426e81abb34a856a6de1ccb..d13fa026048726ddc3826ca440af90dfa2c06791 100644 --- a/cache/repo_cache_test.go +++ b/cache/repo_cache_test.go @@ -86,11 +86,12 @@ func TestCache(t *testing.T) { require.Empty(t, cache.identities) require.Empty(t, cache.identitiesExcerpts) - // Reload, only excerpt are loaded + // Reload, only excerpt are loaded, but as we need to load the identities used in the bugs + // to check the signatures, we also load the identity used above cache, err = NewRepoCache(repo) require.NoError(t, err) require.Empty(t, cache.bugs) - require.Empty(t, cache.identities) + require.Len(t, cache.identities, 1) require.Len(t, cache.bugExcerpts, 2) require.Len(t, cache.identitiesExcerpts, 2) diff --git a/cache/resolvers.go b/cache/resolvers.go index 36b70d3be99835417bf8763964a520ce4fea3fb2..e53c3660a0b3b87ac2088ae0c4ebe858471063e3 100644 --- a/cache/resolvers.go +++ b/cache/resolvers.go @@ -20,3 +20,32 @@ func newIdentityCacheResolver(cache *RepoCache) *identityCacheResolver { func (i *identityCacheResolver) ResolveIdentity(id entity.Id) (identity.Interface, error) { return i.cache.ResolveIdentity(id) } + +var _ identity.Resolver = &identityCacheResolverNoLock{} + +// identityCacheResolverNoLock is an identity Resolver that retrieve identities from +// the cache, without locking it. +type identityCacheResolverNoLock struct { + cache *RepoCache +} + +func newIdentityCacheResolverNoLock(cache *RepoCache) *identityCacheResolverNoLock { + return &identityCacheResolverNoLock{cache: cache} +} + +func (ir *identityCacheResolverNoLock) ResolveIdentity(id entity.Id) (identity.Interface, error) { + cached, ok := ir.cache.identities[id] + if ok { + return cached, nil + } + + i, err := identity.ReadLocal(ir.cache.repo, id) + if err != nil { + return nil, err + } + + cached = NewIdentityCache(ir.cache, i) + ir.cache.identities[id] = cached + + return cached, nil +} diff --git a/entity/dag/clock.go b/entity/dag/clock.go index dc9bb72dab386180cb94b67e0893df68acf2d74b..793fa1bfc5df1fbf47f5bf2d0687acc98bd21789 100644 --- a/entity/dag/clock.go +++ b/entity/dag/clock.go @@ -9,7 +9,7 @@ import ( // ClockLoader is the repository.ClockLoader for Entity func ClockLoader(defs ...Definition) repository.ClockLoader { - clocks := make([]string, len(defs)*2) + clocks := make([]string, 0, len(defs)*2) for _, def := range defs { clocks = append(clocks, fmt.Sprintf(creationClockPattern, def.Namespace)) clocks = append(clocks, fmt.Sprintf(editClockPattern, def.Namespace)) @@ -18,8 +18,9 @@ func ClockLoader(defs ...Definition) repository.ClockLoader { 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() + // we need to actually load the identities because of the commit signature check when reading, + // which require the full identities with crypto keys + resolver := identity.NewCachedResolver(identity.NewSimpleResolver(repo)) for _, def := range defs { // we actually just need to read all entities, diff --git a/identity/resolver.go b/identity/resolver.go index ab380a12802d3bf3e9c4c04298b885fef5095b38..8e066e9db00c013187c59bfb8744cb849aff5776 100644 --- a/identity/resolver.go +++ b/identity/resolver.go @@ -1,6 +1,8 @@ package identity import ( + "sync" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/repository" ) @@ -34,3 +36,36 @@ func NewStubResolver() *StubResolver { func (s *StubResolver) ResolveIdentity(id entity.Id) (Interface, error) { return &IdentityStub{id: id}, nil } + +// CachedResolver is a resolver ensuring that loading is done only once through another Resolver. +type CachedResolver struct { + mu sync.RWMutex + resolver Resolver + identities map[entity.Id]Interface +} + +func NewCachedResolver(resolver Resolver) *CachedResolver { + return &CachedResolver{ + resolver: resolver, + identities: make(map[entity.Id]Interface), + } +} + +func (c *CachedResolver) ResolveIdentity(id entity.Id) (Interface, error) { + c.mu.RLock() + if i, ok := c.identities[id]; ok { + c.mu.RUnlock() + return i, nil + } + c.mu.RUnlock() + + c.mu.Lock() + defer c.mu.Unlock() + + i, err := c.resolver.ResolveIdentity(id) + if err != nil { + return nil, err + } + c.identities[id] = i + return i, nil +} diff --git a/repository/gogit.go b/repository/gogit.go index 248c34d5789f9e15462dfd80af09dbb0ea055913..20454bd71b783d038384c4a386a4e7005420447f 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -335,7 +335,7 @@ func (repo *GoGitRepo) ClearBleveIndex(name string) error { repo.indexesMutex.Lock() defer repo.indexesMutex.Unlock() - path := filepath.Join(repo.path, "indexes", name) + path := filepath.Join(repo.path, "git-bug", "indexes", name) err := os.RemoveAll(path) if err != nil {