diff options
88 files changed, 2954 insertions, 1005 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 786ae3200..151cc3cff 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,7 +4,7 @@ stages: - qa default: - image: registry.gitlab.com/gitlab-org/gitlab-build-images/debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}-golang-${GO_VERSION}-postgresql-11:git-2.33 + image: registry.gitlab.com/gitlab-org/gitlab-build-images/debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}-golang-${GO_VERSION}:git-2.36 tags: - gitlab-org @@ -245,6 +245,8 @@ test:nightly: TEST_TARGET: [ test, test-with-proxies, test-with-praefect ] rules: - if: '$CI_PIPELINE_SOURCE == "schedule"' + - when: manual + allow_failure: true test:praefect_smoke: <<: *test_definition diff --git a/.gitlab/issue_templates/Support Request.md b/.gitlab/issue_templates/Support Request.md index a40b95049..522f12908 100644 --- a/.gitlab/issue_templates/Support Request.md +++ b/.gitlab/issue_templates/Support Request.md @@ -56,5 +56,5 @@ For S1 or S2 issues, please follow https://about.gitlab.com/handbook/engineering - [ ] Severity realistically set - [ ] Clearly articulated what is needed from the Gitaly team to support your request by filling out the _What specifically do you need from the Gitaly team_ -/label ~"Gitaly Customer Issue" ~"group::gitaly" ~"devops::create" ~"workflow::problem validation" +/label ~"Gitaly Customer Issue" ~"group::gitaly" ~"devops::systems" ~"section::enablement" ~"workflow::problem validation" /cc @mjwood @andrashorvath diff --git a/.golangci.yml b/.golangci.yml index db6ed2d7e..a6c5a1351 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,7 +1,7 @@ # options for analysis running run: # timeout for analysis, e.g. 30s, 5m, default is 1m - timeout: 5m + timeout: 10m modules-download-mode: readonly # list of useful linters could be found at https://github.com/golangci/awesome-go-linters @@ -59,9 +59,8 @@ linters-settings: - ^context.Background$ - ^context.TODO$ # Tests should not set the bare environment functions, but instead use - # `testhelper.ModifyEnvironment()`. This function has sanity checks to - # verify we don't use `t.Parallel()` when setting envvars by using the - # `t.Setenv()` helper. + # `t.Setenv()` or `testhelper.Unsetenv()`. These functions have sanity + # checks to verify we don't use `t.Parallel()` when setting envvars. - ^os.Setenv$ - ^os.Unsetenv$ stylecheck: @@ -56,6 +56,7 @@ PROTOC_GEN_GITALY_LINT := ${TOOLS_DIR}/protoc-gen-gitaly-lint PROTOC_GEN_GITALY_PROTOLIST := ${TOOLS_DIR}/protoc-gen-gitaly-protolist GOTESTSUM := ${TOOLS_DIR}/gotestsum GOCOVER_COBERTURA := ${TOOLS_DIR}/gocover-cobertura +DELVE := ${TOOLS_DIR}/dlv # Tool options GOLANGCI_LINT_OPTIONS ?= @@ -91,7 +92,7 @@ ifdef FIPS_MODE endif # Dependency versions -GOLANGCI_LINT_VERSION ?= v1.46.1 +GOLANGCI_LINT_VERSION ?= v1.46.2 PROTOLINT_VERSION ?= v0.38.1 GOCOVER_COBERTURA_VERSION ?= aaee18c8195c3f2d90e5ef80ca918d265463842a GOFUMPT_VERSION ?= v0.3.1 @@ -109,6 +110,7 @@ PROTOC_GEN_GO_GRPC_VERSION?= v1.2.0 # compatibility matrix. GIT2GO_VERSION ?= v33 LIBGIT2_VERSION ?= v1.3.0 +DELVE_VERSION ?= v1.8.3 # protoc target PROTOC_REPO_URL ?= https://github.com/protocolbuffers/protobuf @@ -253,6 +255,17 @@ run_go_tests = PATH='${SOURCE_DIR}/internal/testhelper/testdata/home/bin:${PATH} TEST_TMP_DIR='${TEST_TMP_DIR}' \ ${GOTESTSUM} --format ${TEST_FORMAT} --junitfile ${TEST_REPORT} --jsonfile ${TEST_FULL_OUTPUT} -- -ldflags '${GO_LDFLAGS}' -tags '${SERVER_BUILD_TAGS},${GIT2GO_BUILD_TAGS}' ${TEST_OPTIONS} ${TEST_PACKAGES} +## Test options passed to `dlv test`. +DEBUG_OPTIONS ?= $(patsubst -%,-test.%,${TEST_OPTIONS}) + +# debug_go_tests will execute Go tests from a single package in the delve debugger. +# Its behaviour can be modified via the following variable: +# +# DEBUG_OPTIONS: any additional options, will default to TEST_OPTIONS if not set. +debug_go_tests = PATH='${SOURCE_DIR}/internal/testhelper/testdata/home/bin:${PATH}' \ + TEST_TMP_DIR='${TEST_TMP_DIR}' \ + ${DELVE} test --build-flags="-ldflags '${GO_LDFLAGS}' -tags '${SERVER_BUILD_TAGS},${GIT2GO_BUILD_TAGS}'" ${TEST_PACKAGES} -- ${DEBUG_OPTIONS} + unexport GOROOT export GOCACHE ?= ${BUILD_DIR}/cache export GOPROXY ?= https://proxy.golang.org @@ -340,6 +353,9 @@ endif prepare-tests: libgit2 prepare-test-repos ${SOURCE_DIR}/.ruby-bundle ${GOTESTSUM} ${Q}mkdir -p "$(dir ${TEST_REPORT})" +.PHONY: prepare-debug +prepare-debug: ${DELVE} + .PHONY: prepare-test-repos prepare-test-repos: ${TEST_REPO_MIRROR} ${TEST_REPO} ${TEST_REPO_GIT} @@ -355,6 +371,11 @@ test-ruby: rspec test-go: prepare-tests ${Q}$(call run_go_tests) +.PHONY: debug-go-tests +## Run Go tests in delve debugger. +debug-test-go: prepare-tests prepare-debug + ${Q}$(call debug_go_tests) + .PHONY: test ## Run Go benchmarks. bench: TEST_OPTIONS := ${TEST_OPTIONS} -bench=. -run=^$ @@ -677,6 +698,8 @@ ${PROTOC_GEN_GO}: TOOL_PACKAGE = google.golang.org/protobuf/cmd/protoc-gen-g ${PROTOC_GEN_GO}: TOOL_VERSION = ${PROTOC_GEN_GO_VERSION} ${PROTOC_GEN_GO_GRPC}:TOOL_PACKAGE = google.golang.org/grpc/cmd/protoc-gen-go-grpc ${PROTOC_GEN_GO_GRPC}:TOOL_VERSION = ${PROTOC_GEN_GO_GRPC_VERSION} +${DELVE}: TOOL_PACKAGE = github.com/go-delve/delve/cmd/dlv +${DELVE}: TOOL_VERSION = ${DELVE_VERSION} ${TEST_REPO_MIRROR}: ${GIT} clone --mirror ${GIT_QUIET} https://gitlab.com/gitlab-org/gitlab-test.git $@ @@ -7670,6 +7670,210 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +LICENSE - github.com/go-enry/go-enry/v2 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LICENSE.md - github.com/go-enry/go-license-detector/v4/licensedb Apache License ============== @@ -1 +1 @@ -15.1.0-rc1
\ No newline at end of file +15.2.0-rc1
\ No newline at end of file diff --git a/_support/praefect-schema.sql b/_support/praefect-schema.sql index a1aa215b1..c0874080c 100644 --- a/_support/praefect-schema.sql +++ b/_support/praefect-schema.sql @@ -20,7 +20,7 @@ SET row_security = off; -- Name: praefect_database_schema; Type: DATABASE; Schema: -; Owner: - -- -CREATE DATABASE praefect_database_schema WITH TEMPLATE = template0 ENCODING = 'UTF8' LC_COLLATE = 'en_US.utf8' LC_CTYPE = 'en_US.utf8'; +CREATE DATABASE praefect_database_schema WITH TEMPLATE = template0 ENCODING = 'UTF8' LOCALE = 'en_US.utf8'; \connect praefect_database_schema @@ -98,8 +98,6 @@ CREATE FUNCTION public.notify_on_change() RETURNS trigger SET default_tablespace = ''; -SET default_with_oids = false; - -- -- Name: node_status; Type: TABLE; Schema: public; Owner: - -- diff --git a/client/dial_test.go b/client/dial_test.go index f958fc13c..f488a3ede 100644 --- a/client/dial_test.go +++ b/client/dial_test.go @@ -128,8 +128,9 @@ func TestDial(t *testing.T) { } if tt.envSSLCertFile != "" { - testhelper.ModifyEnvironment(t, gitalyx509.SSLCertFile, tt.envSSLCertFile) + t.Setenv(gitalyx509.SSLCertFile, tt.envSSLCertFile) } + ctx := testhelper.Context(t) conn, err := Dial(tt.rawAddress, tt.dialOpts) @@ -216,8 +217,9 @@ func TestDialSidechannel(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.envSSLCertFile != "" { - testhelper.ModifyEnvironment(t, gitalyx509.SSLCertFile, tt.envSSLCertFile) + t.Setenv(gitalyx509.SSLCertFile, tt.envSSLCertFile) } + ctx := testhelper.Context(t) conn, err := DialSidechannel(ctx, tt.rawAddress, registry, tt.dialOpts) diff --git a/cmd/gitaly-git2go-v15/featureflags.go b/cmd/gitaly-git2go-v15/featureflags.go index b5b4126a1..669fe0445 100644 --- a/cmd/gitaly-git2go-v15/featureflags.go +++ b/cmd/gitaly-git2go-v15/featureflags.go @@ -27,9 +27,16 @@ func (featureFlagsSubcommand) Flags() *flag.FlagSet { } func (featureFlagsSubcommand) Run(ctx context.Context, decoder *gob.Decoder, encoder *gob.Encoder) error { - rawFlags := featureflag.RawFromContext(ctx) + var flags []git2go.FeatureFlag + for flag, value := range featureflag.FromContext(ctx) { + flags = append(flags, git2go.FeatureFlag{ + Name: flag.Name, + MetadataKey: flag.MetadataKey(), + Value: value, + }) + } return encoder.Encode(git2go.FeatureFlags{ - Raw: rawFlags, + Flags: flags, }) } diff --git a/cmd/gitaly-git2go-v15/main.go b/cmd/gitaly-git2go-v15/main.go index 060cdb033..2ce2796ae 100644 --- a/cmd/gitaly-git2go-v15/main.go +++ b/cmd/gitaly-git2go-v15/main.go @@ -9,7 +9,6 @@ import ( "flag" "fmt" "os" - "strconv" "strings" "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus" @@ -17,7 +16,6 @@ import ( "github.com/sirupsen/logrus" "gitlab.com/gitlab-org/gitaly/v15/internal/git2go" glog "gitlab.com/gitlab-org/gitaly/v15/internal/log" - "gitlab.com/gitlab-org/gitaly/v15/internal/metadata" "gitlab.com/gitlab-org/gitaly/v15/internal/metadata/featureflag" "gitlab.com/gitlab-org/labkit/correlation" ) @@ -133,12 +131,8 @@ func main() { subcmdLogger.Infof("starting %s command", subcmdFlags.Name()) ctx = ctxlogrus.ToContext(ctx, subcmdLogger) - - featureFlags := make(featureflag.Raw) - enabledFeatureFlags.SetRaw(featureFlags, true) - disabledFeatureFlags.SetRaw(featureFlags, false) - - ctx = metadata.OutgoingToIncoming(featureflag.OutgoingWithRaw(ctx, featureFlags)) + ctx = enabledFeatureFlags.ToContext(ctx, true) + ctx = disabledFeatureFlags.ToContext(ctx, false) if err := subcmd.Run(ctx, decoder, encoder); err != nil { subcmdLogger.WithError(err).Errorf("%s command failed", subcmdFlags.Name()) @@ -148,10 +142,14 @@ func main() { subcmdLogger.Infof("%s command finished", subcmdFlags.Name()) } -type featureFlagArg []string +type featureFlagArg []featureflag.FeatureFlag func (v *featureFlagArg) String() string { - return strings.Join(*v, ",") + metadataKeys := make([]string, 0, len(*v)) + for _, flag := range *v { + metadataKeys = append(metadataKeys, flag.MetadataKey()) + } + return strings.Join(metadataKeys, ",") } func (v *featureFlagArg) Set(s string) error { @@ -159,15 +157,22 @@ func (v *featureFlagArg) Set(s string) error { return nil } - for _, enabledFF := range strings.Split(s, ",") { - *v = append(*v, enabledFF) + for _, metadataKey := range strings.Split(s, ",") { + flag, err := featureflag.FromMetadataKey(metadataKey) + if err != nil { + return err + } + + *v = append(*v, flag) } return nil } -func (v featureFlagArg) SetRaw(raw featureflag.Raw, enabled bool) { - for _, ff := range v { - raw[ff] = strconv.FormatBool(enabled) +func (v featureFlagArg) ToContext(ctx context.Context, enabled bool) context.Context { + for _, flag := range v { + ctx = featureflag.IncomingCtxWithFeatureFlag(ctx, flag, enabled) } + + return ctx } diff --git a/cmd/gitaly-hooks/hooks.go b/cmd/gitaly-hooks/hooks.go index 35248e143..95bca501e 100644 --- a/cmd/gitaly-hooks/hooks.go +++ b/cmd/gitaly-hooks/hooks.go @@ -161,7 +161,10 @@ func executeHook(cmd hookCommand, args []string) error { hookClient := gitalypb.NewHookServiceClient(conn) - ctx = featureflag.OutgoingWithRaw(ctx, payload.FeatureFlags) + for _, flag := range payload.FeatureFlagsWithValue { + ctx = featureflag.OutgoingCtxWithFeatureFlag(ctx, flag.Flag, flag.Enabled) + } + if err := cmd.exec(ctx, payload, hookClient, args); err != nil { return err } diff --git a/cmd/gitaly-hooks/hooks_test.go b/cmd/gitaly-hooks/hooks_test.go index 9db2ea23f..7e3f6d8ed 100644 --- a/cmd/gitaly-hooks/hooks_test.go +++ b/cmd/gitaly-hooks/hooks_test.go @@ -50,10 +50,10 @@ var ( disabledFeatureFlag = featureflag.FeatureFlag{Name: "disabled-feature-flag", OnByDefault: true} ) -func rawFeatureFlags(ctx context.Context) featureflag.Raw { +func featureFlags(ctx context.Context) map[featureflag.FeatureFlag]bool { ctx = featureflag.IncomingCtxWithFeatureFlag(ctx, enabledFeatureFlag, true) ctx = featureflag.IncomingCtxWithFeatureFlag(ctx, disabledFeatureFlag, false) - return featureflag.RawFromContext(ctx) + return featureflag.FromContext(ctx) } // envForHooks generates a set of environment variables for gitaly hooks @@ -62,7 +62,7 @@ func envForHooks(t testing.TB, ctx context.Context, cfg config.Cfg, repo *gitaly UserID: glHookValues.GLID, Username: glHookValues.GLUsername, Protocol: glHookValues.GLProtocol, - }, git.AllHooks, rawFeatureFlags(ctx)).Env() + }, git.AllHooks, featureFlags(ctx)).Env() require.NoError(t, err) env := append(command.AllowedEnvironment(os.Environ()), []string{ @@ -413,7 +413,7 @@ func TestHooksPostReceiveFailed(t *testing.T) { Protocol: glProtocol, }, git.PostReceiveHook, - rawFeatureFlags(ctx), + featureFlags(ctx), ).Env() require.NoError(t, err) diff --git a/cmd/gitaly-ssh/auth_test.go b/cmd/gitaly-ssh/auth_test.go index a8cf5a6a3..007522314 100644 --- a/cmd/gitaly-ssh/auth_test.go +++ b/cmd/gitaly-ssh/auth_test.go @@ -96,8 +96,7 @@ func TestConnectivity(t *testing.T) { name: "tls", addr: func(t *testing.T, cfg config.Cfg) (string, string) { certFile, keyFile := testhelper.GenerateCerts(t) - - testhelper.ModifyEnvironment(t, x509.SSLCertFile, certFile) + t.Setenv(x509.SSLCertFile, certFile) cfg.TLSListenAddr = "localhost:0" cfg.TLS = config.TLS{ diff --git a/doc/beginners_guide.md b/doc/beginners_guide.md index a610aa2db..ab81e1c80 100644 --- a/doc/beginners_guide.md +++ b/doc/beginners_guide.md @@ -228,6 +228,17 @@ called on `testing.T`. [require]: https://github.com/stretchr/testify/tree/master/require [assert]: https://github.com/stretchr/testify/tree/master/assert +##### Using Delve to debug a test + +The process to debug a test in your terminal using +[Delve](https://github.com/go-delve/delve) is almost the same as +[running a single test](#running-a-specific-test), just change the +target to `debug-test-go`: + +``` +TEST_PACKAGES=./internal/gitaly/service/repository TEST_OPTIONS="-count=1 -run=TestRepositoryExists" make debug-test-go +``` + ##### Useful snippets for creating a test ###### testhelper package @@ -5,9 +5,10 @@ go 1.17 require ( github.com/beevik/ntp v0.3.0 github.com/cloudflare/tableflip v1.2.2 - github.com/containerd/cgroups v0.0.0-20201118023556-2819c83ced99 + github.com/containerd/cgroups v1.0.4 github.com/getsentry/sentry-go v0.13.0 github.com/git-lfs/git-lfs/v3 v3.2.0 + github.com/go-enry/go-enry/v2 v2.8.2 github.com/go-enry/go-license-detector/v4 v4.3.0 github.com/google/go-cmp v0.5.8 github.com/google/uuid v1.3.0 @@ -28,7 +29,7 @@ require ( github.com/prometheus/client_model v0.2.0 github.com/rubenv/sql-migrate v0.0.0-20191213152630-06338513c237 github.com/sirupsen/logrus v1.8.1 - github.com/stretchr/testify v1.7.2 + github.com/stretchr/testify v1.8.0 github.com/uber/jaeger-client-go v2.30.0+incompatible gitlab.com/gitlab-org/gitlab-shell v1.9.8-0.20210720163109-50da611814d2 gitlab.com/gitlab-org/labkit v1.15.0 @@ -97,6 +98,7 @@ require ( github.com/git-lfs/go-netrc v0.0.0-20210914205454-f0c862dd687a // indirect github.com/git-lfs/pktline v0.0.0-20210330133718-06e9096e2825 // indirect github.com/git-lfs/wildmatch/v2 v2.0.1 // indirect + github.com/go-enry/go-oniguruma v1.2.1 // indirect github.com/go-git/gcfg v1.5.0 // indirect github.com/go-git/go-billy/v5 v5.1.0 // indirect github.com/go-git/go-git/v5 v5.3.0 // indirect @@ -252,6 +252,7 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= +github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/reopen v1.0.0 h1:8tpLVR74DLpLObrn2KvsyxJY++2iORGR17WLUdSzUws= @@ -273,8 +274,9 @@ github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMe github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= -github.com/containerd/cgroups v0.0.0-20201118023556-2819c83ced99 h1:8aDph9eB64Upszigf7VXsoA8NVHNQMpbo8luXmT6DkM= github.com/containerd/cgroups v0.0.0-20201118023556-2819c83ced99/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v1.0.4 h1:jN/mbWBEaz+T1pi5OFtnkQ+8qnmEbAr1Oo1FRm5B0dA= +github.com/containerd/cgroups v1.0.4/go.mod h1:nLNQtsF7Sl2HxNebu77i1R0oDlhiTG+kO4JTrUzo6IA= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -349,6 +351,7 @@ github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= @@ -386,8 +389,12 @@ github.com/git-lfs/wildmatch/v2 v2.0.1/go.mod h1:EVqonpk9mXbREP3N8UkwoWdrF249uHp github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/go-enry/go-enry/v2 v2.8.2 h1:uiGmC+3K8sVd/6DOe2AOJEOihJdqda83nPyJNtMR8RI= +github.com/go-enry/go-enry/v2 v2.8.2/go.mod h1:GVzIiAytiS5uT/QiuakK7TF1u4xDab87Y8V5EJRpsIQ= github.com/go-enry/go-license-detector/v4 v4.3.0 h1:OFlQAVNw5FlKUjX4OuW8JOabu8MQHjTKDb9pdeNYMUw= github.com/go-enry/go-license-detector/v4 v4.3.0/go.mod h1:HaM4wdNxSlz/9Gw0uVOKSQS5JVFqf2Pk8xUPEn6bldI= +github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo= +github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= @@ -1041,8 +1048,9 @@ github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 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= @@ -1050,8 +1058,9 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ= diff --git a/internal/backup/backup_test.go b/internal/backup/backup_test.go index 3bcaad4c8..0ae30291f 100644 --- a/internal/backup/backup_test.go +++ b/internal/backup/backup_test.go @@ -602,8 +602,9 @@ func TestResolveSink(t *testing.T) { } { t.Run(tc.desc, func(t *testing.T) { for k, v := range tc.envs { - testhelper.ModifyEnvironment(t, k, v) + t.Setenv(k, v) } + sink, err := ResolveSink(ctx, tc.path) if tc.errMsg != "" { require.EqualError(t, err, tc.errMsg) diff --git a/internal/cache/keyer.go b/internal/cache/keyer.go index 72e696adb..b89b240db 100644 --- a/internal/cache/keyer.go +++ b/internal/cache/keyer.go @@ -274,15 +274,18 @@ func compositeKeyHashHex(ctx context.Context, genID string, req proto.Message) ( h := sha256.New() - ffs := featureflag.AllFlags(ctx) - sort.Strings(ffs) + var flagsWithValue []string + for flag, enabled := range featureflag.FromContext(ctx) { + flagsWithValue = append(flagsWithValue, flag.FormatWithValue(enabled)) + } + sort.Strings(flagsWithValue) for _, i := range []string{ version.GetVersion(), method, genID, string(reqSum), - strings.Join(ffs, " "), + strings.Join(flagsWithValue, " "), } { _, err := h.Write(prefixLen(i)) if err != nil { diff --git a/internal/cgroups/v1_linux.go b/internal/cgroups/v1_linux.go index 3fa5da4c3..17065ba20 100644 --- a/internal/cgroups/v1_linux.go +++ b/internal/cgroups/v1_linux.go @@ -17,10 +17,10 @@ import ( // CGroupV1Manager is the manager for cgroups v1 type CGroupV1Manager struct { - cfg cgroupscfg.Config - hierarchy func() ([]cgroups.Subsystem, error) - memoryFailedTotal, cpuUsage *prometheus.GaugeVec - procs *prometheus.GaugeVec + cfg cgroupscfg.Config + hierarchy func() ([]cgroups.Subsystem, error) + memoryReclaimAttemptsTotal, cpuUsage *prometheus.GaugeVec + procs *prometheus.GaugeVec } func newV1Manager(cfg cgroupscfg.Config) *CGroupV1Manager { @@ -29,16 +29,16 @@ func newV1Manager(cfg cgroupscfg.Config) *CGroupV1Manager { hierarchy: func() ([]cgroups.Subsystem, error) { return defaultSubsystems(cfg.Mountpoint) }, - memoryFailedTotal: prometheus.NewGaugeVec( + memoryReclaimAttemptsTotal: prometheus.NewGaugeVec( prometheus.GaugeOpts{ - Name: "gitaly_cgroup_memory_failed_total", + Name: "gitaly_cgroup_memory_reclaim_attempts_total", Help: "Number of memory usage hits limits", }, []string{"path"}, ), cpuUsage: prometheus.NewGaugeVec( prometheus.GaugeOpts{ - Name: "gitaly_cgroup_cpu_usage", + Name: "gitaly_cgroup_cpu_usage_total", Help: "CPU Usage of Cgroup", }, []string{"path", "type"}, @@ -140,6 +140,10 @@ func (cg *CGroupV1Manager) addToCgroup(pid int, cgroupPath string) error { // Collect collects metrics from the cgroups controller func (cg *CGroupV1Manager) Collect(ch chan<- prometheus.Metric) { + if !cg.cfg.MetricsEnabled { + return + } + for i := 0; i < int(cg.cfg.Repositories.Count); i++ { repoPath := cg.repoPath(i) logger := log.Default().WithField("cgroup_path", repoPath) @@ -155,7 +159,7 @@ func (cg *CGroupV1Manager) Collect(ch chan<- prometheus.Metric) { if metrics, err := control.Stat(); err != nil { logger.WithError(err).Warn("unable to get cgroup stats") } else { - memoryMetric := cg.memoryFailedTotal.WithLabelValues(repoPath) + memoryMetric := cg.memoryReclaimAttemptsTotal.WithLabelValues(repoPath) memoryMetric.Set(float64(metrics.Memory.Usage.Failcnt)) ch <- memoryMetric diff --git a/internal/cgroups/v1_linux_test.go b/internal/cgroups/v1_linux_test.go index aaa095db0..f0c898cfd 100644 --- a/internal/cgroups/v1_linux_test.go +++ b/internal/cgroups/v1_linux_test.go @@ -193,29 +193,42 @@ func TestMetrics(t *testing.T) { repoCgroupPath := filepath.Join(v1Manager1.currentProcessCgroup(), "repos-0") - expected := bytes.NewBufferString(fmt.Sprintf(`# HELP gitaly_cgroup_cpu_usage CPU Usage of Cgroup -# TYPE gitaly_cgroup_cpu_usage gauge -gitaly_cgroup_cpu_usage{path="%s",type="kernel"} 0 -gitaly_cgroup_cpu_usage{path="%s",type="user"} 0 -# HELP gitaly_cgroup_memory_failed_total Number of memory usage hits limits -# TYPE gitaly_cgroup_memory_failed_total gauge -gitaly_cgroup_memory_failed_total{path="%s"} 2 + expected := bytes.NewBufferString(fmt.Sprintf(`# HELP gitaly_cgroup_cpu_usage_total CPU Usage of Cgroup +# TYPE gitaly_cgroup_cpu_usage_total gauge +gitaly_cgroup_cpu_usage_total{path="%s",type="kernel"} 0 +gitaly_cgroup_cpu_usage_total{path="%s",type="user"} 0 +# HELP gitaly_cgroup_memory_reclaim_attempts_total Number of memory usage hits limits +# TYPE gitaly_cgroup_memory_reclaim_attempts_total gauge +gitaly_cgroup_memory_reclaim_attempts_total{path="%s"} 2 # HELP gitaly_cgroup_procs_total Total number of procs # TYPE gitaly_cgroup_procs_total gauge gitaly_cgroup_procs_total{path="%s",subsystem="cpu"} 1 gitaly_cgroup_procs_total{path="%s",subsystem="memory"} 1 `, repoCgroupPath, repoCgroupPath, repoCgroupPath, repoCgroupPath, repoCgroupPath)) - assert.NoError(t, testutil.CollectAndCompare( - v1Manager1, - expected)) - - logEntry := hook.LastEntry() - assert.Contains( - t, - logEntry.Data["command.cgroup_path"], - repoCgroupPath, - "log field includes a cgroup path that is a subdirectory of the current process' cgroup path", - ) + + for _, metricsEnabled := range []bool{true, false} { + t.Run(fmt.Sprintf("metrics enabled: %v", metricsEnabled), func(t *testing.T) { + v1Manager1.cfg.MetricsEnabled = metricsEnabled + + if metricsEnabled { + assert.NoError(t, testutil.CollectAndCompare( + v1Manager1, + expected)) + } else { + assert.NoError(t, testutil.CollectAndCompare( + v1Manager1, + bytes.NewBufferString(""))) + } + + logEntry := hook.LastEntry() + assert.Contains( + t, + logEntry.Data["command.cgroup_path"], + repoCgroupPath, + "log field includes a cgroup path that is a subdirectory of the current process' cgroup path", + ) + }) + } } func readCgroupFile(t *testing.T, path string) []byte { diff --git a/internal/command/command_test.go b/internal/command/command_test.go index 4e2f80248..98569183f 100644 --- a/internal/command/command_test.go +++ b/internal/command/command_test.go @@ -116,7 +116,7 @@ func TestNew_exportedEnvironment(t *testing.T) { t.Skip("System Integrity Protection prevents using dynamic linker (dyld) environment variables on macOS. https://apple.co/2XDH4iC") } - testhelper.ModifyEnvironment(t, tc.key, tc.value) + t.Setenv(tc.key, tc.value) var buf bytes.Buffer cmd, err := New(ctx, exec.Command("/usr/bin/env"), WithStdout(&buf)) @@ -133,7 +133,7 @@ func TestNew_unexportedEnv(t *testing.T) { ctx := testhelper.Context(t) unexportedEnvKey, unexportedEnvVal := "GITALY_UNEXPORTED_ENV", "foobar" - testhelper.ModifyEnvironment(t, unexportedEnvKey, unexportedEnvVal) + t.Setenv(unexportedEnvKey, unexportedEnvVal) var buf bytes.Buffer cmd, err := New(ctx, exec.Command("/usr/bin/env"), WithStdout(&buf)) diff --git a/internal/git/command_factory.go b/internal/git/command_factory.go index b9046b5d4..9a4c8b881 100644 --- a/internal/git/command_factory.go +++ b/internal/git/command_factory.go @@ -520,6 +520,34 @@ func (cf *ExecCommandFactory) globalConfiguration(ctx context.Context) ([]Global // mechanism to replace malicious commits with seemingly benign ones. We thus globally // disable this mechanism. ConfigPair{Key: "core.useReplaceRefs", Value: "false"}, + + // Commit-graphs are used as an optimization to speed up reading commits and to be + // able to perform certain commit-related queries faster. One property that these + // graphs are storing is the generation number of a commit, where there are two + // different types of generation numbers: + // + // - Topological level: a commit with no parents has a level of 1. A commit with + // at least one parent has a level one more than the largest topological level + // of its parents. + // + // - Corrected committer date: a commit with no parents has a corrected commit + // date equal to its committer date. A commit with at least one parent has a + // corrected committer date equal to the maximum between either its own + // committer date or the largest corrected committer date across its parents + // plus 1. + // + // By default, newer Git versions store both generation numbers for commits, where + // the corrected committer date allows for some more optimizations. But due to a bug + // in Git v2.35.0 and earlier, the corrected committer date wasn't ever read. + // + // This bug was fixed in Git v2.36.0, together with a few other bugs in this area. + // But unfortunately, a new bug was introduced: when upgrading a commit-graph + // written by Git v2.35.0 or newer with Git v2.36.0 and later with `--changed-paths` + // enabled then the resulting commit-graph may be corrupt. + // + // Let's disable reading and writing corrected committer dates for now until the fix + // to this issue is upstream. + ConfigPair{Key: "commitGraph.generationVersion", Value: "1"}, } // Git v2.36.0 introduced new fine-grained configuration for what data should be fsynced and diff --git a/internal/git/command_factory_test.go b/internal/git/command_factory_test.go index ede45c745..b47119a71 100644 --- a/internal/git/command_factory_test.go +++ b/internal/git/command_factory_test.go @@ -33,7 +33,7 @@ func TestGitCommandProxy(t *testing.T) { })) defer ts.Close() - testhelper.ModifyEnvironment(t, "http_proxy", ts.URL) + t.Setenv("http_proxy", ts.URL) ctx := testhelper.Context(t) @@ -146,8 +146,8 @@ func TestExecCommandFactory_NewWithDir(t *testing.T) { } func TestCommandFactory_ExecutionEnvironment(t *testing.T) { - testhelper.ModifyEnvironment(t, "GITALY_TESTING_GIT_BINARY", "") - testhelper.ModifyEnvironment(t, "GITALY_TESTING_BUNDLED_GIT_PATH", "") + testhelper.Unsetenv(t, "GITALY_TESTING_GIT_BINARY") + testhelper.Unsetenv(t, "GITALY_TESTING_BUNDLED_GIT_PATH") ctx := testhelper.Context(t) @@ -198,7 +198,7 @@ func TestCommandFactory_ExecutionEnvironment(t *testing.T) { }) t.Run("set using GITALY_TESTING_GIT_BINARY", func(t *testing.T) { - testhelper.ModifyEnvironment(t, "GITALY_TESTING_GIT_BINARY", "/path/to/env_git") + t.Setenv("GITALY_TESTING_GIT_BINARY", "/path/to/env_git") assertExecEnv(t, config.Cfg{ Git: config.Git{ @@ -234,7 +234,7 @@ func TestCommandFactory_ExecutionEnvironment(t *testing.T) { bundledGitExecutable := filepath.Join(bundledGitDir, "gitaly-git"+suffix) bundledGitRemoteExecutable := filepath.Join(bundledGitDir, "gitaly-git-remote-http"+suffix) - testhelper.ModifyEnvironment(t, "GITALY_TESTING_BUNDLED_GIT_PATH", bundledGitDir) + t.Setenv("GITALY_TESTING_BUNDLED_GIT_PATH", bundledGitDir) t.Run("missing bin_dir", func(t *testing.T) { _, _, err := git.NewExecCommandFactory(config.Cfg{Git: config.Git{}}, git.WithSkipHooks()) @@ -321,7 +321,7 @@ func TestCommandFactory_ExecutionEnvironment(t *testing.T) { }) t.Run("doesn't exist in the system", func(t *testing.T) { - testhelper.ModifyEnvironment(t, "PATH", "") + testhelper.Unsetenv(t, "PATH") _, _, err := git.NewExecCommandFactory(config.Cfg{}, git.WithSkipHooks()) require.EqualError(t, err, "setting up Git execution environment: could not set up any Git execution environments") @@ -482,6 +482,7 @@ func TestExecCommandFactory_config(t *testing.T) { "gc.auto=0", "core.autocrlf=input", "core.usereplacerefs=false", + "commitgraph.generationversion=1", } for _, tc := range []struct { @@ -542,6 +543,7 @@ func TestExecCommandFactory_SidecarGitConfiguration(t *testing.T) { {Key: "gc.auto", Value: "0"}, {Key: "core.autocrlf", Value: "input"}, {Key: "core.useReplaceRefs", Value: "false"}, + {Key: "commitGraph.generationVersion", Value: "1"}, } commonTail := []git.ConfigPair{ diff --git a/internal/git/command_options.go b/internal/git/command_options.go index 044ae754d..89e24eabc 100644 --- a/internal/git/command_options.go +++ b/internal/git/command_options.go @@ -285,14 +285,17 @@ func withInternalFetch(req repoScopedRequest, withSidechannel bool) func(ctx con return helper.ErrInvalidArgumentf("empty Gitaly address") } - featureFlagPairs := featureflag.AllFlags(ctx) + var flagsWithValue []string + for flag, value := range featureflag.FromContext(ctx) { + flagsWithValue = append(flagsWithValue, flag.FormatWithValue(value)) + } c.env = append(c.env, fmt.Sprintf("GITALY_PAYLOAD=%s", payload), fmt.Sprintf("GIT_SSH_COMMAND=%s %s", filepath.Join(cfg.BinDir, "gitaly-ssh"), "upload-pack"), fmt.Sprintf("GITALY_ADDRESS=%s", storageInfo.Address), fmt.Sprintf("GITALY_TOKEN=%s", storageInfo.Token), - fmt.Sprintf("GITALY_FEATUREFLAGS=%s", strings.Join(featureFlagPairs, ",")), + fmt.Sprintf("GITALY_FEATUREFLAGS=%s", strings.Join(flagsWithValue, ",")), fmt.Sprintf("CORRELATION_ID=%s", correlation.ExtractFromContextOrGenerate(ctx)), // please see https://github.com/git/git/commit/0da0e49ba12225684b75e86a4c9344ad121652cb for mote details "GIT_SSH_VARIANT=simple", diff --git a/internal/git/execution_environment_test.go b/internal/git/execution_environment_test.go index d617227eb..a82910fcb 100644 --- a/internal/git/execution_environment_test.go +++ b/internal/git/execution_environment_test.go @@ -16,7 +16,7 @@ import ( func TestDistributedGitEnvironmentConstructor(t *testing.T) { constructor := git.DistributedGitEnvironmentConstructor{} - testhelper.ModifyEnvironment(t, "GITALY_TESTING_GIT_BINARY", "") + testhelper.Unsetenv(t, "GITALY_TESTING_GIT_BINARY") t.Run("empty configuration fails", func(t *testing.T) { _, err := constructor.Construct(config.Cfg{}) @@ -37,7 +37,7 @@ func TestDistributedGitEnvironmentConstructor(t *testing.T) { }) t.Run("empty configuration with environment override", func(t *testing.T) { - testhelper.ModifyEnvironment(t, "GITALY_TESTING_GIT_BINARY", "/foo/bar") + t.Setenv("GITALY_TESTING_GIT_BINARY", "/foo/bar") execEnv, err := constructor.Construct(config.Cfg{}) require.NoError(t, err) @@ -48,7 +48,7 @@ func TestDistributedGitEnvironmentConstructor(t *testing.T) { }) t.Run("configuration overrides environment variable", func(t *testing.T) { - testhelper.ModifyEnvironment(t, "GITALY_TESTING_GIT_BINARY", "envvar") + t.Setenv("GITALY_TESTING_GIT_BINARY", "envvar") execEnv, err := constructor.Construct(config.Cfg{ Git: config.Git{ @@ -64,7 +64,7 @@ func TestDistributedGitEnvironmentConstructor(t *testing.T) { } func TestBundledGitEnvironmentConstructor(t *testing.T) { - testhelper.ModifyEnvironment(t, "GITALY_TESTING_BUNDLED_GIT_PATH", "") + testhelper.Unsetenv(t, "GITALY_TESTING_BUNDLED_GIT_PATH") constructor := git.BundledGitEnvironmentConstructor{} @@ -147,13 +147,13 @@ func TestBundledGitEnvironmentConstructor(t *testing.T) { }) t.Run("bundled Git path without binary directory fails", func(t *testing.T) { - testhelper.ModifyEnvironment(t, "GITALY_TESTING_BUNDLED_GIT_PATH", "/does/not/exist") + t.Setenv("GITALY_TESTING_BUNDLED_GIT_PATH", "/does/not/exist") _, err := constructor.Construct(config.Cfg{}) require.Equal(t, errors.New("cannot use bundled binaries without bin path being set"), err) }) t.Run("nonexistent bundled Git path via environment fails", func(t *testing.T) { - testhelper.ModifyEnvironment(t, "GITALY_TESTING_BUNDLED_GIT_PATH", "/does/not/exist") + t.Setenv("GITALY_TESTING_BUNDLED_GIT_PATH", "/does/not/exist") _, err := constructor.Construct(config.Cfg{ BinDir: testhelper.TempDir(t), }) @@ -163,7 +163,7 @@ func TestBundledGitEnvironmentConstructor(t *testing.T) { t.Run("incomplete bundled Git environment fails", func(t *testing.T) { bundledGitPath := seedDirWithExecutables(t, "gitaly-git", "gitaly-git-remote-http") - testhelper.ModifyEnvironment(t, "GITALY_TESTING_BUNDLED_GIT_PATH", bundledGitPath) + t.Setenv("GITALY_TESTING_BUNDLED_GIT_PATH", bundledGitPath) _, err := constructor.Construct(config.Cfg{ BinDir: testhelper.TempDir(t), @@ -174,7 +174,7 @@ func TestBundledGitEnvironmentConstructor(t *testing.T) { t.Run("complete bundled Git environment populates binary directory", func(t *testing.T) { bundledGitPath := seedDirWithExecutables(t, "gitaly-git", "gitaly-git-remote-http", "gitaly-git-http-backend") - testhelper.ModifyEnvironment(t, "GITALY_TESTING_BUNDLED_GIT_PATH", bundledGitPath) + t.Setenv("GITALY_TESTING_BUNDLED_GIT_PATH", bundledGitPath) execEnv, err := constructor.Construct(config.Cfg{ BinDir: testhelper.TempDir(t), @@ -230,7 +230,7 @@ func TestFallbackGitEnvironmentConstructor(t *testing.T) { constructor := git.FallbackGitEnvironmentConstructor{} t.Run("failing lookup of executable causes failure", func(t *testing.T) { - testhelper.ModifyEnvironment(t, "PATH", "/does/not/exist") + t.Setenv("PATH", "/does/not/exist") _, err := constructor.Construct(config.Cfg{}) require.Equal(t, fmt.Errorf("%w: no git executable found in PATH", git.ErrNotConfigured), err) @@ -241,7 +241,7 @@ func TestFallbackGitEnvironmentConstructor(t *testing.T) { gitPath := filepath.Join(tempDir, "git") require.NoError(t, os.WriteFile(gitPath, nil, 0o755)) - testhelper.ModifyEnvironment(t, "PATH", "/does/not/exist:"+tempDir) + t.Setenv("PATH", "/does/not/exist:"+tempDir) execEnv, err := constructor.Construct(config.Cfg{}) require.NoError(t, err) diff --git a/internal/git/gitpipe/diff_tree.go b/internal/git/gitpipe/diff_tree.go new file mode 100644 index 000000000..c41e77a6a --- /dev/null +++ b/internal/git/gitpipe/diff_tree.go @@ -0,0 +1,147 @@ +package gitpipe + +import ( + "bufio" + "bytes" + "context" + "fmt" + "strings" + + "gitlab.com/gitlab-org/gitaly/v15/internal/git" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/localrepo" +) + +// diffTreeConfig is configuration for the DiffTree pipeline step. +type diffTreeConfig struct { + recursive bool + ignoreSubmodules bool + skipResult func(*RevisionResult) bool +} + +// DiffTreeOption is an option for the DiffTree pipeline step. +type DiffTreeOption func(cfg *diffTreeConfig) + +// DiffTreeWithRecursive will make DiffTree recurse into subtrees. +func DiffTreeWithRecursive() DiffTreeOption { + return func(cfg *diffTreeConfig) { + cfg.recursive = true + } +} + +// DiffTreeWithIgnoreSubmodules causes git-diff-tree(1) to exclude submodule changes. +func DiffTreeWithIgnoreSubmodules() DiffTreeOption { + return func(cfg *diffTreeConfig) { + cfg.ignoreSubmodules = true + } +} + +// DiffTreeWithSkip will execute the given function for each RevisionResult processed by the +// pipeline. If the callback returns `true`, then the object will be skipped and not passed down +// the pipeline. +func DiffTreeWithSkip(skipResult func(*RevisionResult) bool) DiffTreeOption { + return func(cfg *diffTreeConfig) { + cfg.skipResult = skipResult + } +} + +// DiffTree runs git-diff-tree(1) between the two given revisions. The returned +// channel will contain the new object IDs listed by this command. For deleted +// files this would be git.ZeroOID. Cancelling the context will cause the +// pipeline to be cancelled, too. By default, it will not recurse into subtrees. +func DiffTree( + ctx context.Context, + repo *localrepo.Repo, + leftRevision, rightRevision string, + options ...DiffTreeOption, +) RevisionIterator { + var cfg diffTreeConfig + for _, option := range options { + option(&cfg) + } + + resultChan := make(chan RevisionResult) + go func() { + defer close(resultChan) + + flags := []git.Option{} + + if cfg.recursive { + flags = append(flags, git.Flag{Name: "-r"}) + } + if cfg.ignoreSubmodules { + flags = append(flags, git.Flag{Name: "--ignore-submodules"}) + } + + var stderr strings.Builder + cmd, err := repo.Exec(ctx, + git.SubCmd{ + Name: "diff-tree", + Flags: flags, + Args: []string{leftRevision, rightRevision}, + }, + git.WithStderr(&stderr), + ) + if err != nil { + sendRevisionResult(ctx, resultChan, RevisionResult{ + err: fmt.Errorf("executing diff-tree: %w", err), + }) + return + } + + scanner := bufio.NewScanner(cmd) + for scanner.Scan() { + // We need to copy the line here because we'll hand it over to the caller + // asynchronously, and the next call to `Scan()` will overwrite the buffer. + line := make([]byte, len(scanner.Bytes())) + copy(line, scanner.Bytes()) + + attrsAndFile := bytes.SplitN(line, []byte{'\t'}, 2) + if len(attrsAndFile) != 2 { + sendRevisionResult(ctx, resultChan, RevisionResult{ + err: fmt.Errorf("splitting diff-tree attributes and file"), + }) + return + } + + attrs := bytes.SplitN(attrsAndFile[0], []byte{' '}, 5) + if len(attrs) != 5 { + sendRevisionResult(ctx, resultChan, RevisionResult{ + err: fmt.Errorf("splitting diff-tree attributes"), + }) + return + } + + result := RevisionResult{ + OID: git.ObjectID(attrs[3]), + ObjectName: attrsAndFile[1], + } + + if cfg.skipResult != nil && cfg.skipResult(&result) { + continue + } + + if isDone := sendRevisionResult(ctx, resultChan, result); isDone { + return + } + } + + if err := scanner.Err(); err != nil { + sendRevisionResult(ctx, resultChan, RevisionResult{ + err: fmt.Errorf("scanning diff-tree output: %w", err), + }) + return + } + + if err := cmd.Wait(); err != nil { + sendRevisionResult(ctx, resultChan, RevisionResult{ + err: fmt.Errorf("diff-tree pipeline command: %w, stderr: %q", err, stderr.String()), + }) + return + } + }() + + return &revisionIterator{ + ctx: ctx, + ch: resultChan, + } +} diff --git a/internal/git/gitpipe/diff_tree_test.go b/internal/git/gitpipe/diff_tree_test.go new file mode 100644 index 000000000..700772244 --- /dev/null +++ b/internal/git/gitpipe/diff_tree_test.go @@ -0,0 +1,140 @@ +package gitpipe + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/gittest" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/localrepo" + "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper" + "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper/testcfg" +) + +func TestDiffTree(t *testing.T) { + cfg := testcfg.Build(t) + + repoProto, _ := gittest.CloneRepo(t, cfg, cfg.Storages[0]) + repo := localrepo.NewTestRepo(t, cfg, repoProto) + + for _, tc := range []struct { + desc string + leftRevision string + rightRevision string + options []DiffTreeOption + expectedResults []RevisionResult + expectedErr error + }{ + { + desc: "single file", + leftRevision: "b83d6e391c22777fca1ed3012fce84f633d7fed0", + rightRevision: "4a24d82dbca5c11c61556f3b35ca472b7463187e", + expectedResults: []RevisionResult{ + { + OID: "c60514b6d3d6bf4bec1030f70026e34dfbd69ad5", + ObjectName: []byte("README.md"), + }, + }, + }, + { + desc: "single file in subtree without recursive", + leftRevision: "7975be0116940bf2ad4321f79d02a55c5f7779aa", + rightRevision: "1e292f8fedd741b75372e19097c76d327140c312", + expectedResults: []RevisionResult{ + { + OID: "ceb102b8d3f9a95c2eb979213e49f7cc1b23d56e", + ObjectName: []byte("files"), + }, + }, + }, + { + desc: "single file in subtree with recursive", + leftRevision: "7975be0116940bf2ad4321f79d02a55c5f7779aa", + rightRevision: "1e292f8fedd741b75372e19097c76d327140c312", + options: []DiffTreeOption{ + DiffTreeWithRecursive(), + }, + expectedResults: []RevisionResult{ + { + OID: "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + ObjectName: []byte("files/flat/path/correct/content.txt"), + }, + }, + }, + { + desc: "with submodules", + leftRevision: "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", + rightRevision: "5937ac0a7beb003549fc5fd26fc247adbce4a52e", + expectedResults: []RevisionResult{ + { + OID: "efd587ccb47caf5f31fc954edb21f0a713d9ecc3", + ObjectName: []byte(".gitmodules"), + }, + { + OID: "645f6c4c82fd3f5e06f67134450a570b795e55a6", + ObjectName: []byte("gitlab-grack"), + }, + }, + }, + { + desc: "without submodules", + leftRevision: "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", + rightRevision: "5937ac0a7beb003549fc5fd26fc247adbce4a52e", + options: []DiffTreeOption{ + DiffTreeWithIgnoreSubmodules(), + }, + expectedResults: []RevisionResult{ + { + OID: "efd587ccb47caf5f31fc954edb21f0a713d9ecc3", + ObjectName: []byte(".gitmodules"), + }, + }, + }, + { + desc: "with skip function", + leftRevision: "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", + rightRevision: "5937ac0a7beb003549fc5fd26fc247adbce4a52e", + options: []DiffTreeOption{ + DiffTreeWithSkip(func(r *RevisionResult) bool { + return r.OID == "efd587ccb47caf5f31fc954edb21f0a713d9ecc3" + }), + }, + expectedResults: []RevisionResult{ + { + OID: "645f6c4c82fd3f5e06f67134450a570b795e55a6", + ObjectName: []byte("gitlab-grack"), + }, + }, + }, + { + desc: "invalid revision", + leftRevision: "refs/heads/master", + rightRevision: "refs/heads/does-not-exist", + expectedErr: errors.New("diff-tree pipeline command: exit status 128, stderr: " + + "\"fatal: ambiguous argument 'refs/heads/does-not-exist': unknown revision or path not in the working tree.\\n" + + "Use '--' to separate paths from revisions, like this:\\n" + + "'git <command> [<revision>...] -- [<file>...]'\\n\""), + }, + } { + t.Run(tc.desc, func(t *testing.T) { + ctx := testhelper.Context(t) + + it := DiffTree(ctx, repo, tc.leftRevision, tc.rightRevision, tc.options...) + + var results []RevisionResult + for it.Next() { + results = append(results, it.Result()) + } + + // We're converting the error here to a plain un-nested error such that we + // don't have to replicate the complete error's structure. + err := it.Err() + if err != nil { + err = errors.New(err.Error()) + } + + require.Equal(t, tc.expectedErr, err) + require.Equal(t, tc.expectedResults, results) + }) + } +} diff --git a/internal/git/gitpipe/ls_tree.go b/internal/git/gitpipe/ls_tree.go new file mode 100644 index 000000000..10c6237c1 --- /dev/null +++ b/internal/git/gitpipe/ls_tree.go @@ -0,0 +1,126 @@ +package gitpipe + +import ( + "context" + "errors" + "fmt" + "io" + "strings" + + "gitlab.com/gitlab-org/gitaly/v15/internal/git" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/localrepo" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/lstree" +) + +// lsTreeConfig is configuration for the LsTree pipeline step. +type lsTreeConfig struct { + recursive bool + typeFilter func(*lstree.Entry) bool + skipResult func(*RevisionResult) bool +} + +// LsTreeOption is an option for the LsTree pipeline step. +type LsTreeOption func(cfg *lsTreeConfig) + +// LsTreeWithRecursive will make LsTree recursive into subtrees. +func LsTreeWithRecursive() LsTreeOption { + return func(cfg *lsTreeConfig) { + cfg.recursive = true + } +} + +// LsTreeWithBlobFilter configures LsTree to only pass through blob objects. +func LsTreeWithBlobFilter() LsTreeOption { + return func(cfg *lsTreeConfig) { + cfg.typeFilter = func(e *lstree.Entry) bool { return e.Type == lstree.Blob } + } +} + +// LsTree runs git-ls-tree(1) for the given revisions. The returned channel will +// contain all object IDs listed by this command. This might include: +// - Blobs +// - Trees, unless you're calling it with LsTreeWithRecursive() +// - Submodules, referring to the commit of the submodule +func LsTree( + ctx context.Context, + repo *localrepo.Repo, + revision string, + options ...LsTreeOption, +) RevisionIterator { + var cfg lsTreeConfig + for _, option := range options { + option(&cfg) + } + + resultChan := make(chan RevisionResult) + go func() { + defer close(resultChan) + + flags := []git.Option{ + git.Flag{Name: "-z"}, + } + + if cfg.recursive { + flags = append(flags, git.Flag{Name: "-r"}) + } + + var stderr strings.Builder + cmd, err := repo.Exec(ctx, + git.SubCmd{ + Name: "ls-tree", + Flags: flags, + Args: []string{revision}, + }, + git.WithStderr(&stderr), + ) + if err != nil { + sendRevisionResult(ctx, resultChan, RevisionResult{ + err: fmt.Errorf("spawning ls-tree: %w", err), + }) + return + } + + parser := lstree.NewParser(cmd) + for { + entry, err := parser.NextEntry() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + sendRevisionResult(ctx, resultChan, RevisionResult{ + err: fmt.Errorf("scanning ls-tree output: %w", err), + }) + return + } + + if cfg.typeFilter != nil && !cfg.typeFilter(entry) { + continue + } + + result := RevisionResult{ + OID: entry.ObjectID, + ObjectName: []byte(entry.Path), + } + + if cfg.skipResult != nil && cfg.skipResult(&result) { + continue + } + + if isDone := sendRevisionResult(ctx, resultChan, result); isDone { + return + } + } + + if err := cmd.Wait(); err != nil { + sendRevisionResult(ctx, resultChan, RevisionResult{ + err: fmt.Errorf("ls-tree pipeline command: %w, stderr: %q", err, stderr.String()), + }) + return + } + }() + + return &revisionIterator{ + ctx: ctx, + ch: resultChan, + } +} diff --git a/internal/git/gitpipe/ls_tree_test.go b/internal/git/gitpipe/ls_tree_test.go new file mode 100644 index 000000000..7d1133055 --- /dev/null +++ b/internal/git/gitpipe/ls_tree_test.go @@ -0,0 +1,151 @@ +package gitpipe + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/gittest" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/localrepo" + "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper" + "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper/testcfg" +) + +func TestLsTree(t *testing.T) { + cfg := testcfg.Build(t) + + repoProto, _ := gittest.CloneRepo(t, cfg, cfg.Storages[0]) + repo := localrepo.NewTestRepo(t, cfg, repoProto) + + for _, tc := range []struct { + desc string + revision string + options []LsTreeOption + expectedResults []RevisionResult + expectedErr error + }{ + { + desc: "initial commit", + revision: "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863", + expectedResults: []RevisionResult{ + {OID: "470ad2fcf1e33798f1afc5781d08e60c40f51e7a", ObjectName: []byte(".gitignore")}, + {OID: "50b27c6518be44c42c4d87966ae2481ce895624c", ObjectName: []byte("LICENSE")}, + {OID: "faaf198af3a36dbf41961466703cc1d47c61d051", ObjectName: []byte("README.md")}, + }, + }, + { + desc: "includes submodule", + revision: "cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + expectedResults: []RevisionResult{ + {OID: "470ad2fcf1e33798f1afc5781d08e60c40f51e7a", ObjectName: []byte(".gitignore")}, + {OID: "fdaada1754989978413d618ee1fb1c0469d6a664", ObjectName: []byte(".gitmodules")}, + {OID: "c1788657b95998a2f177a4f86d68a60f2a80117f", ObjectName: []byte("CONTRIBUTING.md")}, + {OID: "50b27c6518be44c42c4d87966ae2481ce895624c", ObjectName: []byte("LICENSE")}, + {OID: "faaf198af3a36dbf41961466703cc1d47c61d051", ObjectName: []byte("README.md")}, + {OID: "409f37c4f05865e4fb208c771485f211a22c4c2d", ObjectName: []byte("six")}, + }, + }, + { + desc: "filter blobs only", + revision: "cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + options: []LsTreeOption{ + LsTreeWithBlobFilter(), + }, + expectedResults: []RevisionResult{ + {OID: "470ad2fcf1e33798f1afc5781d08e60c40f51e7a", ObjectName: []byte(".gitignore")}, + {OID: "fdaada1754989978413d618ee1fb1c0469d6a664", ObjectName: []byte(".gitmodules")}, + {OID: "c1788657b95998a2f177a4f86d68a60f2a80117f", ObjectName: []byte("CONTRIBUTING.md")}, + {OID: "50b27c6518be44c42c4d87966ae2481ce895624c", ObjectName: []byte("LICENSE")}, + {OID: "faaf198af3a36dbf41961466703cc1d47c61d051", ObjectName: []byte("README.md")}, + }, + }, + { + desc: "empty tree", + revision: "7efb185dd22fd5c51ef044795d62b7847900c341", + }, + { + desc: "non-recursive", + revision: "913c66a37b4a45b9769037c55c2d238bd0942d2e", + expectedResults: []RevisionResult{ + {OID: "fd90a3d2d21d6b4f9bec2c33fb7f49780c55f0d2", ObjectName: []byte(".DS_Store")}, + {OID: "470ad2fcf1e33798f1afc5781d08e60c40f51e7a", ObjectName: []byte(".gitignore")}, + {OID: "fdaada1754989978413d618ee1fb1c0469d6a664", ObjectName: []byte(".gitmodules")}, + {OID: "c74175afd117781cbc983664339a0f599b5bb34e", ObjectName: []byte("CHANGELOG")}, + {OID: "c1788657b95998a2f177a4f86d68a60f2a80117f", ObjectName: []byte("CONTRIBUTING.md")}, + {OID: "50b27c6518be44c42c4d87966ae2481ce895624c", ObjectName: []byte("LICENSE")}, + {OID: "95d9f0a5e7bb054e9dd3975589b8dfc689e20e88", ObjectName: []byte("MAINTENANCE.md")}, + {OID: "bf757025c40c62e6ffa6f11d3819c769a76dbe09", ObjectName: []byte("PROCESS.md")}, + {OID: "faaf198af3a36dbf41961466703cc1d47c61d051", ObjectName: []byte("README.md")}, + {OID: "998707b421c89bd9a3063333f9f728ef3e43d101", ObjectName: []byte("VERSION")}, + {OID: "3c122d2b7830eca25235131070602575cf8b41a1", ObjectName: []byte("encoding")}, + {OID: "ab746f8ad0b84b147290041dc13cc9c7adc52930", ObjectName: []byte("files")}, + {OID: "409f37c4f05865e4fb208c771485f211a22c4c2d", ObjectName: []byte("six")}, + }, + }, + { + desc: "recursive", + revision: "913c66a37b4a45b9769037c55c2d238bd0942d2e", + options: []LsTreeOption{ + LsTreeWithRecursive(), + }, + expectedResults: []RevisionResult{ + {OID: "fd90a3d2d21d6b4f9bec2c33fb7f49780c55f0d2", ObjectName: []byte(".DS_Store")}, + {OID: "470ad2fcf1e33798f1afc5781d08e60c40f51e7a", ObjectName: []byte(".gitignore")}, + {OID: "fdaada1754989978413d618ee1fb1c0469d6a664", ObjectName: []byte(".gitmodules")}, + {OID: "c74175afd117781cbc983664339a0f599b5bb34e", ObjectName: []byte("CHANGELOG")}, + {OID: "c1788657b95998a2f177a4f86d68a60f2a80117f", ObjectName: []byte("CONTRIBUTING.md")}, + {OID: "50b27c6518be44c42c4d87966ae2481ce895624c", ObjectName: []byte("LICENSE")}, + {OID: "95d9f0a5e7bb054e9dd3975589b8dfc689e20e88", ObjectName: []byte("MAINTENANCE.md")}, + {OID: "bf757025c40c62e6ffa6f11d3819c769a76dbe09", ObjectName: []byte("PROCESS.md")}, + {OID: "faaf198af3a36dbf41961466703cc1d47c61d051", ObjectName: []byte("README.md")}, + {OID: "998707b421c89bd9a3063333f9f728ef3e43d101", ObjectName: []byte("VERSION")}, + {OID: "8af7f880ce38649fc49f66e3f38857bfbec3f0b7", ObjectName: []byte("encoding/feature-1.txt")}, + {OID: "16ca0b267f82cd2f5ca1157dd162dae98745eab8", ObjectName: []byte("encoding/feature-2.txt")}, + {OID: "0fb47f093f769008049a0b0976ac3fa6d6125033", ObjectName: []byte("encoding/hotfix-1.txt")}, + {OID: "4ae6c5e14452a35d04156277ae63e8356eb17cae", ObjectName: []byte("encoding/hotfix-2.txt")}, + {OID: "570f8e1dfe8149c1d17002712310d43dfeb43159", ObjectName: []byte("encoding/russian.rb")}, + {OID: "7a17968582c21c9153ec24c6a9d5f33592ad9103", ObjectName: []byte("encoding/test.txt")}, + {OID: "f3064a3aa9c14277483f690250072e987e2c8356", ObjectName: []byte("encoding/\343\203\206\343\202\271\343\203\210.txt")}, + {OID: "3a26c18b02e843b459732e7ade7ab9a154a1002b", ObjectName: []byte("encoding/\343\203\206\343\202\271\343\203\210.xls")}, + {OID: "60d7a906c2fd9e4509aeb1187b98d0ea7ce827c9", ObjectName: []byte("files/.DS_Store")}, + {OID: "c84b9e90e4bee2666f5cb3688690d9824d88f3ee", ObjectName: []byte("files/html/500.html")}, + {OID: "4a96572d570108366da2cf5aa8213f69b591a2a3", ObjectName: []byte("files/images/logo-black.png")}, + {OID: "bc2ef601a538d69ef99d5bdafa605e63f902e8e4", ObjectName: []byte("files/images/logo-white.png")}, + {OID: "4a393dbfe81113d35d9aedeb5c76f763190c1b2f", ObjectName: []byte("files/js/application.js")}, + {OID: "5f53439ca4b009096571d3c8bc3d09d30e7431b3", ObjectName: []byte("files/js/commit.js.coffee")}, + {OID: "3da5c4ddbaf17d37aea52c485d8bb85b07d77816", ObjectName: []byte("files/markdown/ruby-style-guide.md")}, + {OID: "e2fbafb389911ef68e4da2da7da917f6acb9e86a", ObjectName: []byte("files/ruby/popen.rb")}, + {OID: "d18fc8bf2cecbe21bbbd5da692eaf83e32c48638", ObjectName: []byte("files/ruby/regex.rb")}, + {OID: "6ee41e85cc9bf33c10b690df09ca735b22f3790f", ObjectName: []byte("files/ruby/version_info.rb")}, + {OID: "409f37c4f05865e4fb208c771485f211a22c4c2d", ObjectName: []byte("six")}, + }, + }, + { + desc: "invalid revision", + revision: "refs/heads/does-not-exist", + expectedErr: errors.New("ls-tree pipeline command: exit status 128, stderr: " + + "\"fatal: Not a valid object name refs/heads/does-not-exist\\n\""), + }, + } { + t.Run(tc.desc, func(t *testing.T) { + ctx := testhelper.Context(t) + + it := LsTree(ctx, repo, tc.revision, tc.options...) + + var results []RevisionResult + for it.Next() { + results = append(results, it.Result()) + } + + // We're converting the error here to a plain un-nested error such that we + // don't have to replicate the complete error's structure. + err := it.Err() + if err != nil { + err = errors.New(err.Error()) + } + + require.Equal(t, tc.expectedErr, err) + require.Equal(t, tc.expectedResults, results) + }) + } +} diff --git a/internal/git/hooks_options.go b/internal/git/hooks_options.go index 5a24f8a9d..9a77a2ce8 100644 --- a/internal/git/hooks_options.go +++ b/internal/git/hooks_options.go @@ -115,7 +115,7 @@ func (cc *cmdCfg) configureHooks( transaction, userDetails, requestedHooks, - featureflag.RawFromContext(ctx)).Env() + featureflag.FromContext(ctx)).Env() if err != nil { return err } diff --git a/internal/git/hooks_payload.go b/internal/git/hooks_payload.go index f0185d495..918164f79 100644 --- a/internal/git/hooks_payload.go +++ b/internal/git/hooks_payload.go @@ -47,14 +47,23 @@ const ( ReceivePackHooks = ReferenceTransactionHook | UpdateHook | PreReceiveHook | PostReceiveHook ) +// FeatureFlagWithValue is used as part of the HooksPayload to pass on feature flags with their +// values to gitaly-hooks. +type FeatureFlagWithValue struct { + // Flag is the feature flag. + Flag featureflag.FeatureFlag `json:"flag"` + // Enabled indicates whether the flag is enabled or not. + Enabled bool `json:"enabled"` +} + // HooksPayload holds parameters required for all hooks. type HooksPayload struct { // RequestedHooks is a bitfield of requested Hooks. Hooks which // were not requested will not get executed. RequestedHooks Hook `json:"requested_hooks"` - // FeatureFlags contains feature flags with their values. They are set - // into the outgoing context when calling HookService. - FeatureFlags featureflag.Raw `json:"feature_flags,omitempty"` + // FeatureFlagsWithValue contains feature flags with their values. They are set into the + // outgoing context when calling HookService. + FeatureFlagsWithValue []FeatureFlagWithValue `json:"feature_flags_with_value,omitempty"` // Repo is the repository in which the hook is running. Repo *gitalypb.Repository `json:"-"` @@ -110,17 +119,25 @@ func NewHooksPayload( tx *txinfo.Transaction, userDetails *UserDetails, requestedHooks Hook, - featureFlags featureflag.Raw, + featureFlagsWithValue map[featureflag.FeatureFlag]bool, ) HooksPayload { + flags := make([]FeatureFlagWithValue, 0, len(featureFlagsWithValue)) + for flag, enabled := range featureFlagsWithValue { + flags = append(flags, FeatureFlagWithValue{ + Flag: flag, + Enabled: enabled, + }) + } + return HooksPayload{ - Repo: repo, - RuntimeDir: cfg.RuntimeDir, - InternalSocket: cfg.InternalSocketPath(), - InternalSocketToken: cfg.Auth.Token, - Transaction: tx, - UserDetails: userDetails, - RequestedHooks: requestedHooks, - FeatureFlags: featureFlags, + Repo: repo, + RuntimeDir: cfg.RuntimeDir, + InternalSocket: cfg.InternalSocketPath(), + InternalSocketToken: cfg.Auth.Token, + Transaction: tx, + UserDetails: userDetails, + RequestedHooks: requestedHooks, + FeatureFlagsWithValue: flags, } } diff --git a/internal/git/hooks_payload_test.go b/internal/git/hooks_payload_test.go index b6220d656..361e68a7f 100644 --- a/internal/git/hooks_payload_test.go +++ b/internal/git/hooks_payload_test.go @@ -27,7 +27,9 @@ func TestHooksPayload(t *testing.T) { }) t.Run("roundtrip succeeds", func(t *testing.T) { - env, err := git.NewHooksPayload(cfg, repo, nil, nil, git.PreReceiveHook, featureflag.Raw{"flag-key": "flag-value"}).Env() + env, err := git.NewHooksPayload(cfg, repo, nil, nil, git.PreReceiveHook, map[featureflag.FeatureFlag]bool{ + {Name: "flag_key"}: true, + }).Env() require.NoError(t, err) payload, err := git.HooksPayloadFromEnv([]string{ @@ -43,7 +45,12 @@ func TestHooksPayload(t *testing.T) { RuntimeDir: cfg.RuntimeDir, InternalSocket: cfg.InternalSocketPath(), RequestedHooks: git.PreReceiveHook, - FeatureFlags: featureflag.Raw{"flag-key": "flag-value"}, + FeatureFlagsWithValue: []git.FeatureFlagWithValue{ + { + Flag: featureflag.FeatureFlag{Name: "flag_key"}, + Enabled: true, + }, + }, }, payload) }) diff --git a/internal/git/localrepo/repo.go b/internal/git/localrepo/repo.go index 6d6ac6da3..730176543 100644 --- a/internal/git/localrepo/repo.go +++ b/internal/git/localrepo/repo.go @@ -205,3 +205,18 @@ func (repo *Repo) Size(ctx context.Context, opts ...RepoSizeOption) (int64, erro return size, nil } + +// StorageTempDir returns the temporary dir for the storage where the repo is on. +// When this directory does not exist yet, it's being created. +func (repo *Repo) StorageTempDir() (string, error) { + tempPath, err := repo.locator.TempDir(repo.GetStorageName()) + if err != nil { + return "", err + } + + if err := os.MkdirAll(tempPath, 0o755); err != nil { + return "", err + } + + return tempPath, nil +} diff --git a/internal/git/localrepo/repo_test.go b/internal/git/localrepo/repo_test.go index bc986eddf..f90e5377e 100644 --- a/internal/git/localrepo/repo_test.go +++ b/internal/git/localrepo/repo_test.go @@ -342,3 +342,23 @@ func TestSize(t *testing.T) { }) } } + +func TestRepo_StorageTempDir(t *testing.T) { + cfg := testcfg.Build(t) + gitCmdFactory := gittest.NewCommandFactory(t, cfg) + catfileCache := catfile.NewCache(cfg) + t.Cleanup(catfileCache.Stop) + locator := config.NewLocator(cfg) + + pbRepo, _ := gittest.CloneRepo(t, cfg, cfg.Storages[0]) + repo := New(locator, gitCmdFactory, catfileCache, pbRepo) + + expected, err := locator.TempDir(cfg.Storages[0].Name) + require.NoError(t, err) + require.NoDirExists(t, expected) + + tempPath, err := repo.StorageTempDir() + require.NoError(t, err) + require.DirExists(t, expected) + require.Equal(t, expected, tempPath) +} diff --git a/internal/git/updateref/update_with_hooks.go b/internal/git/updateref/update_with_hooks.go index de4c7cb92..fbd615be4 100644 --- a/internal/git/updateref/update_with_hooks.go +++ b/internal/git/updateref/update_with_hooks.go @@ -188,7 +188,7 @@ func (u *UpdaterWithHooks) UpdateReference( quarantinedRepo = quarantineDir.QuarantinedRepo() } - hooksPayload, err := git.NewHooksPayload(u.cfg, quarantinedRepo, transaction, &receiveHooksPayload, git.ReceivePackHooks, featureflag.RawFromContext(ctx)).Env() + hooksPayload, err := git.NewHooksPayload(u.cfg, quarantinedRepo, transaction, &receiveHooksPayload, git.ReceivePackHooks, featureflag.FromContext(ctx)).Env() if err != nil { return err } @@ -210,7 +210,7 @@ func (u *UpdaterWithHooks) UpdateReference( // We only need to update the hooks payload to the unquarantined repo in case we // had a quarantine environment. Otherwise, the initial hooks payload is for the // real repository anyway. - hooksPayload, err = git.NewHooksPayload(u.cfg, repo, transaction, &receiveHooksPayload, git.ReceivePackHooks, featureflag.RawFromContext(ctx)).Env() + hooksPayload, err = git.NewHooksPayload(u.cfg, repo, transaction, &receiveHooksPayload, git.ReceivePackHooks, featureflag.FromContext(ctx)).Env() if err != nil { return err } diff --git a/internal/git/updateref/update_with_hooks_test.go b/internal/git/updateref/update_with_hooks_test.go index fa02c5069..cf2464a58 100644 --- a/internal/git/updateref/update_with_hooks_test.go +++ b/internal/git/updateref/update_with_hooks_test.go @@ -110,15 +110,24 @@ func TestUpdaterWithHooks_UpdateReference(t *testing.T) { oldRev := "1e292f8fedd741b75372e19097c76d327140c312" - payload, err := git.NewHooksPayload(cfg, repo, nil, &git.UserDetails{ - UserID: "1234", - Username: "Username", - Protocol: "web", - }, git.ReceivePackHooks, featureflag.RawFromContext(ctx)).Env() - require.NoError(t, err) + requirePayload := func(t *testing.T, env []string) { + require.Len(t, env, 1) + + expectedPayload := git.NewHooksPayload(cfg, repo, nil, &git.UserDetails{ + UserID: "1234", + Username: "Username", + Protocol: "web", + }, git.ReceivePackHooks, featureflag.FromContext(ctx)) + + actualPayload, err := git.HooksPayloadFromEnv(env) + require.NoError(t, err) + + // Flags aren't sorted, so we just verify they contain the same elements. + require.ElementsMatch(t, expectedPayload.FeatureFlagsWithValue, actualPayload.FeatureFlagsWithValue) + expectedPayload.FeatureFlagsWithValue = nil + actualPayload.FeatureFlagsWithValue = nil - expectedEnv := []string{ - payload, + require.Equal(t, expectedPayload, actualPayload) } referenceTransactionCalls := 0 @@ -138,21 +147,21 @@ func TestUpdaterWithHooks_UpdateReference(t *testing.T) { require.NoError(t, err) require.Equal(t, fmt.Sprintf("%s %s refs/heads/master\n", oldRev, git.ZeroOID.String()), string(changes)) require.Empty(t, pushOptions) - require.Equal(t, env, expectedEnv) + requirePayload(t, env) return nil }, update: func(t *testing.T, ctx context.Context, repo *gitalypb.Repository, ref, oldValue, newValue string, env []string, stdout, stderr io.Writer) error { require.Equal(t, "refs/heads/master", ref) require.Equal(t, oldRev, oldValue) require.Equal(t, newValue, git.ZeroOID.String()) - require.Equal(t, env, expectedEnv) + requirePayload(t, env) return nil }, postReceive: func(t *testing.T, ctx context.Context, repo *gitalypb.Repository, pushOptions, env []string, stdin io.Reader, stdout, stderr io.Writer) error { changes, err := io.ReadAll(stdin) require.NoError(t, err) require.Equal(t, fmt.Sprintf("%s %s refs/heads/master\n", oldRev, git.ZeroOID.String()), string(changes)) - require.Equal(t, env, expectedEnv) + requirePayload(t, env) require.Empty(t, pushOptions) return nil }, @@ -169,7 +178,7 @@ func TestUpdaterWithHooks_UpdateReference(t *testing.T) { } referenceTransactionCalls++ - require.Equal(t, env, expectedEnv) + requirePayload(t, env) return nil }, expectedRefDeletion: true, diff --git a/internal/git/updateref/updateref_test.go b/internal/git/updateref/updateref_test.go index 692493d27..743e215a8 100644 --- a/internal/git/updateref/updateref_test.go +++ b/internal/git/updateref/updateref_test.go @@ -122,9 +122,14 @@ func TestUpdater_prepareLocksTransaction(t *testing.T) { func TestUpdater_concurrentLocking(t *testing.T) { t.Parallel() - ctx := testhelper.Context(t) cfg, protoRepo, _ := testcfg.BuildWithRepo(t) + ctx := testhelper.Context(t) + + if !gitSupportsStatusFlushing(t, ctx, cfg) { + t.Skip("git does not support flushing yet, which is known to be flaky") + } + repo := localrepo.NewTestRepo(t, cfg, protoRepo, git.WithSkipHooks()) commit, logErr := repo.ReadCommit(ctx, "refs/heads/master") @@ -139,22 +144,11 @@ func TestUpdater_concurrentLocking(t *testing.T) { require.NoError(t, err) require.NoError(t, secondUpdater.Update("refs/heads/master", "", commit.Id)) - // With flushing, we're able to detect concurrent locking at prepare time already instead of - // at commit time. - if gitSupportsStatusFlushing(t, ctx, cfg) { - err := secondUpdater.Prepare() - require.Error(t, err) - require.Contains(t, err.Error(), "fatal: prepare: cannot lock ref 'refs/heads/master'") - - require.NoError(t, firstUpdater.Commit()) - } else { - require.NoError(t, secondUpdater.Prepare()) - require.NoError(t, firstUpdater.Commit()) + err = secondUpdater.Prepare() + require.Error(t, err) + require.Contains(t, err.Error(), "fatal: prepare: cannot lock ref 'refs/heads/master'") - err := secondUpdater.Commit() - require.Error(t, err) - require.Contains(t, err.Error(), "fatal: prepare: cannot lock ref 'refs/heads/master'") - } + require.NoError(t, firstUpdater.Commit()) } func TestBulkOperation(t *testing.T) { diff --git a/internal/git2go/executor.go b/internal/git2go/executor.go index c1e220f43..5007b6c6f 100644 --- a/internal/git2go/executor.go +++ b/internal/git2go/executor.go @@ -59,14 +59,12 @@ func (b *Executor) run(ctx context.Context, repo repository.GitRepo, stdin io.Re var enabledFeatureFlags, disabledFeatureFlags []string - for ff, value := range featureflag.RawFromContext(ctx) { + for flag, value := range featureflag.FromContext(ctx) { switch value { - case "true": - enabledFeatureFlags = append(enabledFeatureFlags, ff) - case "false": - disabledFeatureFlags = append(disabledFeatureFlags, ff) - default: - return nil, fmt.Errorf("invalid value for feature flag %q: %q", ff, value) + case true: + enabledFeatureFlags = append(enabledFeatureFlags, flag.MetadataKey()) + case false: + disabledFeatureFlags = append(disabledFeatureFlags, flag.MetadataKey()) } } diff --git a/internal/git2go/featureflags.go b/internal/git2go/featureflags.go index cabc60e6f..bfde6867b 100644 --- a/internal/git2go/featureflags.go +++ b/internal/git2go/featureflags.go @@ -1,11 +1,21 @@ package git2go +// FeatureFlag is a feature flag state as seen by the `gitaly-git2go featureflag` test subcommand. +type FeatureFlag struct { + // MetadataKey is the metadata key of the feature flag. + MetadataKey string + // Name is the name of the feature flag. + Name string + // Value is the value of the feature flag. + Value bool +} + // FeatureFlags is a struct only used by tests to confirm that feature flags are // being properly propagated from the git2go Executor to the gitaly-git2go // binary type FeatureFlags struct { - // Raw is a map of feature flags and their corresponding values - Raw map[string]string + // Flags is the set of feature flags observed by the command. + Flags []FeatureFlag // Err is set if an error occurred. Err must exist on all gob serialized // results so that any error can be returned. Err error diff --git a/internal/git2go/featureflags_test.go b/internal/git2go/featureflags_test.go index 06741fb05..61ebd6d31 100644 --- a/internal/git2go/featureflags_test.go +++ b/internal/git2go/featureflags_test.go @@ -3,7 +3,6 @@ package git2go import ( "context" "encoding/gob" - "strconv" "testing" "github.com/stretchr/testify/require" @@ -16,7 +15,7 @@ import ( "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper/testcfg" ) -func (b *Executor) FeatureFlags(ctx context.Context, repo repository.GitRepo) (featureflag.Raw, error) { +func (b *Executor) FeatureFlags(ctx context.Context, repo repository.GitRepo) ([]FeatureFlag, error) { output, err := b.run(ctx, repo, nil, "feature-flags") if err != nil { return nil, err @@ -31,17 +30,16 @@ func (b *Executor) FeatureFlags(ctx context.Context, repo repository.GitRepo) (f return nil, result.Err } - return result.Raw, err + return result.Flags, err } var ( - featureA = featureflag.NewFeatureFlag("feature-a", "", "", false) - featureB = featureflag.NewFeatureFlag("feature-b", "", "", true) + featureA = featureflag.NewFeatureFlag("feature_a", "", "", false) + featureB = featureflag.NewFeatureFlag("feature_b", "", "", true) ) -func TestFeatureFlagsExecutor_FeatureFlags(t *testing.T) { - testhelper.NewFeatureSets(featureA, featureB). - Run(t, testExecutorFeatureFlags) +func TestExecutor_explicitFeatureFlags(t *testing.T) { + testhelper.NewFeatureSets(featureA, featureB).Run(t, testExecutorFeatureFlags) } func testExecutorFeatureFlags(t *testing.T, ctx context.Context) { @@ -54,9 +52,19 @@ func testExecutorFeatureFlags(t *testing.T, ctx context.Context) { executor := NewExecutor(cfg, gittest.NewCommandFactory(t, cfg), config.NewLocator(cfg)) - raw, err := executor.FeatureFlags(ctx, repo) + flags, err := executor.FeatureFlags(ctx, repo) require.NoError(t, err) - require.Equal(t, strconv.FormatBool(featureA.IsEnabled(ctx)), raw["gitaly-feature-feature-a"]) - require.Equal(t, strconv.FormatBool(featureB.IsEnabled(ctx)), raw["gitaly-feature-feature-b"]) + require.Subset(t, flags, []FeatureFlag{ + { + Name: "feature_a", + MetadataKey: "gitaly-feature-feature-a", + Value: featureA.IsEnabled(ctx), + }, + { + Name: "feature_b", + MetadataKey: "gitaly-feature-feature-b", + Value: featureB.IsEnabled(ctx), + }, + }, flags) } diff --git a/internal/gitaly/config/cgroups/cgroups.go b/internal/gitaly/config/cgroups/cgroups.go index 935a1a565..8d9094552 100644 --- a/internal/gitaly/config/cgroups/cgroups.go +++ b/internal/gitaly/config/cgroups/cgroups.go @@ -14,7 +14,8 @@ type Config struct { MemoryBytes int64 `toml:"memory_bytes"` // CPUShares are the shares of CPU the parent cgroup is allowed to utilize. A value of 1024 // is full utilization of the CPU. 0 implies no CPU limit. - CPUShares uint64 `toml:"cpu_shares"` + CPUShares uint64 `toml:"cpu_shares"` + MetricsEnabled bool `toml:"metrics_enabled"` // Deprecated: No longer supported after 15.0 Count uint `toml:"count"` diff --git a/internal/gitaly/config/config_test.go b/internal/gitaly/config/config_test.go index 2967fd398..9c1961e99 100644 --- a/internal/gitaly/config/config_test.go +++ b/internal/gitaly/config/config_test.go @@ -1082,6 +1082,28 @@ func TestValidateCgroups(t *testing.T) { }, validateErr: errors.New("cgroups.repositories: cpu shares cannot exceed parent"), }, + { + name: "metrics enabled", + rawCfg: `[cgroups] + mountpoint = "/sys/fs/cgroup" + hierarchy_root = "gitaly" + metrics_enabled = true + [cgroups.repositories] + count = 10 + memory_bytes = 1024 + cpu_shares = 512 + `, + expect: cgroups.Config{ + Mountpoint: "/sys/fs/cgroup", + HierarchyRoot: "gitaly", + MetricsEnabled: true, + Repositories: cgroups.Repositories{ + Count: 10, + MemoryBytes: 1024, + CPUShares: 512, + }, + }, + }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/gitaly/hook/postreceive_test.go b/internal/gitaly/hook/postreceive_test.go index d8afe3ff5..f78265d9f 100644 --- a/internal/gitaly/hook/postreceive_test.go +++ b/internal/gitaly/hook/postreceive_test.go @@ -83,7 +83,7 @@ func TestPostReceive_customHook(t *testing.T) { ctx := testhelper.Context(t) - payload, err := git.NewHooksPayload(cfg, repo, nil, receiveHooksPayload, git.PostReceiveHook, featureflag.RawFromContext(ctx)).Env() + payload, err := git.NewHooksPayload(cfg, repo, nil, receiveHooksPayload, git.PostReceiveHook, featureflag.FromContext(ctx)).Env() require.NoError(t, err) primaryPayload, err := git.NewHooksPayload( @@ -94,7 +94,7 @@ func TestPostReceive_customHook(t *testing.T) { }, receiveHooksPayload, git.PostReceiveHook, - featureflag.RawFromContext(ctx), + featureflag.FromContext(ctx), ).Env() require.NoError(t, err) @@ -106,7 +106,7 @@ func TestPostReceive_customHook(t *testing.T) { }, receiveHooksPayload, git.PostReceiveHook, - featureflag.RawFromContext(ctx), + featureflag.FromContext(ctx), ).Env() require.NoError(t, err) @@ -387,7 +387,7 @@ func TestPostReceive_quarantine(t *testing.T) { Protocol: "web", }, git.PreReceiveHook, - featureflag.RawFromContext(ctx), + featureflag.FromContext(ctx), ).Env() require.NoError(t, err) diff --git a/internal/gitaly/hook/prereceive_test.go b/internal/gitaly/hook/prereceive_test.go index 12dc504bd..7dfc05cee 100644 --- a/internal/gitaly/hook/prereceive_test.go +++ b/internal/gitaly/hook/prereceive_test.go @@ -42,7 +42,7 @@ func TestPrereceive_customHooks(t *testing.T) { ctx := testhelper.Context(t) - payload, err := git.NewHooksPayload(cfg, repo, nil, receiveHooksPayload, git.PreReceiveHook, featureflag.RawFromContext(ctx)).Env() + payload, err := git.NewHooksPayload(cfg, repo, nil, receiveHooksPayload, git.PreReceiveHook, featureflag.FromContext(ctx)).Env() require.NoError(t, err) primaryPayload, err := git.NewHooksPayload( @@ -53,7 +53,7 @@ func TestPrereceive_customHooks(t *testing.T) { }, receiveHooksPayload, git.PreReceiveHook, - featureflag.RawFromContext(ctx), + featureflag.FromContext(ctx), ).Env() require.NoError(t, err) @@ -65,7 +65,7 @@ func TestPrereceive_customHooks(t *testing.T) { }, receiveHooksPayload, git.PreReceiveHook, - featureflag.RawFromContext(ctx), + featureflag.FromContext(ctx), ).Env() require.NoError(t, err) @@ -206,7 +206,7 @@ func TestPrereceive_quarantine(t *testing.T) { Protocol: "web", }, git.PreReceiveHook, - featureflag.RawFromContext(ctx), + featureflag.FromContext(ctx), ).Env() require.NoError(t, err) diff --git a/internal/gitaly/hook/transactions_test.go b/internal/gitaly/hook/transactions_test.go index 5c485983f..727485fd9 100644 --- a/internal/gitaly/hook/transactions_test.go +++ b/internal/gitaly/hook/transactions_test.go @@ -45,7 +45,7 @@ func TestHookManager_stopCalled(t *testing.T) { Protocol: "web", }, git.ReferenceTransactionHook, - featureflag.RawFromContext(ctx), + featureflag.FromContext(ctx), ).Env() require.NoError(t, err) diff --git a/internal/gitaly/hook/update_test.go b/internal/gitaly/hook/update_test.go index 464340861..326f2dca1 100644 --- a/internal/gitaly/hook/update_test.go +++ b/internal/gitaly/hook/update_test.go @@ -39,7 +39,7 @@ func TestUpdate_customHooks(t *testing.T) { ctx := testhelper.Context(t) - payload, err := git.NewHooksPayload(cfg, repo, nil, receiveHooksPayload, git.UpdateHook, featureflag.RawFromContext(ctx)).Env() + payload, err := git.NewHooksPayload(cfg, repo, nil, receiveHooksPayload, git.UpdateHook, featureflag.FromContext(ctx)).Env() require.NoError(t, err) primaryPayload, err := git.NewHooksPayload( @@ -50,7 +50,7 @@ func TestUpdate_customHooks(t *testing.T) { }, receiveHooksPayload, git.UpdateHook, - featureflag.RawFromContext(ctx), + featureflag.FromContext(ctx), ).Env() require.NoError(t, err) @@ -62,7 +62,7 @@ func TestUpdate_customHooks(t *testing.T) { }, receiveHooksPayload, git.UpdateHook, - featureflag.RawFromContext(ctx), + featureflag.FromContext(ctx), ).Env() require.NoError(t, err) @@ -237,7 +237,7 @@ func TestUpdate_quarantine(t *testing.T) { Protocol: "web", }, git.PreReceiveHook, - featureflag.RawFromContext(ctx), + featureflag.FromContext(ctx), ).Env() require.NoError(t, err) diff --git a/internal/gitaly/linguist/language_stats.go b/internal/gitaly/linguist/language_stats.go new file mode 100644 index 000000000..9dad17c11 --- /dev/null +++ b/internal/gitaly/linguist/language_stats.go @@ -0,0 +1,157 @@ +package linguist + +import ( + "compress/zlib" + "encoding/json" + "fmt" + "os" + "path/filepath" + "sync" + + "gitlab.com/gitlab-org/gitaly/v15/internal/git/localrepo" +) + +const ( + // languageStatsFilename is the name of the file in the repo that stores + // a cached version of the language statistics. The name is + // intentionally different from what the linguist gem uses. + languageStatsFilename = "gitaly-language.stats" + languageStatsVersion = "v1:gitaly" +) + +// languageStats takes care of accumulating and caching language statistics for +// a repository. +type languageStats struct { + // Version holds the file format version + Version string `json:"version"` + // CommitID holds the commit ID for the cached Totals + CommitID string `json:"commit_id"` + + // m will protect concurrent writes to Totals & ByFile maps + m sync.Mutex + + // Totals contains the total statistics for the CommitID + Totals ByteCountPerLanguage `json:"totals"` + // ByFile contains the statistics for a single file, where the filename + // is it's key. + ByFile map[string]ByteCountPerLanguage `json:"by_file"` +} + +// newLanguageStats creates a languageStats object and tries to load the +// optionally available stats from file. +func newLanguageStats(repo *localrepo.Repo) (*languageStats, error) { + stats := languageStats{ + Totals: ByteCountPerLanguage{}, + ByFile: make(map[string]ByteCountPerLanguage), + } + + objPath, err := repo.Path() + if err != nil { + return &stats, fmt.Errorf("new language stats get repo path: %w", err) + } + + file, err := os.Open(filepath.Join(objPath, languageStatsFilename)) + if err != nil { + if os.IsNotExist(err) { + return &stats, nil + } + return &stats, fmt.Errorf("new language stats open: %w", err) + } + defer file.Close() + + r, err := zlib.NewReader(file) + if err != nil { + return &stats, fmt.Errorf("new language stats zlib reader: %w", err) + } + + var loaded languageStats + if err = json.NewDecoder(r).Decode(&loaded); err != nil { + return &stats, fmt.Errorf("new language stats json decode: %w", err) + } + + if loaded.Version != languageStatsVersion { + return &stats, fmt.Errorf("new language stats version mismatch %s vs %s", languageStatsVersion, loaded.Version) + } + + return &loaded, nil +} + +// add the statistics for the given filename +func (c *languageStats) add(filename, language string, size uint64) { + c.m.Lock() + defer c.m.Unlock() + + for k, v := range c.ByFile[filename] { + c.Totals[k] -= v + if c.Totals[k] <= 0 { + delete(c.Totals, k) + } + } + + c.ByFile[filename] = ByteCountPerLanguage{language: size} + c.Totals[language] += size +} + +// drop statistics for the given files +func (c *languageStats) drop(filenames ...string) { + c.m.Lock() + defer c.m.Unlock() + + for _, f := range filenames { + for k, v := range c.ByFile[f] { + c.Totals[k] -= v + if c.Totals[k] <= 0 { + delete(c.Totals, k) + } + } + delete(c.ByFile, f) + } +} + +// save the language stats to file in the repository +func (c *languageStats) save(repo *localrepo.Repo, commitID string) error { + c.CommitID = commitID + c.Version = languageStatsVersion + + repoPath, err := repo.Path() + if err != nil { + return fmt.Errorf("languageStats save get repo path: %w", err) + } + + tempPath, err := repo.StorageTempDir() + if err != nil { + return fmt.Errorf("languageStats locate temp dir: %w", err) + } + + file, err := os.CreateTemp(tempPath, languageStatsFilename) + if err != nil { + return fmt.Errorf("languageStats create temp file: %w", err) + } + defer func() { + file.Close() + _ = os.Remove(file.Name()) + }() + + w := zlib.NewWriter(file) + defer w.Close() + + if err = json.NewEncoder(w).Encode(c); err != nil { + return fmt.Errorf("languageStats encode json: %w", err) + } + + if err = w.Close(); err != nil { + return fmt.Errorf("languageStats zlib write: %w", err) + } + if err = file.Sync(); err != nil { + return fmt.Errorf("languageStats flush: %w", err) + } + if err = file.Close(); err != nil { + return fmt.Errorf("languageStats close: %w", err) + } + + if err = os.Rename(file.Name(), filepath.Join(repoPath, languageStatsFilename)); err != nil { + return fmt.Errorf("languageStats rename: %w", err) + } + + return nil +} diff --git a/internal/gitaly/linguist/language_stats_test.go b/internal/gitaly/linguist/language_stats_test.go new file mode 100644 index 000000000..56f03bce7 --- /dev/null +++ b/internal/gitaly/linguist/language_stats_test.go @@ -0,0 +1,195 @@ +package linguist + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/gittest" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/localrepo" + "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper/testcfg" +) + +func TestNewLanguageStats(t *testing.T) { + t.Parallel() + + cfg := testcfg.Build(t) + repoProto, repoPath := gittest.CloneRepo(t, cfg, cfg.Storages[0]) + repo := localrepo.NewTestRepo(t, cfg, repoProto) + + t.Run("non-existing cache", func(t *testing.T) { + s, err := newLanguageStats(repo) + require.NoError(t, err) + require.Empty(t, s.Totals) + require.Empty(t, s.ByFile) + }) + + t.Run("pre-existing cache", func(t *testing.T) { + s, err := newLanguageStats(repo) + require.NoError(t, err) + + s.Totals["C"] = 555 + require.NoError(t, s.save(repo, "badcafe")) + + require.Equal(t, ByteCountPerLanguage{"C": 555}, s.Totals) + }) + + t.Run("corrupt cache", func(t *testing.T) { + require.NoError(t, os.WriteFile(filepath.Join(repoPath, languageStatsFilename), []byte("garbage"), 0o644)) + + s, err := newLanguageStats(repo) + require.Errorf(t, err, "new language stats zlib reader: invalid header") + require.Empty(t, s.Totals) + require.Empty(t, s.ByFile) + }) +} + +func TestLanguageStats_add(t *testing.T) { + t.Parallel() + + cfg := testcfg.Build(t) + repoProto, _ := gittest.CloneRepo(t, cfg, cfg.Storages[0]) + repo := localrepo.NewTestRepo(t, cfg, repoProto) + + for _, tc := range []struct { + desc string + run func(*testing.T, *languageStats) + }{ + { + desc: "adds to the total", + run: func(t *testing.T, s *languageStats) { + s.add("main.go", "Go", 100) + + require.Equal(t, uint64(100), s.Totals["Go"]) + require.Len(t, s.ByFile, 1) + require.Equal(t, ByteCountPerLanguage{"Go": 100}, s.ByFile["main.go"]) + }, + }, + { + desc: "accumulates", + run: func(t *testing.T, s *languageStats) { + s.add("main.go", "Go", 100) + s.add("main_test.go", "Go", 80) + + require.Equal(t, uint64(180), s.Totals["Go"]) + require.Len(t, s.ByFile, 2) + require.Equal(t, ByteCountPerLanguage{"Go": 100}, s.ByFile["main.go"]) + require.Equal(t, ByteCountPerLanguage{"Go": 80}, s.ByFile["main_test.go"]) + }, + }, + { + desc: "languages don't interfere", + run: func(t *testing.T, s *languageStats) { + s.add("main.go", "Go", 60) + s.add("Makefile", "Make", 30) + + require.Equal(t, uint64(60), s.Totals["Go"]) + require.Equal(t, uint64(30), s.Totals["Make"]) + require.Len(t, s.ByFile, 2) + require.Equal(t, ByteCountPerLanguage{"Go": 60}, s.ByFile["main.go"]) + require.Equal(t, ByteCountPerLanguage{"Make": 30}, s.ByFile["Makefile"]) + }, + }, + { + desc: "updates the stat for a file", + run: func(t *testing.T, s *languageStats) { + s.add("main.go", "Go", 60) + s.add("main.go", "Go", 30) + + require.Equal(t, uint64(30), s.Totals["Go"]) + require.Len(t, s.ByFile, 1) + require.Equal(t, ByteCountPerLanguage{"Go": 30}, s.ByFile["main.go"]) + }, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + s, err := newLanguageStats(repo) + require.NoError(t, err) + + tc.run(t, s) + }) + } +} + +func TestLanguageStats_drop(t *testing.T) { + t.Parallel() + + cfg := testcfg.Build(t) + repoProto, _ := gittest.CloneRepo(t, cfg, cfg.Storages[0]) + repo := localrepo.NewTestRepo(t, cfg, repoProto) + + for _, tc := range []struct { + desc string + run func(*testing.T, *languageStats) + }{ + { + desc: "existing file", + run: func(t *testing.T, s *languageStats) { + s.drop("main.go") + + require.Equal(t, uint64(20), s.Totals["Go"]) + require.Len(t, s.ByFile, 1) + require.Equal(t, ByteCountPerLanguage{"Go": 20}, s.ByFile["main_test.go"]) + }, + }, + { + desc: "non-existing file", + run: func(t *testing.T, s *languageStats) { + s.drop("foo.go") + + require.Equal(t, uint64(100), s.Totals["Go"]) + require.Len(t, s.ByFile, 2) + require.Equal(t, ByteCountPerLanguage{"Go": 80}, s.ByFile["main.go"]) + require.Equal(t, ByteCountPerLanguage{"Go": 20}, s.ByFile["main_test.go"]) + }, + }, + { + desc: "all files", + run: func(t *testing.T, s *languageStats) { + s.drop("main.go", "main_test.go") + + require.Empty(t, s.Totals) + require.Empty(t, s.ByFile) + }, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + s, err := newLanguageStats(repo) + require.NoError(t, err) + + s.Totals["Go"] = 100 + s.ByFile["main.go"] = ByteCountPerLanguage{"Go": 80} + s.ByFile["main_test.go"] = ByteCountPerLanguage{"Go": 20} + + tc.run(t, s) + }) + } +} + +func TestLanguageStats_save(t *testing.T) { + t.Parallel() + + cfg := testcfg.Build(t) + repoProto, repoPath := gittest.CloneRepo(t, cfg, cfg.Storages[0]) + repo := localrepo.NewTestRepo(t, cfg, repoProto) + + s, err := newLanguageStats(repo) + require.NoError(t, err) + + s.Totals["Go"] = 100 + s.ByFile["main.go"] = ByteCountPerLanguage{"Go": 80} + s.ByFile["main_test.go"] = ByteCountPerLanguage{"Go": 20} + + err = s.save(repo, "buzz") + require.NoError(t, err) + require.FileExists(t, filepath.Join(repoPath, languageStatsFilename)) + + loaded, err := newLanguageStats(repo) + require.NoError(t, err) + + require.Equal(t, "buzz", loaded.CommitID) + require.Equal(t, languageStatsVersion, loaded.Version) + require.Equal(t, s.Totals, loaded.Totals) + require.Equal(t, s.ByFile, loaded.ByFile) +} diff --git a/internal/gitaly/linguist/linguist.go b/internal/gitaly/linguist/linguist.go index da085b58a..2a1a92c60 100644 --- a/internal/gitaly/linguist/linguist.go +++ b/internal/gitaly/linguist/linguist.go @@ -10,10 +10,16 @@ import ( "os/exec" "path/filepath" + "github.com/go-enry/go-enry/v2" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus" "gitlab.com/gitlab-org/gitaly/v15/internal/command" "gitlab.com/gitlab-org/gitaly/v15/internal/git" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/catfile" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/gitpipe" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/localrepo" "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/config" "gitlab.com/gitlab-org/gitaly/v15/internal/helper/env" + "gitlab.com/gitlab-org/gitaly/v15/internal/metadata/featureflag" ) // Language is used to parse Linguist's language.json file. @@ -53,7 +59,16 @@ func New(cfg config.Cfg, gitCmdFactory git.CommandFactory) (*Instance, error) { } // Stats returns the repository's language stats as reported by 'git-linguist'. -func (inst *Instance) Stats(ctx context.Context, repoPath string, commitID string) (ByteCountPerLanguage, error) { +func (inst *Instance) Stats(ctx context.Context, repo *localrepo.Repo, commitID string, catfileCache catfile.Cache) (ByteCountPerLanguage, error) { + if featureflag.GoLanguageStats.IsEnabled(ctx) { + return inst.enryStats(ctx, repo, commitID, catfileCache) + } + + repoPath, err := repo.Path() + if err != nil { + return nil, fmt.Errorf("get repo path: %w", err) + } + cmd, err := inst.startGitLinguist(ctx, repoPath, commitID) if err != nil { return nil, fmt.Errorf("starting linguist: %w", err) @@ -145,3 +160,98 @@ func openLanguagesJSON(cfg config.Cfg) (io.ReadCloser, error) { return os.Open(filepath.Join(linguistPathSymlink.Name(), "lib", "linguist", "languages.json")) } + +func (inst *Instance) enryStats(ctx context.Context, repo *localrepo.Repo, commitID string, catfileCache catfile.Cache) (ByteCountPerLanguage, error) { + stats, err := newLanguageStats(repo) + if err != nil { + ctxlogrus.Extract(ctx).WithError(err).Info("linguist load from cache") + } + if stats.CommitID == commitID { + return stats.Totals, nil + } + + objectReader, cancel, err := catfileCache.ObjectReader(ctx, repo) + if err != nil { + return nil, fmt.Errorf("create object reader: %w", err) + } + defer cancel() + + var revlistIt gitpipe.RevisionIterator + + if stats.CommitID == "" { + // No existing stats cached, so get all the files for the commit + // using git-ls-tree(1). + revlistIt = gitpipe.LsTree(ctx, repo, + commitID, + gitpipe.LsTreeWithRecursive(), + gitpipe.LsTreeWithBlobFilter(), + ) + } else { + // Stats are cached for one commit, so get the git-diff-tree(1) + // between that commit and the one we're calculating stats for. + + skipDeleted := func(result *gitpipe.RevisionResult) bool { + // Skip files that are deleted. + if result.OID.IsZeroOID() { + // It's a little bit of a hack to use this skip + // function, but for every file that's deleted, + // remove the stats. + stats.drop(string(result.ObjectName)) + return true + } + return false + } + + revlistIt = gitpipe.DiffTree(ctx, repo, + stats.CommitID, commitID, + gitpipe.DiffTreeWithRecursive(), + gitpipe.DiffTreeWithIgnoreSubmodules(), + gitpipe.DiffTreeWithSkip(skipDeleted), + ) + } + + objectIt, err := gitpipe.CatfileObject(ctx, objectReader, revlistIt) + if err != nil { + return nil, fmt.Errorf("linguist gitpipe: %w", err) + } + + for objectIt.Next() { + object := objectIt.Result() + filename := string(object.ObjectName) + + // Read arbitrary number of bytes considered enough to determine language + content, err := io.ReadAll(io.LimitReader(object, 2048)) + if err != nil { + return nil, fmt.Errorf("linguist read blob: %w", err) + } + + if _, err := io.Copy(io.Discard, object); err != nil { + return nil, fmt.Errorf("linguist discard excess blob: %w", err) + } + + lang := enry.GetLanguage(filename, content) + + // Ignore anything that's neither markup nor a programming language, + // similar to what the linguist gem does: + // https://github.com/github/linguist/blob/v7.20.0/lib/linguist/blob_helper.rb#L378-L387 + if enry.GetLanguageType(lang) != enry.Programming && + enry.GetLanguageType(lang) != enry.Markup { + // The file might have been included in the stats before + stats.drop(filename) + + continue + } + + stats.add(filename, lang, uint64(object.Object.ObjectSize())) + } + + if err := objectIt.Err(); err != nil { + return nil, fmt.Errorf("linguist object iterator: %w", err) + } + + if err := stats.save(repo, commitID); err != nil { + return nil, fmt.Errorf("linguist language stats save: %w", err) + } + + return stats.Totals, nil +} diff --git a/internal/gitaly/linguist/linguist_test.go b/internal/gitaly/linguist/linguist_test.go index 24cacf81e..b42168b41 100644 --- a/internal/gitaly/linguist/linguist_test.go +++ b/internal/gitaly/linguist/linguist_test.go @@ -1,43 +1,113 @@ package linguist import ( + "context" "encoding/json" "os" "path/filepath" "testing" + "github.com/go-enry/go-enry/v2" + enrydata "github.com/go-enry/go-enry/v2/data" + "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/test" "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitaly/v15/internal/git" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/catfile" "gitlab.com/gitlab-org/gitaly/v15/internal/git/gittest" + "gitlab.com/gitlab-org/gitaly/v15/internal/git/localrepo" "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/config" + "gitlab.com/gitlab-org/gitaly/v15/internal/metadata/featureflag" "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper" "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper/testcfg" + "gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb" ) func TestMain(m *testing.M) { testhelper.Run(m) } +// TestNew_knownLanguages tests the compatibility between the Ruby and the Go +// implementation. This test will be removed together with the Ruby implementation. +func TestNew_knownLanguages(t *testing.T) { + t.Parallel() + + cfg := testcfg.Build(t, testcfg.WithRealLinguist()) + gitCmdFactory := gittest.NewCommandFactory(t, cfg) + + linguist, err := New(cfg, gitCmdFactory) + require.NoError(t, err) + + t.Run("by name", func(t *testing.T) { + linguistLanguages := make([]string, 0, len(linguist.colorMap)) + for language := range linguist.colorMap { + linguistLanguages = append(linguistLanguages, language) + } + + enryLanguages := make([]string, 0, len(enrydata.IDByLanguage)) + for language := range enrydata.IDByLanguage { + enryLanguages = append(enryLanguages, language) + } + + require.ElementsMatch(t, linguistLanguages, enryLanguages) + }) + + t.Run("with their color", func(t *testing.T) { + exclude := map[string]struct{}{} + + linguistLanguages := make(map[string]string, len(linguist.colorMap)) + for language, color := range linguist.colorMap { + if color.Color == "" { + exclude[language] = struct{}{} + continue + } + linguistLanguages[language] = color.Color + } + + enryLanguages := make(map[string]string, len(enrydata.IDByLanguage)) + for language := range enrydata.IDByLanguage { + if _, excluded := exclude[language]; excluded { + continue + } + + color := enry.GetColor(language) + enryLanguages[language] = color + } + + require.Equal(t, linguistLanguages, enryLanguages) + }) +} + func TestInstance_Stats(t *testing.T) { - ctx := testhelper.Context(t) + testhelper.NewFeatureSets(featureflag.GoLanguageStats). + Run(t, testInstanceStats) +} + +func testInstanceStats(t *testing.T, ctx context.Context) { cfg := testcfg.Build(t) + gitCmdFactory := gittest.NewCommandFactory(t, cfg) - linguist, err := New(cfg, gittest.NewCommandFactory(t, cfg)) + linguist, err := New(cfg, gitCmdFactory) require.NoError(t, err) + catfileCache := catfile.NewCache(cfg) + t.Cleanup(catfileCache.Stop) + commitID := git.ObjectID("1e292f8fedd741b75372e19097c76d327140c312") + languageStatsFilename := filenameForCache(ctx) + for _, tc := range []struct { desc string - setup func(t *testing.T) (string, git.ObjectID) + setup func(t *testing.T) (*gitalypb.Repository, string, git.ObjectID) expectedStats ByteCountPerLanguage expectedErr string }{ { desc: "successful", - setup: func(t *testing.T) (string, git.ObjectID) { - _, repoPath := gittest.CloneRepo(t, cfg, cfg.Storages[0]) - return repoPath, commitID + setup: func(t *testing.T) (*gitalypb.Repository, string, git.ObjectID) { + repoProto, repoPath := gittest.CloneRepo(t, cfg, cfg.Storages[0]) + return repoProto, repoPath, commitID }, expectedStats: map[string]uint64{ "CoffeeScript": 107, @@ -48,16 +118,20 @@ func TestInstance_Stats(t *testing.T) { }, { desc: "preexisting cache", - setup: func(t *testing.T) (string, git.ObjectID) { - _, repoPath := gittest.CloneRepo(t, cfg, cfg.Storages[0]) + setup: func(t *testing.T) (*gitalypb.Repository, string, git.ObjectID) { + repoProto, repoPath := gittest.CloneRepo(t, cfg, cfg.Storages[0]) + repo := localrepo.NewTestRepo(t, cfg, repoProto) // We simply run the linguist once before so that it can already // write the cache. - _, err := linguist.Stats(ctx, repoPath, commitID.String()) + _, err := linguist.Stats(ctx, repo, commitID.String(), catfileCache) require.NoError(t, err) - require.FileExists(t, filepath.Join(repoPath, "language-stats.cache")) + require.FileExists(t, filepath.Join(repoPath, languageStatsFilename)) + + // Make sure it isn't able to generate stats from scratch + require.NoError(t, os.RemoveAll(filepath.Join(repoPath, "objects", "pack"))) - return repoPath, commitID + return repoProto, repoPath, commitID }, expectedStats: map[string]uint64{ "CoffeeScript": 107, @@ -68,12 +142,12 @@ func TestInstance_Stats(t *testing.T) { }, { desc: "corrupted cache", - setup: func(t *testing.T) (string, git.ObjectID) { - _, repoPath := gittest.CloneRepo(t, cfg, cfg.Storages[0]) + setup: func(t *testing.T) (*gitalypb.Repository, string, git.ObjectID) { + repoProto, repoPath := gittest.CloneRepo(t, cfg, cfg.Storages[0]) - require.NoError(t, os.WriteFile(filepath.Join(repoPath, "language-stats.cache"), []byte("garbage"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(repoPath, languageStatsFilename), []byte("garbage"), 0o644)) - return repoPath, commitID + return repoProto, repoPath, commitID }, expectedStats: map[string]uint64{ "CoffeeScript": 107, @@ -84,8 +158,9 @@ func TestInstance_Stats(t *testing.T) { }, { desc: "old cache", - setup: func(t *testing.T) (string, git.ObjectID) { - _, repoPath := gittest.InitRepo(t, cfg, cfg.Storages[0]) + setup: func(t *testing.T) (*gitalypb.Repository, string, git.ObjectID) { + repoProto, repoPath := gittest.InitRepo(t, cfg, cfg.Storages[0]) + repo := localrepo.NewTestRepo(t, cfg, repoProto) oldCommitID := gittest.WriteCommit(t, cfg, repoPath, gittest.WithParents(), gittest.WithTreeEntries( gittest.TreeEntry{Path: "main.rb", Content: "require 'fileutils'", Mode: "100644"}, @@ -96,14 +171,14 @@ func TestInstance_Stats(t *testing.T) { // Precreate the cache with the old commit. This ensures that // linguist knows to update the cache. - stats, err := linguist.Stats(ctx, repoPath, oldCommitID.String()) + stats, err := linguist.Stats(ctx, repo, oldCommitID.String(), catfileCache) require.NoError(t, err) - require.FileExists(t, filepath.Join(repoPath, "language-stats.cache")) + require.FileExists(t, filepath.Join(repoPath, languageStatsFilename)) require.Equal(t, ByteCountPerLanguage{ "Ruby": 19, }, stats) - return repoPath, newCommitID + return repoProto, repoPath, newCommitID }, expectedStats: map[string]uint64{ "Go": 12, @@ -111,30 +186,34 @@ func TestInstance_Stats(t *testing.T) { }, { desc: "missing repository", - setup: func(t *testing.T) (string, git.ObjectID) { - return filepath.Join(testhelper.TempDir(t), "nonexistent"), commitID + setup: func(t *testing.T) (*gitalypb.Repository, string, git.ObjectID) { + repoPath := filepath.Join(testhelper.TempDir(t), "nonexistent") + repoProto := &gitalypb.Repository{StorageName: cfg.Storages[0].Name, RelativePath: "nonexistent"} + + return repoProto, repoPath, commitID }, - expectedErr: "waiting for linguist: exit status 1", + expectedErr: "GetRepoPath: not a git repository", }, { desc: "missing commit", - setup: func(t *testing.T) (string, git.ObjectID) { - _, repoPath := gittest.InitRepo(t, cfg, cfg.Storages[0]) - return repoPath, commitID + setup: func(t *testing.T) (*gitalypb.Repository, string, git.ObjectID) { + repoProto, repoPath := gittest.InitRepo(t, cfg, cfg.Storages[0]) + return repoProto, repoPath, commitID }, - expectedErr: "waiting for linguist: exit status 1", + expectedErr: "linguist", }, } { t.Run(tc.desc, func(t *testing.T) { - repoPath, objectID := tc.setup(t) + repoProto, repoPath, objectID := tc.setup(t) + repo := localrepo.NewTestRepo(t, cfg, repoProto) - stats, err := linguist.Stats(ctx, repoPath, objectID.String()) + stats, err := linguist.Stats(ctx, repo, objectID.String(), catfileCache) if tc.expectedErr == "" { require.NoError(t, err) require.Equal(t, tc.expectedStats, stats) - require.FileExists(t, filepath.Join(repoPath, "language-stats.cache")) + require.FileExists(t, filepath.Join(repoPath, languageStatsFilename)) } else { - require.EqualError(t, err, tc.expectedErr) + require.Contains(t, err.Error(), tc.expectedErr) } }) } @@ -142,20 +221,64 @@ func TestInstance_Stats(t *testing.T) { func TestInstance_Stats_unmarshalJSONError(t *testing.T) { cfg := testcfg.Build(t) - ctx := testhelper.Context(t) + ctx := featureflag.ContextWithFeatureFlag(testhelper.Context(t), featureflag.GoLanguageStats, false) + gitCmdFactory := gittest.NewCommandFactory(t, cfg) + invalidRepo := &gitalypb.Repository{StorageName: "fake", RelativePath: "path"} - ling, err := New(cfg, gittest.NewCommandFactory(t, cfg)) + catfileCache := catfile.NewCache(cfg) + t.Cleanup(catfileCache.Stop) + + repo := localrepo.New(config.NewLocator(cfg), gitCmdFactory, catfileCache, invalidRepo) + + ling, err := New(cfg, gitCmdFactory) require.NoError(t, err) // When an error occurs, this used to trigger JSON marshelling of a plain string // the new behaviour shouldn't do that, and return an command error - _, err = ling.Stats(ctx, "/var/empty", "deadbeef") + _, err = ling.Stats(ctx, repo, "deadbeef", catfileCache) require.Error(t, err) _, ok := err.(*json.SyntaxError) require.False(t, ok, "expected the error not be a json Syntax Error") } +func TestInstance_Stats_incremental(t *testing.T) { + t.Parallel() + + cfg := testcfg.Build(t) + logger, hook := test.NewNullLogger() + ctx := testhelper.Context(t, testhelper.ContextWithLogger(logrus.NewEntry(logger))) + ctx = featureflag.ContextWithFeatureFlag(ctx, featureflag.GoLanguageStats, true) + + gitCmdFactory := gittest.NewCommandFactory(t, cfg) + + linguist, err := New(cfg, gitCmdFactory) + require.NoError(t, err) + + catfileCache := catfile.NewCache(cfg) + t.Cleanup(catfileCache.Stop) + + repoProto, repoPath := gittest.CloneRepo(t, cfg, cfg.Storages[0]) + repo := localrepo.NewTestRepo(t, cfg, repoProto) + + cleanStats, err := linguist.Stats(ctx, repo, "1e292f8fedd741b75372e19097c76d327140c312", catfileCache) + require.NoError(t, err) + require.Len(t, hook.AllEntries(), 0) + require.NoError(t, os.Remove(filepath.Join(repoPath, languageStatsFilename))) + + _, err = linguist.Stats(ctx, repo, "cfe32cf61b73a0d5e9f13e774abde7ff789b1660", catfileCache) + require.NoError(t, err) + require.Len(t, hook.AllEntries(), 0) + require.FileExists(t, filepath.Join(repoPath, languageStatsFilename)) + + incStats, err := linguist.Stats(ctx, repo, "1e292f8fedd741b75372e19097c76d327140c312", catfileCache) + require.NoError(t, err) + require.Len(t, hook.AllEntries(), 0) + require.FileExists(t, filepath.Join(repoPath, languageStatsFilename)) + + require.Equal(t, cleanStats, incStats) +} + func TestNew(t *testing.T) { cfg := testcfg.Build(t, testcfg.WithRealLinguist()) @@ -176,3 +299,63 @@ func TestNew_loadLanguagesCustomPath(t *testing.T) { require.Equal(t, "foo color", ling.Color("FooBar")) } + +// filenameForCache returns the filename where the cache is stored, depending on +// the feature flag. +func filenameForCache(ctx context.Context) string { + if featureflag.GoLanguageStats.IsDisabled(ctx) { + return "language-stats.cache" + } + return languageStatsFilename +} + +func BenchmarkInstance_Stats(b *testing.B) { + testhelper.NewFeatureSets(featureflag.GoLanguageStats). + Bench(b, benchmarkInstanceStats) +} + +func benchmarkInstanceStats(b *testing.B, ctx context.Context) { + cfg := testcfg.Build(b) + gitCmdFactory := gittest.NewCommandFactory(b, cfg) + languageStatsFilename := filenameForCache(ctx) + + linguist, err := New(cfg, gitCmdFactory) + require.NoError(b, err) + + catfileCache := catfile.NewCache(cfg) + b.Cleanup(catfileCache.Stop) + + repoProto, repoPath := gittest.CloneRepo(b, cfg, cfg.Storages[0], gittest.CloneRepoOpts{ + SourceRepo: "benchmark.git", + }) + repo := localrepo.NewTestRepo(b, cfg, repoProto) + + var scratchStat ByteCountPerLanguage + var incStats ByteCountPerLanguage + + b.Run("from scratch", func(b *testing.B) { + for i := 0; i < b.N; i++ { + b.StopTimer() + require.NoError(b, os.RemoveAll(filepath.Join(repoPath, languageStatsFilename))) + b.StartTimer() + + scratchStat, err = linguist.Stats(ctx, repo, "f5dfdd0057cd6bffc6259a5c8533dde5bf6a9d37", catfileCache) + require.NoError(b, err) + } + }) + + b.Run("incremental", func(b *testing.B) { + for i := 0; i < b.N; i++ { + b.StopTimer() + require.NoError(b, os.RemoveAll(filepath.Join(repoPath, languageStatsFilename))) + // a commit about 3 months older than the next + _, err = linguist.Stats(ctx, repo, "3c813b292d25a9b2ffda70e7f609f623bfc0cb37", catfileCache) + b.StartTimer() + + incStats, err = linguist.Stats(ctx, repo, "f5dfdd0057cd6bffc6259a5c8533dde5bf6a9d37", catfileCache) + require.NoError(b, err) + } + }) + + require.Equal(b, scratchStat, incStats) +} diff --git a/internal/gitaly/server/server_factory_test.go b/internal/gitaly/server/server_factory_test.go index a0cfafd1f..8053b5a1e 100644 --- a/internal/gitaly/server/server_factory_test.go +++ b/internal/gitaly/server/server_factory_test.go @@ -148,7 +148,7 @@ func TestGitalyServerFactory(t *testing.T) { }) t.Run("logging check", func(t *testing.T) { - testhelper.ModifyEnvironment(t, "GITALY_LOG_REQUEST_METHOD_ALLOW_PATTERN", ".") + t.Setenv("GITALY_LOG_REQUEST_METHOD_ALLOW_PATTERN", ".") cfg := testcfg.Build(t) logger, hook := test.NewNullLogger() diff --git a/internal/gitaly/service/commit/languages.go b/internal/gitaly/service/commit/languages.go index 9577d4864..4363011c0 100644 --- a/internal/gitaly/service/commit/languages.go +++ b/internal/gitaly/service/commit/languages.go @@ -38,11 +38,7 @@ func (s *server) CommitLanguages(ctx context.Context, req *gitalypb.CommitLangua return nil, helper.ErrInternalf("looking up revision: %w", err) } - repoPath, err := repo.Path() - if err != nil { - return nil, helper.ErrInternalf("repository path: %w", err) - } - stats, err := s.linguist.Stats(ctx, repoPath, commitID) + stats, err := s.linguist.Stats(ctx, repo, commitID, s.catfileCache) if err != nil { return nil, helper.ErrInternalf("language stats: %w", err) } diff --git a/internal/gitaly/service/commit/languages_test.go b/internal/gitaly/service/commit/languages_test.go index 8f4790c89..731f950cd 100644 --- a/internal/gitaly/service/commit/languages_test.go +++ b/internal/gitaly/service/commit/languages_test.go @@ -1,10 +1,12 @@ package commit import ( + "context" "testing" "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitaly/v15/internal/git/gittest" + "gitlab.com/gitlab-org/gitaly/v15/internal/metadata/featureflag" "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper" "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper/testcfg" "gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb" @@ -12,12 +14,16 @@ import ( ) func TestLanguages(t *testing.T) { + testhelper.NewFeatureSets(featureflag.GoLanguageStats). + Run(t, testLanguagesFeatured) +} + +func testLanguagesFeatured(t *testing.T, ctx context.Context) { t.Parallel() cfg := testcfg.Build(t, testcfg.WithRealLinguist()) cfg.SocketPath = startTestServices(t, cfg) - ctx := testhelper.Context(t) repo, _ := gittest.CreateRepository(ctx, t, cfg, gittest.CreateRepositoryConfig{ Seed: gittest.SeedGitLabTest, }) @@ -32,30 +38,25 @@ func TestLanguages(t *testing.T) { resp, err := client.CommitLanguages(ctx, request) require.NoError(t, err) - require.NotZero(t, len(resp.Languages), "number of languages in response") - expectedLanguages := []*gitalypb.CommitLanguagesResponse_Language{ - {Name: "Ruby", Share: 66, Color: "#701516", FileCount: 4, Bytes: 2943}, - {Name: "JavaScript", Share: 22, Color: "#f1e05a", FileCount: 1, Bytes: 1014}, - {Name: "HTML", Share: 7, Color: "#e34c26", FileCount: 1, Bytes: 349}, - {Name: "CoffeeScript", Share: 2, Color: "#244776", FileCount: 1, Bytes: 107}, - // Modula-2 is a special case because Linguist has no color for it. This - // test case asserts that we invent a color for it (SHA256 of the name). - {Name: "Modula-2", Share: 2, Color: "#3fd5e0", FileCount: 1, Bytes: 95}, + {Name: "Ruby", Share: 65.28394, Color: "#701516", Bytes: 2943}, + {Name: "JavaScript", Share: 22.493345, Color: "#f1e05a", Bytes: 1014}, + {Name: "HTML", Share: 7.741792, Color: "#e34c26", Bytes: 349}, + {Name: "CoffeeScript", Share: 2.373558, Color: "#244776", Bytes: 107}, + {Name: "Modula-2", Share: 2.1073646, Color: "#10253f", Bytes: 95}, } - require.Equal(t, len(expectedLanguages), len(resp.Languages)) - - for i, el := range expectedLanguages { - actualLanguage := resp.Languages[i] - requireLanguageEqual(t, el, actualLanguage) - } + testhelper.ProtoEqual(t, expectedLanguages, resp.Languages) } func TestFileCountIsZeroWhenFeatureIsDisabled(t *testing.T) { + testhelper.NewFeatureSets(featureflag.GoLanguageStats). + Run(t, testFileCountIsZeroWhenFeatureIsDisabled) +} + +func testFileCountIsZeroWhenFeatureIsDisabled(t *testing.T, ctx context.Context) { t.Parallel() - ctx := testhelper.Context(t) _, repo, _, client := setupCommitServiceWithRepo(ctx, t) request := &gitalypb.CommitLanguagesRequest{ @@ -74,19 +75,14 @@ func TestFileCountIsZeroWhenFeatureIsDisabled(t *testing.T) { } } -func requireLanguageEqual(t *testing.T, expected, actual *gitalypb.CommitLanguagesResponse_Language) { - t.Helper() - - require.Equal(t, expected.Name, actual.Name) - require.Equal(t, expected.Color, actual.Color) - require.False(t, (expected.Share-actual.Share)*(expected.Share-actual.Share) >= 1.0, "shares do not match") - require.Equal(t, expected.Bytes, actual.Bytes) +func TestLanguagesEmptyRevision(t *testing.T) { + testhelper.NewFeatureSets(featureflag.GoLanguageStats). + Run(t, testLanguagesEmptyRevisionFeatured) } -func TestLanguagesEmptyRevision(t *testing.T) { +func testLanguagesEmptyRevisionFeatured(t *testing.T, ctx context.Context) { t.Parallel() - ctx := testhelper.Context(t) _, repo, _, client := setupCommitServiceWithRepo(ctx, t) request := &gitalypb.CommitLanguagesRequest{ @@ -107,9 +103,13 @@ func TestLanguagesEmptyRevision(t *testing.T) { } func TestInvalidCommitLanguagesRequestRevision(t *testing.T) { + testhelper.NewFeatureSets(featureflag.GoLanguageStats). + Run(t, testInvalidCommitLanguagesRequestRevisionFeatured) +} + +func testInvalidCommitLanguagesRequestRevisionFeatured(t *testing.T, ctx context.Context) { t.Parallel() - ctx := testhelper.Context(t) _, repo, _, client := setupCommitServiceWithRepo(ctx, t) _, err := client.CommitLanguages(ctx, &gitalypb.CommitLanguagesRequest{ @@ -120,9 +120,13 @@ func TestInvalidCommitLanguagesRequestRevision(t *testing.T) { } func TestAmbiguousRefCommitLanguagesRequestRevision(t *testing.T) { + testhelper.NewFeatureSets(featureflag.GoLanguageStats). + Run(t, testAmbiguousRefCommitLanguagesRequestRevisionFeatured) +} + +func testAmbiguousRefCommitLanguagesRequestRevisionFeatured(t *testing.T, ctx context.Context) { t.Parallel() - ctx := testhelper.Context(t) _, repo, _, client := setupCommitServiceWithRepo(ctx, t) // gitlab-test repo has both a branch and a tag named 'v1.1.0' diff --git a/internal/gitaly/service/commit/tree_entries.go b/internal/gitaly/service/commit/tree_entries.go index a70d01e5e..2a2489526 100644 --- a/internal/gitaly/service/commit/tree_entries.go +++ b/internal/gitaly/service/commit/tree_entries.go @@ -70,6 +70,7 @@ func (s *server) sendTreeEntries( repo *localrepo.Repo, revision, path string, recursive bool, + skipFlatPaths bool, sort gitalypb.GetTreeEntriesRequest_SortBy, p *gitalypb.PaginationParameter, ) error { @@ -187,7 +188,7 @@ func (s *server) sendTreeEntries( treeSender.SetPaginationCursor(cursor) } - if !recursive { + if !recursive && !skipFlatPaths { // When we're not doing a recursive request, then we need to populate flat // paths. A flat path of a tree entry refers to the first subtree of that // entry which either has at least one blob or more than two subtrees. In @@ -296,7 +297,7 @@ func (s *server) GetTreeEntries(in *gitalypb.GetTreeEntriesRequest, stream gital revision := string(in.GetRevision()) path := string(in.GetPath()) - return s.sendTreeEntries(stream, repo, revision, path, in.Recursive, in.GetSort(), in.GetPaginationParams()) + return s.sendTreeEntries(stream, repo, revision, path, in.Recursive, in.SkipFlatPaths, in.GetSort(), in.GetPaginationParams()) } func paginateTreeEntries(entries []*gitalypb.TreeEntry, p *gitalypb.PaginationParameter) ([]*gitalypb.TreeEntry, string, error) { diff --git a/internal/gitaly/service/commit/tree_entries_test.go b/internal/gitaly/service/commit/tree_entries_test.go index e377988b1..b94ec8cc9 100644 --- a/internal/gitaly/service/commit/tree_entries_test.go +++ b/internal/gitaly/service/commit/tree_entries_test.go @@ -14,6 +14,7 @@ import ( "gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" ) func TestGetTreeEntries_curlyBraces(t *testing.T) { @@ -327,16 +328,29 @@ func TestGetTreeEntries_successful(t *testing.T) { }, } + stripFlatPaths := func(entries []*gitalypb.TreeEntry) []*gitalypb.TreeEntry { + strippedEntries := make([]*gitalypb.TreeEntry, 0, len(entries)) + + for _, entry := range entries { + entry := proto.Clone(entry).(*gitalypb.TreeEntry) + entry.FlatPath = nil + strippedEntries = append(strippedEntries, entry) + } + + return strippedEntries + } + testCases := []struct { - description string - revision []byte - path []byte - recursive bool - sortBy gitalypb.GetTreeEntriesRequest_SortBy - entries []*gitalypb.TreeEntry - pageToken string - pageLimit int32 - cursor string + description string + revision []byte + path []byte + recursive bool + sortBy gitalypb.GetTreeEntriesRequest_SortBy + entries []*gitalypb.TreeEntry + pageToken string + pageLimit int32 + cursor string + skipFlatPaths bool }{ { description: "with root path", @@ -345,12 +359,26 @@ func TestGetTreeEntries_successful(t *testing.T) { entries: rootEntries, }, { + description: "with root path and disabled flat paths", + revision: []byte(commitID), + path: []byte("."), + skipFlatPaths: true, + entries: stripFlatPaths(rootEntries), + }, + { description: "with a folder", revision: []byte(commitID), path: []byte("files"), entries: filesDirEntries, }, { + description: "with a folder and disabled flat paths", + revision: []byte(commitID), + path: []byte("files"), + skipFlatPaths: true, + entries: stripFlatPaths(filesDirEntries), + }, + { description: "with recursive", revision: []byte(commitID), path: []byte("level-0"), @@ -451,11 +479,12 @@ func TestGetTreeEntries_successful(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.description, func(t *testing.T) { request := &gitalypb.GetTreeEntriesRequest{ - Repository: repo, - Revision: testCase.revision, - Path: testCase.path, - Recursive: testCase.recursive, - Sort: testCase.sortBy, + Repository: repo, + Revision: testCase.revision, + Path: testCase.path, + Recursive: testCase.recursive, + Sort: testCase.sortBy, + SkipFlatPaths: testCase.skipFlatPaths, } if testCase.pageToken != "" || testCase.pageLimit > 0 { @@ -469,7 +498,7 @@ func TestGetTreeEntries_successful(t *testing.T) { require.NoError(t, err) fetchedEntries, cursor := getTreeEntriesFromTreeEntryClient(t, c, nil) - require.Equal(t, testCase.entries, fetchedEntries) + testhelper.ProtoEqual(t, testCase.entries, fetchedEntries) if testCase.pageLimit > 0 && len(testCase.entries) < len(rootEntries) { require.NotNil(t, cursor) diff --git a/internal/gitaly/service/hook/post_receive_test.go b/internal/gitaly/service/hook/post_receive_test.go index 39b0aeb27..96b3d221f 100644 --- a/internal/gitaly/service/hook/post_receive_test.go +++ b/internal/gitaly/service/hook/post_receive_test.go @@ -109,7 +109,7 @@ func TestHooksMissingStdin(t *testing.T) { Protocol: "protocol", }, git.PostReceiveHook, - featureflag.RawFromContext(ctx), + featureflag.FromContext(ctx), ).Env() require.NoError(t, err) @@ -237,7 +237,7 @@ To create a merge request for okay, visit: Protocol: "protocol", }, git.PostReceiveHook, - featureflag.RawFromContext(ctx), + featureflag.FromContext(ctx), ).Env() require.NoError(t, err) diff --git a/internal/gitaly/service/hook/pre_receive_test.go b/internal/gitaly/service/hook/pre_receive_test.go index 34175bb46..12fed62be 100644 --- a/internal/gitaly/service/hook/pre_receive_test.go +++ b/internal/gitaly/service/hook/pre_receive_test.go @@ -149,7 +149,7 @@ func TestPreReceiveHook_GitlabAPIAccess(t *testing.T) { Protocol: protocol, }, git.PreReceiveHook, - featureflag.RawFromContext(ctx), + featureflag.FromContext(ctx), ).Env() require.NoError(t, err) @@ -271,7 +271,7 @@ func TestPreReceive_APIErrors(t *testing.T) { Protocol: "web", }, git.PreReceiveHook, - featureflag.RawFromContext(ctx), + featureflag.FromContext(ctx), ).Env() require.NoError(t, err) @@ -345,7 +345,7 @@ exit %d Protocol: "web", }, git.PreReceiveHook, - featureflag.RawFromContext(ctx), + featureflag.FromContext(ctx), ).Env() require.NoError(t, err) @@ -478,7 +478,7 @@ func TestPreReceiveHook_Primary(t *testing.T) { Protocol: "web", }, git.PreReceiveHook, - featureflag.RawFromContext(ctx), + featureflag.FromContext(ctx), ).Env() require.NoError(t, err) diff --git a/internal/gitaly/service/hook/reference_transaction_test.go b/internal/gitaly/service/hook/reference_transaction_test.go index b466cee33..5a1c2c9eb 100644 --- a/internal/gitaly/service/hook/reference_transaction_test.go +++ b/internal/gitaly/service/hook/reference_transaction_test.go @@ -155,7 +155,7 @@ func TestReferenceTransactionHook(t *testing.T) { }, nil, git.ReferenceTransactionHook, - featureflag.RawFromContext(ctx), + featureflag.FromContext(ctx), ).Env() require.NoError(t, err) diff --git a/internal/gitaly/service/hook/update_test.go b/internal/gitaly/service/hook/update_test.go index 98e30678c..233745f6c 100644 --- a/internal/gitaly/service/hook/update_test.go +++ b/internal/gitaly/service/hook/update_test.go @@ -47,7 +47,7 @@ func TestUpdate_CustomHooks(t *testing.T) { Protocol: "web", }, git.UpdateHook, - featureflag.RawFromContext(ctx), + featureflag.FromContext(ctx), ).Env() require.NoError(t, err) diff --git a/internal/gitaly/service/repository/create_fork_test.go b/internal/gitaly/service/repository/create_fork_test.go index d4ccd2994..a25c83aa5 100644 --- a/internal/gitaly/service/repository/create_fork_test.go +++ b/internal/gitaly/service/repository/create_fork_test.go @@ -222,8 +222,7 @@ func TestCreateFork_targetExists(t *testing.T) { func injectCustomCATestCerts(t *testing.T) (*x509.CertPool, config.TLS) { certFile, keyFile := testhelper.GenerateCerts(t) - - testhelper.ModifyEnvironment(t, gitalyx509.SSLCertFile, certFile) + t.Setenv(gitalyx509.SSLCertFile, certFile) caPEMBytes := testhelper.MustReadFile(t, certFile) pool := x509.NewCertPool() diff --git a/internal/gitaly/service/smarthttp/inforefs_test.go b/internal/gitaly/service/smarthttp/inforefs_test.go index 3aa10036f..ca0e6e4cc 100644 --- a/internal/gitaly/service/smarthttp/inforefs_test.go +++ b/internal/gitaly/service/smarthttp/inforefs_test.go @@ -360,7 +360,7 @@ func TestCacheInfoRefsUploadPack(t *testing.T) { // Praefect explicitly injects all unset feature flags, the key is thus differend depending // on whether Praefect is in use or not. We thus manually inject all feature flags here such // that they're forced to the same state. - for _, ff := range featureflag.All { + for _, ff := range featureflag.DefinedFlags() { ctx = featureflag.OutgoingCtxWithFeatureFlag(ctx, ff, true) ctx = featureflag.IncomingCtxWithFeatureFlag(ctx, ff, true) } diff --git a/internal/gitaly/service/smarthttp/receive_pack_test.go b/internal/gitaly/service/smarthttp/receive_pack_test.go index 981e658e5..7f9a46ff1 100644 --- a/internal/gitaly/service/smarthttp/receive_pack_test.go +++ b/internal/gitaly/service/smarthttp/receive_pack_test.go @@ -68,7 +68,7 @@ func TestPostReceivePack_successful(t *testing.T) { // the context's feature flags we see here and the context's metadata as it would // arrive on the proxied Gitaly. To fix this, we thus inject all feature flags // explicitly here. - for _, ff := range featureflag.All { + for _, ff := range featureflag.DefinedFlags() { ctx = featureflag.OutgoingCtxWithFeatureFlag(ctx, ff, true) ctx = featureflag.IncomingCtxWithFeatureFlag(ctx, ff, true) } @@ -105,6 +105,18 @@ func TestPostReceivePack_successful(t *testing.T) { // figuring out their actual contents. So let's just remove it, too. payload.Transaction = nil + var expectedFeatureFlags []git.FeatureFlagWithValue + for feature, enabled := range featureflag.FromContext(ctx) { + expectedFeatureFlags = append(expectedFeatureFlags, git.FeatureFlagWithValue{ + Flag: feature, Enabled: enabled, + }) + } + + // Compare here without paying attention to the order given that flags aren't sorted and + // unset the struct member afterwards. + require.ElementsMatch(t, expectedFeatureFlags, payload.FeatureFlagsWithValue) + payload.FeatureFlagsWithValue = nil + require.Equal(t, git.HooksPayload{ RuntimeDir: cfg.RuntimeDir, InternalSocket: cfg.InternalSocketPath(), @@ -115,7 +127,6 @@ func TestPostReceivePack_successful(t *testing.T) { Protocol: "http", }, RequestedHooks: git.ReceivePackHooks, - FeatureFlags: featureflag.RawFromContext(ctx), }, payload) } diff --git a/internal/gitaly/service/ssh/receive_pack_test.go b/internal/gitaly/service/ssh/receive_pack_test.go index 66fc1e596..a65c17ef5 100644 --- a/internal/gitaly/service/ssh/receive_pack_test.go +++ b/internal/gitaly/service/ssh/receive_pack_test.go @@ -120,7 +120,7 @@ func TestReceivePackPushSuccess(t *testing.T) { // when deserializing the HooksPayload. By setting all flags to `true` explicitly, we both // verify that gitaly-ssh picks up feature flags correctly and fix the test to behave the // same with and without Praefect. - for _, featureFlag := range featureflag.All { + for _, featureFlag := range featureflag.DefinedFlags() { ctx = featureflag.ContextWithFeatureFlag(ctx, featureFlag, true) } @@ -152,11 +152,18 @@ func TestReceivePackPushSuccess(t *testing.T) { // figuring out their actual contents. So let's just remove it, too. payload.Transaction = nil - expectedFeatureFlags := featureflag.Raw{} - for _, feature := range featureflag.All { - expectedFeatureFlags[feature.MetadataKey()] = "true" + var expectedFeatureFlags []git.FeatureFlagWithValue + for _, feature := range featureflag.DefinedFlags() { + expectedFeatureFlags = append(expectedFeatureFlags, git.FeatureFlagWithValue{ + Flag: feature, Enabled: true, + }) } + // Compare here without paying attention to the order given that flags aren't sorted and + // unset the struct member afterwards. + require.ElementsMatch(t, expectedFeatureFlags, payload.FeatureFlagsWithValue) + payload.FeatureFlagsWithValue = nil + require.Equal(t, git.HooksPayload{ RuntimeDir: cfg.RuntimeDir, InternalSocket: cfg.InternalSocketPath(), @@ -167,7 +174,6 @@ func TestReceivePackPushSuccess(t *testing.T) { Protocol: "ssh", }, RequestedHooks: git.ReceivePackHooks, - FeatureFlags: expectedFeatureFlags, }, payload) } @@ -668,11 +674,16 @@ func sshPushCommand(ctx context.Context, t *testing.T, cfg config.Cfg, cloneDeta }) require.NoError(t, err) + var flagsWithValues []string + for flag, value := range featureflag.FromContext(ctx) { + flagsWithValues = append(flagsWithValues, flag.FormatWithValue(value)) + } + cmd := gittest.NewCommand(t, cfg, "-C", cloneDetails.LocalRepoPath, "push", "-v", "git@localhost:test/test.git", "master") cmd.Env = []string{ fmt.Sprintf("GITALY_PAYLOAD=%s", payload), fmt.Sprintf("GITALY_ADDRESS=%s", serverSocketPath), - fmt.Sprintf("GITALY_FEATUREFLAGS=%s", strings.Join(featureflag.AllFlags(ctx), ",")), + fmt.Sprintf("GITALY_FEATUREFLAGS=%s", strings.Join(flagsWithValues, ",")), fmt.Sprintf("GIT_SSH_COMMAND=%s receive-pack", filepath.Join(cfg.BinDir, "gitaly-ssh")), } diff --git a/internal/gitaly/service/ssh/upload_pack_test.go b/internal/gitaly/service/ssh/upload_pack_test.go index 113cd816a..a9d010699 100644 --- a/internal/gitaly/service/ssh/upload_pack_test.go +++ b/internal/gitaly/service/ssh/upload_pack_test.go @@ -56,10 +56,15 @@ func runClone( payload, err := protojson.Marshal(request) require.NoError(t, err) + var flagsWithValues []string + for flag, value := range featureflag.FromContext(ctx) { + flagsWithValues = append(flagsWithValues, flag.FormatWithValue(value)) + } + env := []string{ fmt.Sprintf("GITALY_ADDRESS=%s", cfg.SocketPath), fmt.Sprintf("GITALY_PAYLOAD=%s", payload), - fmt.Sprintf("GITALY_FEATUREFLAGS=%s", strings.Join(featureflag.AllFlags(ctx), ",")), + fmt.Sprintf("GITALY_FEATUREFLAGS=%s", strings.Join(flagsWithValues, ",")), fmt.Sprintf(`GIT_SSH_COMMAND=%s upload-pack`, filepath.Join(cfg.BinDir, "gitaly-ssh")), } if withSidechannel { diff --git a/internal/helper/env/env_test.go b/internal/helper/env/env_test.go index d38cf0362..5a83ebbe4 100644 --- a/internal/helper/env/env_test.go +++ b/internal/helper/env/env_test.go @@ -1,208 +1,288 @@ package env_test import ( - "errors" "fmt" "strconv" "testing" "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitaly/v15/internal/helper/env" "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper" ) func TestGetBool(t *testing.T) { + const envvar = "TEST_BOOL" + for _, tc := range []struct { - value string + desc string + setup func(t *testing.T) fallback bool - expected bool - expectedErrIs error + expectedValue bool + expectedErr error }{ { - value: "true", - expected: true, + desc: "explicitly true", + setup: setupEnv(envvar, "true"), + expectedValue: true, + }, + { + desc: "explicitly false", + setup: setupEnv(envvar, "false"), + expectedValue: false, }, { - value: "false", - expected: false, + desc: "explicitly 1", + setup: setupEnv(envvar, "1"), + expectedValue: true, }, { - value: "1", - expected: true, + desc: "explicitly 0", + setup: setupEnv(envvar, "0"), + expectedValue: false, + }, + { + desc: "missing value", + setup: func(t *testing.T) { + testhelper.Unsetenv(t, envvar) + }, + expectedValue: false, }, { - value: "0", - expected: false, + desc: "missing value with fallback", + setup: func(t *testing.T) { + testhelper.Unsetenv(t, envvar) + }, + fallback: true, + expectedValue: true, }, { - value: "", - expected: false, + desc: "empty value", + setup: setupEnv(envvar, ""), + expectedValue: false, }, { - value: "", - fallback: true, - expected: true, + desc: "empty value with fallback", + setup: setupEnv(envvar, ""), + fallback: true, + expectedValue: true, }, { - value: "bad", - expected: false, - expectedErrIs: strconv.ErrSyntax, + desc: "invalid value", + setup: setupEnv(envvar, "bad"), + expectedValue: false, + expectedErr: fmt.Errorf("get bool TEST_BOOL: %w", &strconv.NumError{ + Func: "ParseBool", + Num: "bad", + Err: strconv.ErrSyntax, + }), }, { - value: "bad", + desc: "invalid value with fallback", + setup: setupEnv(envvar, "bad"), fallback: true, - expected: true, - expectedErrIs: strconv.ErrSyntax, + expectedValue: true, + expectedErr: fmt.Errorf("get bool TEST_BOOL: %w", &strconv.NumError{ + Func: "ParseBool", + Num: "bad", + Err: strconv.ErrSyntax, + }), }, } { - t.Run(fmt.Sprintf("value=%s,fallback=%t", tc.value, tc.fallback), func(t *testing.T) { - testhelper.ModifyEnvironment(t, "TEST_BOOL", tc.value) - - result, err := env.GetBool("TEST_BOOL", tc.fallback) - - if tc.expectedErrIs != nil { - assert.Error(t, err) - assert.True(t, errors.Is(err, tc.expectedErrIs), err) - } else { - assert.NoError(t, err) - } + t.Run(tc.desc, func(t *testing.T) { + tc.setup(t) - assert.Equal(t, tc.expected, result) + value, err := env.GetBool(envvar, tc.fallback) + require.Equal(t, tc.expectedErr, err) + require.Equal(t, tc.expectedValue, value) }) } } func TestGetInt(t *testing.T) { + const envvar = "TEST_INT" + for _, tc := range []struct { - value string + desc string + setup func(t *testing.T) fallback int - expected int - expectedErrIs error + expectedValue int + expectedErr error }{ { - value: "3", - expected: 3, + desc: "valid number", + setup: setupEnv(envvar, "3"), + expectedValue: 3, + }, + { + desc: "unset value", + setup: func(t *testing.T) { + testhelper.Unsetenv(t, envvar) + }, + expectedValue: 0, + }, + { + desc: "unset value with fallback", + setup: func(t *testing.T) { + testhelper.Unsetenv(t, envvar) + }, + fallback: 3, + expectedValue: 3, }, { - value: "", - expected: 0, + desc: "empty value", + setup: setupEnv(envvar, ""), + expectedValue: 0, }, { - value: "", - fallback: 3, - expected: 3, + desc: "empty value with fallback", + setup: setupEnv(envvar, ""), + fallback: 3, + expectedValue: 3, }, { - value: "bad", - expected: 0, - expectedErrIs: strconv.ErrSyntax, + desc: "invalid value", + setup: setupEnv(envvar, "bad"), + expectedValue: 0, + expectedErr: fmt.Errorf("get int TEST_INT: %w", &strconv.NumError{ + Func: "Atoi", + Num: "bad", + Err: strconv.ErrSyntax, + }), }, { - value: "bad", + desc: "invalid value with fallback", + setup: setupEnv(envvar, "bad"), fallback: 3, - expected: 3, - expectedErrIs: strconv.ErrSyntax, + expectedValue: 3, + expectedErr: fmt.Errorf("get int TEST_INT: %w", &strconv.NumError{ + Func: "Atoi", + Num: "bad", + Err: strconv.ErrSyntax, + }), }, } { - t.Run(fmt.Sprintf("value=%s,fallback=%d", tc.value, tc.fallback), func(t *testing.T) { - testhelper.ModifyEnvironment(t, "TEST_INT", tc.value) - - result, err := env.GetInt("TEST_INT", tc.fallback) - - if tc.expectedErrIs != nil { - assert.Error(t, err) - assert.True(t, errors.Is(err, tc.expectedErrIs), err) - } else { - assert.NoError(t, err) - } + t.Run(tc.desc, func(t *testing.T) { + tc.setup(t) - assert.Equal(t, tc.expected, result) + value, err := env.GetInt(envvar, tc.fallback) + require.Equal(t, tc.expectedErr, err) + require.Equal(t, tc.expectedValue, value) }) } } func TestGetDuration(t *testing.T) { + const envvar = "TEST_DURATION" + for _, tc := range []struct { - value string - fallback time.Duration - expected time.Duration - expectedErr string + desc string + setup func(t *testing.T) + fallback time.Duration + expectedValue time.Duration + expectedErr error }{ { - value: "3m", - fallback: 0, - expected: 3 * time.Minute, + desc: "valid duration", + setup: setupEnv(envvar, "3m"), + fallback: 0, + expectedValue: 3 * time.Minute, + }, + { + desc: "unset value", + setup: func(t *testing.T) { + testhelper.Unsetenv(t, envvar) + }, + expectedValue: 0, + }, + { + desc: "unset value with fallback", + setup: func(t *testing.T) { + testhelper.Unsetenv(t, envvar) + }, + fallback: 3, + expectedValue: 3, }, { - value: "", - expected: 0, + desc: "empty value", + setup: setupEnv(envvar, ""), + expectedValue: 0, }, { - value: "", - fallback: 3, - expected: 3, + desc: "empty value with fallback", + setup: setupEnv(envvar, ""), + fallback: 3, + expectedValue: 3, }, { - value: "bad", - expected: 0, - expectedErr: `get duration TEST_DURATION: time: invalid duration "bad"`, + desc: "invalid value", + setup: setupEnv(envvar, "bad"), + expectedValue: 0, + expectedErr: fmt.Errorf("get duration TEST_DURATION: %w", fmt.Errorf("time: invalid duration \"bad\"")), }, { - value: "bad", - fallback: 3, - expected: 3, - expectedErr: `get duration TEST_DURATION: time: invalid duration "bad"`, + desc: "invalid value with fallback", + setup: setupEnv(envvar, "bad"), + fallback: 3, + expectedValue: 3, + expectedErr: fmt.Errorf("get duration TEST_DURATION: %w", fmt.Errorf("time: invalid duration \"bad\"")), }, } { - t.Run(fmt.Sprintf("value=%s,fallback=%d", tc.value, tc.fallback), func(t *testing.T) { - testhelper.ModifyEnvironment(t, "TEST_DURATION", tc.value) - - result, err := env.GetDuration("TEST_DURATION", tc.fallback) - - if tc.expectedErr != "" { - assert.Error(t, err) - assert.EqualError(t, err, tc.expectedErr) - } else { - assert.NoError(t, err) - } + t.Run(tc.desc, func(t *testing.T) { + tc.setup(t) - assert.Equal(t, tc.expected, result) + value, err := env.GetDuration(envvar, tc.fallback) + require.Equal(t, tc.expectedErr, err) + require.Equal(t, tc.expectedValue, value) }) } } func TestGetString(t *testing.T) { + const envvar = "TEST_STRING" + for _, tc := range []struct { - value string - fallback string - expected string + desc string + setup func(t *testing.T) + fallback string + expectedValue string }{ { - value: "Hello", - expected: "Hello", + desc: "simple string", + setup: setupEnv(envvar, "Hello"), + expectedValue: "Hello", }, { - value: "hello ", - expected: "hello", + desc: "string with trailing whitespace", + setup: setupEnv(envvar, "hello "), + expectedValue: "hello", + }, + { + desc: "unset value", + setup: func(t *testing.T) { + testhelper.Unsetenv(t, envvar) + }, + fallback: "fallback value", + expectedValue: "fallback value", }, { - fallback: "fallback value", - expected: "fallback value", + desc: "empty value", + setup: setupEnv(envvar, ""), + fallback: "fallback value", + expectedValue: "fallback value", }, { - value: " ", - fallback: "fallback value", - expected: "", + desc: "whitespace only", + setup: setupEnv(envvar, " "), + fallback: "fallback value", + expectedValue: "", }, } { - t.Run(fmt.Sprintf("value=%s,fallback=%s", tc.value, tc.fallback), func(t *testing.T) { - testhelper.ModifyEnvironment(t, "TEST_STRING", tc.value) - - result := env.GetString("TEST_STRING", tc.fallback) + t.Run(tc.desc, func(t *testing.T) { + tc.setup(t) - assert.Equal(t, tc.expected, result) + value := env.GetString(envvar, tc.fallback) + require.Equal(t, tc.expectedValue, value) }) } } @@ -262,3 +342,9 @@ func TestExtractKey(t *testing.T) { }) } } + +func setupEnv(key, value string) func(t *testing.T) { + return func(t *testing.T) { + t.Setenv(key, value) + } +} diff --git a/internal/metadata/featureflag/context.go b/internal/metadata/featureflag/context.go index 9483abf25..950e81c70 100644 --- a/internal/metadata/featureflag/context.go +++ b/internal/metadata/featureflag/context.go @@ -10,13 +10,19 @@ import ( ) const ( - // Delim is a delimiter used between a feature flag name and its value. - Delim = ":" - - // ffPrefix is the prefix used for Gitaly-scoped feature flags. - ffPrefix = "gitaly-feature-" + // explicitFeatureFlagKey is used by ContextWithExplicitFeatureFlags to mark a context as + // requiring all feature flags to have been explicitly defined. + explicitFeatureFlagKey = "require_explicit_feature_flag_checks" ) +// ContextWithExplicitFeatureFlags marks the context such that all feature flags which are checked +// must have been explicitly set in that context. If a feature flag wasn't set to an explicit value, +// then checking this feature flag will panic. This is not for use in production systems, but is +// intended for tests to verify that we test each feature flag properly. +func ContextWithExplicitFeatureFlags(ctx context.Context) context.Context { + return injectIntoIncomingAndOutgoingContext(ctx, explicitFeatureFlagKey, true) +} + // ContextWithFeatureFlag sets the feature flag in both the incoming and outgoing context. func ContextWithFeatureFlag(ctx context.Context, flag FeatureFlag, enabled bool) context.Context { return injectIntoIncomingAndOutgoingContext(ctx, flag.MetadataKey(), enabled) @@ -68,56 +74,44 @@ func incomingCtxWithFeatureFlag(ctx context.Context, key string, enabled bool) c return metadata.NewIncomingContext(ctx, md) } -// AllFlags returns all feature flags with their value that use the Gitaly metadata -// prefix. Note: results will not be sorted. -func AllFlags(ctx context.Context) []string { - featureFlagMap := RawFromContext(ctx) - featureFlagSlice := make([]string, 0, len(featureFlagMap)) - for key, value := range featureFlagMap { - featureFlagSlice = append(featureFlagSlice, - fmt.Sprintf("%s%s%s", strings.TrimPrefix(key, ffPrefix), Delim, value), - ) +func injectIntoIncomingAndOutgoingContext(ctx context.Context, key string, enabled bool) context.Context { + incomingMD, ok := metadata.FromIncomingContext(ctx) + if !ok { + incomingMD = metadata.New(map[string]string{}) } - return featureFlagSlice + incomingMD.Set(key, strconv.FormatBool(enabled)) + + ctx = metadata.NewIncomingContext(ctx, incomingMD) + + return metadata.AppendToOutgoingContext(ctx, key, strconv.FormatBool(enabled)) } -// Raw contains feature flags and their values in their raw form with header prefix in place -// and values unparsed. -type Raw map[string]string - -// RawFromContext returns a map that contains all feature flags with their values. The feature -// flags are in their raw format with the header prefix in place. If multiple values are set a -// flag, the first occurrence is used. -// -// This is mostly intended for propagating the feature flags by other means than the metadata, -// for example into the hooks through the environment. -func RawFromContext(ctx context.Context) Raw { +// FromContext returns the set of all feature flags defined in the context. This returns both +// feature flags that are currently defined by Gitaly, but may also return some that aren't defined +// by us in case they match the feature flag prefix but don't have a definition. This function also +// returns the state of the feature flag *as defined in the context*. This value may be overridden. +func FromContext(ctx context.Context) map[FeatureFlag]bool { md, ok := metadata.FromIncomingContext(ctx) if !ok { - return nil + return map[FeatureFlag]bool{} } - featureFlags := map[string]string{} - for key, values := range md { - if !strings.HasPrefix(key, ffPrefix) || len(values) == 0 { + flags := map[FeatureFlag]bool{} + for metadataName, values := range md { + if len(values) == 0 { continue } - featureFlags[key] = values[0] - } - - return featureFlags -} + flag, err := FromMetadataKey(metadataName) + if err != nil { + continue + } -// OutgoingWithRaw returns a new context with raw flags appended into the outgoing -// metadata. -func OutgoingWithRaw(ctx context.Context, flags Raw) context.Context { - for key, value := range flags { - ctx = metadata.AppendToOutgoingContext(ctx, key, value) + flags[flag] = values[0] == "true" } - return ctx + return flags } func rubyHeaderKey(flag string) string { diff --git a/internal/metadata/featureflag/context_test.go b/internal/metadata/featureflag/context_test.go index 35e85a9c8..8e4e222da 100644 --- a/internal/metadata/featureflag/context_test.go +++ b/internal/metadata/featureflag/context_test.go @@ -119,41 +119,61 @@ func TestGRPCMetadataFeatureFlag(t *testing.T) { } } -func TestAllEnabledFlags(t *testing.T) { - flags := map[string]string{ - ffPrefix + "meow": "true", - ffPrefix + "foo": "true", - ffPrefix + "woof": "false", // not enabled - ffPrefix + "bar": "TRUE", // not enabled +func TestFromContext(t *testing.T) { + defer func(old map[string]FeatureFlag) { + flagsByName = old + }(flagsByName) + + defaultDisabledFlag := FeatureFlag{ + Name: "default_disabled", + OnByDefault: false, } - ctx := metadata.NewIncomingContext(createContext(), metadata.New(flags)) - require.ElementsMatch(t, AllFlags(ctx), []string{"meow:true", "foo:true", "woof:false", "bar:TRUE"}) -} - -func TestRaw(t *testing.T) { - enabledFlag := FeatureFlag{Name: "enabled-flag"} - disabledFlag := FeatureFlag{Name: "disabled-flag"} + defaultEnabledFlag := FeatureFlag{ + Name: "default_enabled", + OnByDefault: true, + } - raw := Raw{ - ffPrefix + enabledFlag.Name: "true", - ffPrefix + disabledFlag.Name: "false", + flagsByName = map[string]FeatureFlag{ + "default_disabled": defaultDisabledFlag, + "default_enabled": defaultEnabledFlag, } - t.Run("RawFromContext", func(t *testing.T) { - ctx := createContext() - ctx = IncomingCtxWithFeatureFlag(ctx, enabledFlag, true) - ctx = IncomingCtxWithFeatureFlag(ctx, disabledFlag, false) + t.Run("with no defined flags", func(t *testing.T) { + require.Empty(t, FromContext(createContext())) + }) + + t.Run("with single defined flag", func(t *testing.T) { + ctx := ContextWithFeatureFlag(createContext(), defaultDisabledFlag, false) + ctx = ContextWithFeatureFlag(ctx, defaultEnabledFlag, true) - require.Equal(t, raw, RawFromContext(ctx)) + require.Equal(t, map[FeatureFlag]bool{ + defaultDisabledFlag: false, + defaultEnabledFlag: true, + }, FromContext(ctx)) }) - t.Run("OutgoingWithRaw", func(t *testing.T) { - outgoingMD, ok := metadata.FromOutgoingContext(OutgoingWithRaw(createContext(), raw)) - require.True(t, ok) - require.Equal(t, metadata.MD{ - ffPrefix + enabledFlag.Name: {"true"}, - ffPrefix + disabledFlag.Name: {"false"}, - }, outgoingMD) + t.Run("with defined flags and non-default values", func(t *testing.T) { + ctx := ContextWithFeatureFlag(createContext(), defaultDisabledFlag, true) + ctx = ContextWithFeatureFlag(ctx, defaultEnabledFlag, false) + + require.Equal(t, map[FeatureFlag]bool{ + defaultDisabledFlag: true, + defaultEnabledFlag: false, + }, FromContext(ctx)) + }) + + t.Run("with defined and undefined flag", func(t *testing.T) { + undefinedFlag := FeatureFlag{ + Name: "undefined_flag", + } + + ctx := ContextWithFeatureFlag(createContext(), defaultDisabledFlag, false) + ctx = ContextWithFeatureFlag(ctx, undefinedFlag, false) + + require.Equal(t, map[FeatureFlag]bool{ + defaultDisabledFlag: false, + undefinedFlag: false, + }, FromContext(ctx)) }) } diff --git a/internal/metadata/featureflag/featureflag.go b/internal/metadata/featureflag/featureflag.go index 70f90d2d9..e2c91568f 100644 --- a/internal/metadata/featureflag/featureflag.go +++ b/internal/metadata/featureflag/featureflag.go @@ -32,31 +32,22 @@ var ( []string{"flag", "enabled"}, ) - // All is the list of all registered feature flags. - All = []FeatureFlag{} + // flagsByName is the set of defined feature flags mapped by their respective name. + flagsByName = map[string]FeatureFlag{} ) -const explicitFeatureFlagKey = "require_explicit_feature_flag_checks" +const ( + // ffPrefix is the prefix used for Gitaly-scoped feature flags. + ffPrefix = "gitaly-feature-" +) -func injectIntoIncomingAndOutgoingContext(ctx context.Context, key string, enabled bool) context.Context { - incomingMD, ok := metadata.FromIncomingContext(ctx) - if !ok { - incomingMD = metadata.New(map[string]string{}) +// DefinedFlags returns the set of feature flags that have been explicitly defined. +func DefinedFlags() []FeatureFlag { + flags := make([]FeatureFlag, 0, len(flagsByName)) + for _, flag := range flagsByName { + flags = append(flags, flag) } - - incomingMD.Set(key, strconv.FormatBool(enabled)) - - ctx = metadata.NewIncomingContext(ctx, incomingMD) - - return metadata.AppendToOutgoingContext(ctx, key, strconv.FormatBool(enabled)) -} - -// ContextWithExplicitFeatureFlags marks the context such that all feature flags which are checked -// must have been explicitly set in that context. If a feature flag wasn't set to an explicit value, -// then checking this feature flag will panic. This is not for use in production systems, but is -// intended for tests to verify that we test each feature flag properly. -func ContextWithExplicitFeatureFlags(ctx context.Context) context.Context { - return injectIntoIncomingAndOutgoingContext(ctx, explicitFeatureFlagKey, true) + return flags } // FeatureFlag gates the implementation of new or changed functionality. @@ -73,14 +64,56 @@ type FeatureFlag struct { // input that are not used for anything but only for the sake of linking to the feature flag rollout // issue in the Gitaly project. func NewFeatureFlag(name, version, rolloutIssueURL string, onByDefault bool) FeatureFlag { + if strings.ContainsAny(name, "-:") { + // It is critical that feature flags don't contain either a dash nor a colon: + // + // - We convert dashes to underscores when converting a feature flag's name to the + // metadata key, and vice versa. The name wouldn't round-trip in case it had + // underscores and must thus use dashes instead. + // - We use colons to separate feature flags and their values. + panic("Feature flags must not contain dashes or colons.") + } + featureFlag := FeatureFlag{ Name: name, OnByDefault: onByDefault, } - All = append(All, featureFlag) + + flagsByName[name] = featureFlag + return featureFlag } +// FromMetadataKey parses the given gRPC metadata key into a Gitaly feature flag and performs the +// necessary conversions. Returns an error in case the metadata does not refer to a feature flag. +// +// This function tries to look up the default value via our set of flag definitions. In case the +// flag definition is unknown to Gitaly it assumes a default value of `false`. +func FromMetadataKey(metadataKey string) (FeatureFlag, error) { + if !strings.HasPrefix(metadataKey, ffPrefix) { + return FeatureFlag{}, fmt.Errorf("not a feature flag: %q", metadataKey) + } + + flagName := strings.TrimPrefix(metadataKey, ffPrefix) + flagName = strings.ReplaceAll(flagName, "-", "_") + + flag, ok := flagsByName[flagName] + if !ok { + flag = FeatureFlag{ + Name: flagName, + OnByDefault: false, + } + } + + return flag, nil +} + +// FormatWithValue converts the feature flag into a string with the given state. Note that this +// function uses the feature flag name and not the raw metadata key as used in gRPC metadata. +func (ff FeatureFlag) FormatWithValue(enabled bool) string { + return fmt.Sprintf("%s:%v", ff.Name, enabled) +} + // IsEnabled checks if the feature flag is enabled for the passed context. // Only returns true if the metadata for the feature flag is set to "true" func (ff FeatureFlag) IsEnabled(ctx context.Context) bool { diff --git a/internal/metadata/featureflag/featureflag_test.go b/internal/metadata/featureflag/featureflag_test.go index 8944e21fd..7a5d5915b 100644 --- a/internal/metadata/featureflag/featureflag_test.go +++ b/internal/metadata/featureflag/featureflag_test.go @@ -1,12 +1,74 @@ package featureflag import ( + "fmt" "testing" "github.com/stretchr/testify/require" "google.golang.org/grpc/metadata" ) +func TestFeatureFlag_FromMetadataKey(t *testing.T) { + defer func(old map[string]FeatureFlag) { + flagsByName = old + }(flagsByName) + + defaultEnabled := FeatureFlag{ + Name: "default_enabled", + OnByDefault: true, + } + defaultDisabled := FeatureFlag{ + Name: "default_disabled", + OnByDefault: false, + } + + flagsByName = map[string]FeatureFlag{ + defaultEnabled.Name: defaultEnabled, + defaultDisabled.Name: defaultDisabled, + } + + for _, tc := range []struct { + desc string + metadataKey string + expectedErr error + expectedFlag FeatureFlag + }{ + { + desc: "empty key", + metadataKey: "", + expectedErr: fmt.Errorf("not a feature flag: \"\""), + }, + { + desc: "invalid prefix", + metadataKey: "invalid-prefix", + expectedErr: fmt.Errorf("not a feature flag: \"invalid-prefix\""), + }, + { + desc: "default enabled flag", + metadataKey: defaultEnabled.MetadataKey(), + expectedFlag: defaultEnabled, + }, + { + desc: "default disabled flag", + metadataKey: defaultDisabled.MetadataKey(), + expectedFlag: defaultDisabled, + }, + { + desc: "undefined flag", + metadataKey: "gitaly-feature-not-defined", + expectedFlag: FeatureFlag{ + Name: "not_defined", + }, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + flag, err := FromMetadataKey(tc.metadataKey) + require.Equal(t, tc.expectedErr, err) + require.Equal(t, tc.expectedFlag, flag) + }) + } +} + func TestFeatureFlag_enabled(t *testing.T) { for _, tc := range []struct { desc string diff --git a/internal/metadata/featureflag/ff_go_language_stats.go b/internal/metadata/featureflag/ff_go_language_stats.go new file mode 100644 index 000000000..cf4626a06 --- /dev/null +++ b/internal/metadata/featureflag/ff_go_language_stats.go @@ -0,0 +1,10 @@ +package featureflag + +// GoLanguageStats flag enables getting CommitLanguages statistics written in +// Go. +var GoLanguageStats = NewFeatureFlag( + "go_language_stats", + "v15.2.0", + "https://gitlab.com/gitlab-org/gitaly/-/issues/4254", + false, +) diff --git a/internal/middleware/featureflag/featureflag_handler.go b/internal/middleware/featureflag/featureflag_handler.go index 33c3c046d..36e64edc9 100644 --- a/internal/middleware/featureflag/featureflag_handler.go +++ b/internal/middleware/featureflag/featureflag_handler.go @@ -26,11 +26,13 @@ func StreamInterceptor(srv interface{}, stream grpc.ServerStream, _ *grpc.Stream // track adds the list of the feature flags into the logging context. // The list is sorted by feature flag name to produce consistent output. func track(ctx context.Context) { - flags := featureflag.AllFlags(ctx) - if len(flags) != 0 { - sort.Slice(flags, func(i, j int) bool { - return flags[i][:strings.Index(flags[i], featureflag.Delim)] < flags[j][:strings.Index(flags[j], featureflag.Delim)] - }) - ctxlogrus.AddFields(ctx, logrus.Fields{"feature_flags": strings.Join(flags, " ")}) + var flagsWithValue []string + for flag, value := range featureflag.FromContext(ctx) { + flagsWithValue = append(flagsWithValue, flag.FormatWithValue(value)) + } + sort.Strings(flagsWithValue) + + if len(flagsWithValue) != 0 { + ctxlogrus.AddFields(ctx, logrus.Fields{"feature_flags": strings.Join(flagsWithValue, " ")}) } } diff --git a/internal/praefect/checks_test.go b/internal/praefect/checks_test.go index cddaa7849..2c1c3c147 100644 --- a/internal/praefect/checks_test.go +++ b/internal/praefect/checks_test.go @@ -535,7 +535,7 @@ func TestNewClockSyncCheck(t *testing.T) { return true, nil }, setup: func(t *testing.T) { - testhelper.ModifyEnvironment(t, "NTP_HOST", "custom") + t.Setenv("NTP_HOST", "custom") }, }, } { diff --git a/internal/praefect/coordinator.go b/internal/praefect/coordinator.go index 38e61c0dc..6f6d439b6 100644 --- a/internal/praefect/coordinator.go +++ b/internal/praefect/coordinator.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strconv" "sync" "time" @@ -589,8 +588,6 @@ func (c *Coordinator) maintenanceStreamParameters(ctx context.Context, call grpc // streamParametersContexts converts the contexts with incoming metadata into a context that is // usable by peer Gitaly nodes. func streamParametersContext(ctx context.Context) context.Context { - outgoingCtx := metadata.IncomingToOutgoing(ctx) - // When upgrading Gitaly nodes where the upgrade contains feature flag default changes, then // there will be a window where a subset of Gitaly nodes has a different understanding of // the current default value. If the feature flag wasn't set explicitly on upgrade by @@ -614,17 +611,20 @@ func streamParametersContext(ctx context.Context) context.Context { // Praefect. But given that feature flags should be introduced with a default value of // `false` to account for zero-dodwntime upgrades, the view would also be consistent in that // case. - rawFeatureFlags := featureflag.RawFromContext(ctx) - if rawFeatureFlags == nil { - rawFeatureFlags = map[string]string{} + + explicitlySetFlags := map[string]bool{} + for flag := range featureflag.FromContext(ctx) { + explicitlySetFlags[flag.Name] = true } - for _, ff := range featureflag.All { - if _, ok := rawFeatureFlags[ff.MetadataKey()]; !ok { - rawFeatureFlags[ff.MetadataKey()] = strconv.FormatBool(ff.OnByDefault) + outgoingCtx := metadata.IncomingToOutgoing(ctx) + for _, flag := range featureflag.DefinedFlags() { + if !explicitlySetFlags[flag.Name] { + outgoingCtx = featureflag.OutgoingCtxWithFeatureFlag( + outgoingCtx, flag, flag.OnByDefault, + ) } } - outgoingCtx = featureflag.OutgoingWithRaw(outgoingCtx, rawFeatureFlags) return outgoingCtx } diff --git a/internal/praefect/coordinator_test.go b/internal/praefect/coordinator_test.go index 0b8201f33..38e26a9f0 100644 --- a/internal/praefect/coordinator_test.go +++ b/internal/praefect/coordinator_test.go @@ -2689,8 +2689,8 @@ func TestNewRequestFinalizer_contextIsDisjointedFromTheRPC(t *testing.T) { func TestStreamParametersContext(t *testing.T) { // Because we're using NewFeatureFlag, they'll end up in the All array. - enabledFF := featureflag.NewFeatureFlag("default-enabled", "", "", true) - disabledFF := featureflag.NewFeatureFlag("default-disabled", "", "", false) + enabledFF := featureflag.NewFeatureFlag("default_enabled", "", "", true) + disabledFF := featureflag.NewFeatureFlag("default_disabled", "", "", false) type expectedFlag struct { flag featureflag.FeatureFlag @@ -2699,7 +2699,7 @@ func TestStreamParametersContext(t *testing.T) { expectedFlags := func(overrides ...expectedFlag) []expectedFlag { flagValues := map[featureflag.FeatureFlag]bool{} - for _, flag := range featureflag.All { + for _, flag := range featureflag.DefinedFlags() { flagValues[flag] = flag.OnByDefault } for _, override := range overrides { @@ -2803,10 +2803,6 @@ func TestStreamParametersContext(t *testing.T) { ), expectedOutgoingMD: metadata.Join( metadataForFlags(expectedFlags()), - metadata.Pairs( - enabledFF.MetadataKey(), "true", - disabledFF.MetadataKey(), "false", - ), ), expectedFlags: expectedFlags(), }, @@ -2827,10 +2823,6 @@ func TestStreamParametersContext(t *testing.T) { expectedFlag{flag: enabledFF, enabled: false}, expectedFlag{flag: disabledFF, enabled: true}, )), - metadata.Pairs( - enabledFF.MetadataKey(), "false", - disabledFF.MetadataKey(), "true", - ), ), expectedFlags: expectedFlags( expectedFlag{flag: enabledFF, enabled: false}, @@ -2856,7 +2848,6 @@ func TestStreamParametersContext(t *testing.T) { expectedFlag{flag: disabledFF, enabled: true}, )), metadata.Pairs( - disabledFF.MetadataKey(), "true", "incoming", "value", ), ), diff --git a/internal/supervisor/supervisor_test.go b/internal/supervisor/supervisor_test.go index f7bb2e990..0dd3d3658 100644 --- a/internal/supervisor/supervisor_test.go +++ b/internal/supervisor/supervisor_test.go @@ -179,7 +179,7 @@ func TestNewConfigFromEnv(t *testing.T) { } { t.Run(tc.desc, func(t *testing.T) { for key, value := range tc.envvars { - testhelper.ModifyEnvironment(t, key, value) + t.Setenv(key, value) } config, err := NewConfigFromEnv() diff --git a/internal/testhelper/featureset_test.go b/internal/testhelper/featureset_test.go index ddbb8652c..f3311b7d2 100644 --- a/internal/testhelper/featureset_test.go +++ b/internal/testhelper/featureset_test.go @@ -171,15 +171,12 @@ func TestNewFeatureSetsWithRubyFlags(t *testing.T) { } func TestFeatureSets_Run(t *testing.T) { - // This test depends on feature flags being default-enabled in the test - // context, which requires those flags to exist in the ff.All slice. So - // let's just append them here so we do not need to use a "real" - // feature flag, as that would require constant change when we remove - // old feature flags. - defer func(old []ff.FeatureFlag) { - ff.All = old - }(ff.All) - ff.All = append(ff.All, featureFlagA, featureFlagB) + // Define two default-enabled feature flags. Note that with `NewFeatureFlag()`, we + // automatically add them to the list of defined feature flags. While this is stateful and + // would theoretically also impact other tests, we don't really need to mind that given + // that we use test-specific names for the flags here. + featureFlagA := ff.NewFeatureFlag("global_feature_flag_a", "", "", true) + featureFlagB := ff.NewFeatureFlag("global_feature_flag_b", "", "", true) var featureFlags [][2]bool NewFeatureSets(featureFlagB, featureFlagA).Run(t, func(t *testing.T, ctx context.Context) { diff --git a/internal/testhelper/testcfg/build.go b/internal/testhelper/testcfg/build.go index f4e6be180..b49b8f47b 100644 --- a/internal/testhelper/testcfg/build.go +++ b/internal/testhelper/testcfg/build.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "sync" "testing" @@ -116,6 +117,30 @@ func BuildBinary(t testing.TB, targetDir, sourcePath string) string { "PATH=%s:%s", filepath.Dir(gitExecEnv.BinaryPath), os.Getenv("PATH"), )) + // Go 1.18 has started to extract VCS information so that it can be embedded into + // the resulting binary and will thus execute Git in the Gitaly repository. In CI, + // the Gitaly repository is owned by a different user than the one that is executing + // tests though, which means that Git will refuse to open the repository because of + // CVE-2022-24765. + // + // Let's override this mechanism by labelling the Git repository as safe. While this + // does in theory make us vulnerable to this exploit, it is clear that any adversary + // would already have arbitrary code execution because we are executing code right + // now that would be controlled by the very same adversary. + // + // Note that we cannot pass `safe.directory` via command line arguments by design. + // Instead, we just override the system-level gitconfig to point to a temporary file + // that contains this setting. + _, currentFile, _, ok := runtime.Caller(0) + require.True(t, ok) + gitconfigPath := filepath.Join(testhelper.TempDir(t), "gitconfig") + require.NoError(t, os.WriteFile(gitconfigPath, []byte( + "[safe]\ndirectory = "+filepath.Join(filepath.Dir(currentFile), "..", "..", "..")+"\n"), 0o400), + ) + gitEnvironment = append(gitEnvironment, + "GIT_CONFIG_SYSTEM="+gitconfigPath, + ) + buildTags := []string{ "static", "system_libgit2", "gitaly_test", } diff --git a/internal/testhelper/testhelper.go b/internal/testhelper/testhelper.go index dacf805b3..e917a7b79 100644 --- a/internal/testhelper/testhelper.go +++ b/internal/testhelper/testhelper.go @@ -267,21 +267,19 @@ func WriteExecutable(t testing.TB, path string, content []byte) string { return path } -// ModifyEnvironment will change an environment variable and revert it when the test completed. -func ModifyEnvironment(t testing.TB, key string, value string) { +// Unsetenv unsets an environment variable. The variable will be restored after the test has +// finished. +func Unsetenv(t testing.TB, key string) { t.Helper() - oldValue, hasOldValue := os.LookupEnv(key) - if value == "" { - require.NoError(t, os.Unsetenv(key)) - t.Cleanup(func() { - if hasOldValue { - require.NoError(t, os.Setenv(key, oldValue)) - } - }) - } else { - t.Setenv(key, value) - } + // We're first using `t.Setenv()` here due to two reasons: first, it will automitcally + // handle restoring the environment variable for us after the test has finished. And second, + // it performs a check whether we're running with `t.Parallel()`. + t.Setenv(key, "") + + // And now we can unset the environment variable given that we know we're not running in a + // parallel test and where the cleanup function has been installed. + require.NoError(t, os.Unsetenv(key)) } // GenerateCerts creates a certificate that can be used to establish TLS protected TCP connection. diff --git a/internal/testhelper/testserver/gitaly.go b/internal/testhelper/testserver/gitaly.go index 0313ba34c..54cf7f155 100644 --- a/internal/testhelper/testserver/gitaly.go +++ b/internal/testhelper/testserver/gitaly.go @@ -1,6 +1,7 @@ package testserver import ( + "context" "net" "os" "testing" @@ -82,7 +83,7 @@ func runPraefectProxy(t testing.TB, gitalyCfg config.Cfg, gitalyAddr string) Pra Replication: praefectconfig.DefaultReplicationConfig(), Logging: gitalylog.Config{ Format: "json", - Level: "panic", + Level: "info", }, VirtualStorages: []*praefectconfig.VirtualStorage{ { @@ -119,9 +120,8 @@ func (gs GitalyServer) Address() string { return gs.address } -// waitHealthy waits until the server hosted at address becomes healthy. Times out after a fixed -// amount of time. -func waitHealthy(t testing.TB, addr string, authToken string) { +// waitHealthy waits until the server hosted at address becomes healthy. +func waitHealthy(ctx context.Context, t testing.TB, addr string, authToken string) { grpcOpts := []grpc.DialOption{ grpc.WithBlock(), } @@ -129,8 +129,6 @@ func waitHealthy(t testing.TB, addr string, authToken string) { grpcOpts = append(grpcOpts, grpc.WithPerRPCCredentials(gitalyauth.RPCCredentialsV2(authToken))) } - ctx := testhelper.Context(t) - conn, err := client.DialContext(ctx, addr, grpcOpts) require.NoError(t, err) defer testhelper.MustClose(t, conn) @@ -178,7 +176,8 @@ func runGitaly(t testing.TB, cfg config.Cfg, rubyServer *rubyserver.Server, regi assert.NoError(t, internalServer.Serve(internalListener), "failure to serve internal gRPC") }() - waitHealthy(t, "unix://"+internalListener.Addr().String(), cfg.Auth.Token) + ctx := testhelper.Context(t) + waitHealthy(ctx, t, "unix://"+internalListener.Addr().String(), cfg.Auth.Token) } externalServer, err := serverFactory.CreateExternal(cfg.TLS.CertPath != "" && cfg.TLS.KeyPath != "") @@ -212,7 +211,8 @@ func runGitaly(t testing.TB, cfg config.Cfg, rubyServer *rubyserver.Server, regi assert.NoError(t, externalServer.Serve(listener), "failure to serve external gRPC") }() - waitHealthy(t, addr, cfg.Auth.Token) + ctx := testhelper.Context(t) + waitHealthy(ctx, t, addr, cfg.Auth.Token) return externalServer, addr, gsd.disablePraefect } diff --git a/internal/testhelper/testserver/praefect.go b/internal/testhelper/testserver/praefect.go index 2f433dd4c..98eb7f89f 100644 --- a/internal/testhelper/testserver/praefect.go +++ b/internal/testhelper/testserver/praefect.go @@ -1,11 +1,14 @@ package testserver import ( + "bytes" + "context" "net" "os" "os/exec" "path/filepath" "testing" + "time" "github.com/pelletier/go-toml" "github.com/stretchr/testify/require" @@ -76,7 +79,9 @@ func StartPraefect(t testing.TB, cfg config.Config) PraefectServer { }) cmd := exec.Command(binaryPath, "-config", configFilePath) - cmd.Stderr = os.Stderr + // Logs are written to stderr, we can ignore stdout. + var stderr bytes.Buffer + cmd.Stderr = &stderr cmd.Stdout = os.Stdout require.NoError(t, cmd.Start()) @@ -90,7 +95,23 @@ func StartPraefect(t testing.TB, cfg config.Config) PraefectServer { } t.Cleanup(praefectServer.Shutdown) - waitHealthy(t, praefectServer.Address(), cfg.Auth.Token) + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + + // Ensure this runs even if context ends in waitHealthy. + defer func() { + select { + case <-ctx.Done(): + switch ctx.Err() { + case context.DeadlineExceeded: + // Capture Praefect logs when waitHealthy takes too long. + t.Errorf("Failed to connect to Praefect:\n%v", stderr.String()) + } + default: + } + }() + + waitHealthy(ctx, t, praefectServer.Address(), cfg.Auth.Token) return praefectServer } diff --git a/proto/commit.proto b/proto/commit.proto index 4aaa7f086..36143dd28 100644 --- a/proto/commit.proto +++ b/proto/commit.proto @@ -99,7 +99,8 @@ service CommitService { }; } - // This comment is left unintentionally blank. + // CommitLanguages detects the source code languages of the whole tree for a + // given commit. Returns an error in case no languages could be detected. rpc CommitLanguages(CommitLanguagesRequest) returns (CommitLanguagesResponse) { option (op_type) = { op: ACCESSOR @@ -451,6 +452,8 @@ message GetTreeEntriesRequest { // The page token is the last commit OID that was sent. It's expected to be the // full object ID to guard against ambigious OIDs. PaginationParameter pagination_params = 6; + // SkipFlatPath is an option to skip the expensive operation of populating flat paths. + bool skip_flat_paths = 7; } // This comment is left unintentionally blank. @@ -609,31 +612,35 @@ message FindCommitsResponse { repeated GitCommit commits = 1; } -// This comment is left unintentionally blank. +// CommitLanguagesRequest requests to detect the source code languages. message CommitLanguagesRequest { - // This comment is left unintentionally blank. + // Repository is the repository where to detect the languages in. Repository repository = 1 [(target_repository)=true]; - // This comment is left unintentionally blank. + // Revision tells for which commit the languages should be detected. If it's + // omitted, the HEAD commit of the default branch is used. bytes revision = 2; } -// This comment is left unintentionally blank. +// CommitLanguagesResponse returns the language statistics. message CommitLanguagesResponse { - // This comment is left unintentionally blank. + // Language specifies the statistics for one language. message Language { - // This comment is left unintentionally blank. + // Name is the name of the detected language, for example: Ruby, Go, HTML + // A full list of language names can be found at: + // https://github.com/github/linguist/blob/master/lib/linguist/languages.yml string name = 1; - // This comment is left unintentionally blank. + // Share is the percentual share (value between 0 and 100) of this language + // in relation to other languages that exist in the given revision. float share = 2; - // This comment is left unintentionally blank. + // Color specifies the associated color for this language, for example #3fd5e0. string color = 3; - // This comment is left unintentionally blank. + // FileCount tells how many files with this language are found. uint32 file_count = 4; - // This comment is left unintentionally blank. + // Bytes is the total amount of bytes written in this language uint64 bytes = 5; } - // This comment is left unintentionally blank. + // Languages is a set of all the detected languages and their statistics. repeated Language languages = 1; } diff --git a/proto/go/gitalypb/commit.pb.go b/proto/go/gitalypb/commit.pb.go index 98ff1ce0c..349e7e0c0 100644 --- a/proto/go/gitalypb/commit.pb.go +++ b/proto/go/gitalypb/commit.pb.go @@ -1506,6 +1506,8 @@ type GetTreeEntriesRequest struct { // The page token is the last commit OID that was sent. It's expected to be the // full object ID to guard against ambigious OIDs. PaginationParams *PaginationParameter `protobuf:"bytes,6,opt,name=pagination_params,json=paginationParams,proto3" json:"pagination_params,omitempty"` + // SkipFlatPath is an option to skip the expensive operation of populating flat paths. + SkipFlatPaths bool `protobuf:"varint,7,opt,name=skip_flat_paths,json=skipFlatPaths,proto3" json:"skip_flat_paths,omitempty"` } func (x *GetTreeEntriesRequest) Reset() { @@ -1582,6 +1584,13 @@ func (x *GetTreeEntriesRequest) GetPaginationParams() *PaginationParameter { return nil } +func (x *GetTreeEntriesRequest) GetSkipFlatPaths() bool { + if x != nil { + return x.SkipFlatPaths + } + return false +} + // This comment is left unintentionally blank. type GetTreeEntriesResponse struct { state protoimpl.MessageState @@ -2444,15 +2453,16 @@ func (x *FindCommitsResponse) GetCommits() []*GitCommit { return nil } -// This comment is left unintentionally blank. +// CommitLanguagesRequest requests to detect the source code languages. type CommitLanguagesRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // This comment is left unintentionally blank. + // Repository is the repository where to detect the languages in. Repository *Repository `protobuf:"bytes,1,opt,name=repository,proto3" json:"repository,omitempty"` - // This comment is left unintentionally blank. + // Revision tells for which commit the languages should be detected. If it's + // omitted, the HEAD commit of the default branch is used. Revision []byte `protobuf:"bytes,2,opt,name=revision,proto3" json:"revision,omitempty"` } @@ -2502,13 +2512,13 @@ func (x *CommitLanguagesRequest) GetRevision() []byte { return nil } -// This comment is left unintentionally blank. +// CommitLanguagesResponse returns the language statistics. type CommitLanguagesResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // This comment is left unintentionally blank. + // Languages is a set of all the detected languages and their statistics. Languages []*CommitLanguagesResponse_Language `protobuf:"bytes,1,rep,name=languages,proto3" json:"languages,omitempty"` } @@ -3754,21 +3764,24 @@ func (x *ListCommitsByRefNameResponse_CommitForRef) GetRefName() []byte { return nil } -// This comment is left unintentionally blank. +// Language specifies the statistics for one language. type CommitLanguagesResponse_Language struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // This comment is left unintentionally blank. + // Name is the name of the detected language, for example: Ruby, Go, HTML + // A full list of language names can be found at: + // https://github.com/github/linguist/blob/master/lib/linguist/languages.yml Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // This comment is left unintentionally blank. + // Share is the percentual share (value between 0 and 100) of this language + // in relation to other languages that exist in the given revision. Share float32 `protobuf:"fixed32,2,opt,name=share,proto3" json:"share,omitempty"` - // This comment is left unintentionally blank. + // Color specifies the associated color for this language, for example #3fd5e0. Color string `protobuf:"bytes,3,opt,name=color,proto3" json:"color,omitempty"` - // This comment is left unintentionally blank. + // FileCount tells how many files with this language are found. FileCount uint32 `protobuf:"varint,4,opt,name=file_count,json=fileCount,proto3" json:"file_count,omitempty"` - // This comment is left unintentionally blank. + // Bytes is the total amount of bytes written in this language Bytes uint64 `protobuf:"varint,5,opt,name=bytes,proto3" json:"bytes,omitempty"` } @@ -4129,7 +4142,7 @@ var file_commit_proto_rawDesc = []byte{ 0x08, 0x66, 0x6c, 0x61, 0x74, 0x50, 0x61, 0x74, 0x68, 0x22, 0x2b, 0x0a, 0x09, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x42, 0x4c, 0x4f, 0x42, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x54, 0x52, 0x45, 0x45, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x4f, - 0x4d, 0x4d, 0x49, 0x54, 0x10, 0x03, 0x22, 0xcb, 0x02, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x54, 0x72, + 0x4d, 0x4d, 0x49, 0x54, 0x10, 0x03, 0x22, 0xf3, 0x02, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x54, 0x72, 0x65, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, @@ -4147,451 +4160,453 @@ var file_commit_proto_rawDesc = []byte{ 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x10, 0x70, 0x61, 0x67, 0x69, - 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x22, 0x26, 0x0a, 0x06, - 0x53, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, - 0x54, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x54, 0x52, 0x45, 0x45, 0x53, 0x5f, 0x46, 0x49, 0x52, - 0x53, 0x54, 0x10, 0x01, 0x22, 0x8c, 0x01, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x54, 0x72, 0x65, 0x65, - 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x2b, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x11, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x54, 0x72, 0x65, 0x65, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x45, 0x0a, 0x11, - 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x75, 0x72, 0x73, 0x6f, - 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, - 0x2e, 0x50, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, - 0x72, 0x52, 0x10, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, - 0x73, 0x6f, 0x72, 0x22, 0x68, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, - 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, - 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, - 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, - 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x29, 0x0a, - 0x11, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0c, 0x52, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x22, 0x85, 0x01, 0x0a, 0x11, 0x46, 0x69, 0x6e, - 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, - 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, - 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, - 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x73, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x74, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x73, - 0x22, 0x3f, 0x0a, 0x12, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, - 0x47, 0x69, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x69, - 0x74, 0x22, 0x65, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, - 0x42, 0x79, 0x4f, 0x69, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, - 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, - 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, - 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6f, 0x69, 0x64, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x03, 0x6f, 0x69, 0x64, 0x22, 0x47, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, - 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, 0x4f, 0x69, 0x64, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, - 0x69, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, - 0x73, 0x22, 0x74, 0x0a, 0x1b, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, - 0x42, 0x79, 0x52, 0x65, 0x66, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, - 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, - 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, - 0x66, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x08, 0x72, - 0x65, 0x66, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x22, 0xce, 0x01, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, - 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, 0x52, 0x65, 0x66, 0x4e, 0x61, 0x6d, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x6d, - 0x69, 0x74, 0x5f, 0x72, 0x65, 0x66, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, - 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, - 0x74, 0x73, 0x42, 0x79, 0x52, 0x65, 0x66, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x52, 0x65, 0x66, - 0x52, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x66, 0x73, 0x1a, 0x54, 0x0a, 0x0c, - 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x52, 0x65, 0x66, 0x12, 0x29, 0x0a, 0x06, - 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, - 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x69, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, - 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x65, 0x66, 0x5f, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x72, 0x65, 0x66, 0x4e, 0x61, - 0x6d, 0x65, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 0x80, 0x02, 0x0a, 0x15, 0x46, 0x69, 0x6e, - 0x64, 0x41, 0x6c, 0x6c, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, - 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, - 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1a, 0x0a, 0x08, - 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, - 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x6d, 0x61, 0x78, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6b, 0x69, 0x70, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x04, 0x73, 0x6b, 0x69, 0x70, 0x12, 0x39, 0x0a, 0x05, 0x6f, 0x72, 0x64, - 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, - 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x41, 0x6c, 0x6c, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x05, 0x6f, - 0x72, 0x64, 0x65, 0x72, 0x22, 0x25, 0x0a, 0x05, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x08, 0x0a, - 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x54, 0x4f, 0x50, 0x4f, 0x10, - 0x01, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x41, 0x54, 0x45, 0x10, 0x02, 0x22, 0x45, 0x0a, 0x16, 0x46, - 0x69, 0x6e, 0x64, 0x41, 0x6c, 0x6c, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, - 0x47, 0x69, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, - 0x74, 0x73, 0x22, 0xec, 0x04, 0x0a, 0x12, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x69, - 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, - 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, - 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, - 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, - 0x6f, 0x72, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, - 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, - 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x14, 0x0a, - 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x05, 0x70, 0x61, - 0x74, 0x68, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x12, 0x1f, 0x0a, 0x0b, 0x73, - 0x6b, 0x69, 0x70, 0x5f, 0x6d, 0x65, 0x72, 0x67, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0a, 0x73, 0x6b, 0x69, 0x70, 0x4d, 0x65, 0x72, 0x67, 0x65, 0x73, 0x12, 0x21, 0x0a, 0x0c, - 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x77, 0x61, 0x6c, 0x6b, 0x18, 0x08, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x57, 0x61, 0x6c, 0x6b, 0x12, - 0x30, 0x0a, 0x05, 0x61, 0x66, 0x74, 0x65, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x61, 0x66, 0x74, 0x65, - 0x72, 0x12, 0x32, 0x0a, 0x06, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, 0x62, - 0x65, 0x66, 0x6f, 0x72, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x0b, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x69, 0x72, 0x73, 0x74, - 0x5f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x66, - 0x69, 0x72, 0x73, 0x74, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x61, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x12, 0x36, 0x0a, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x20, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x43, - 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4f, 0x72, - 0x64, 0x65, 0x72, 0x52, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x3c, 0x0a, 0x0e, 0x67, 0x6c, - 0x6f, 0x62, 0x61, 0x6c, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0f, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x6c, 0x6f, 0x62, - 0x61, 0x6c, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0d, 0x67, 0x6c, 0x6f, 0x62, 0x61, - 0x6c, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x72, 0x61, 0x69, - 0x6c, 0x65, 0x72, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x74, 0x72, 0x61, 0x69, - 0x6c, 0x65, 0x72, 0x73, 0x22, 0x1b, 0x0a, 0x05, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x08, 0x0a, - 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x54, 0x4f, 0x50, 0x4f, 0x10, - 0x01, 0x22, 0x42, 0x0a, 0x13, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, - 0x69, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, 0x69, 0x74, 0x61, - 0x6c, 0x79, 0x2e, 0x47, 0x69, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x07, 0x63, 0x6f, - 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x22, 0x6e, 0x0a, 0x16, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x4c, - 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x26, 0x0a, 0x0f, + 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x66, 0x6c, 0x61, 0x74, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x73, 0x6b, 0x69, 0x70, 0x46, 0x6c, 0x61, 0x74, 0x50, + 0x61, 0x74, 0x68, 0x73, 0x22, 0x26, 0x0a, 0x06, 0x53, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x12, 0x0b, + 0x0a, 0x07, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x54, + 0x52, 0x45, 0x45, 0x53, 0x5f, 0x46, 0x49, 0x52, 0x53, 0x54, 0x10, 0x01, 0x22, 0x8c, 0x01, 0x0a, + 0x16, 0x47, 0x65, 0x74, 0x54, 0x72, 0x65, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, + 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, + 0x79, 0x2e, 0x54, 0x72, 0x65, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x65, 0x6e, 0x74, + 0x72, 0x69, 0x65, 0x73, 0x12, 0x45, 0x0a, 0x11, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x18, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x10, 0x70, 0x61, 0x67, 0x69, 0x6e, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x22, 0x68, 0x0a, 0x10, 0x4c, + 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0xe2, 0x01, 0x0a, 0x17, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, - 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x46, 0x0a, 0x09, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6f, - 0x6d, 0x6d, 0x69, 0x74, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x52, 0x09, - 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x73, 0x1a, 0x7f, 0x0a, 0x08, 0x4c, 0x61, 0x6e, - 0x67, 0x75, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x68, 0x61, - 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x73, 0x68, 0x61, 0x72, 0x65, 0x12, - 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, 0x22, 0x91, 0x01, 0x0a, 0x0f, 0x52, - 0x61, 0x77, 0x42, 0x6c, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, - 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, - 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, - 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x76, 0x69, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, - 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x22, 0x26, - 0x0a, 0x10, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0xed, 0x01, 0x0a, 0x18, 0x4c, 0x61, 0x73, 0x74, 0x43, - 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x50, 0x61, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, - 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, - 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1a, 0x0a, - 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, - 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x29, 0x0a, - 0x10, 0x6c, 0x69, 0x74, 0x65, 0x72, 0x61, 0x6c, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x70, 0x65, - 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6c, 0x69, 0x74, 0x65, 0x72, 0x61, 0x6c, - 0x50, 0x61, 0x74, 0x68, 0x73, 0x70, 0x65, 0x63, 0x12, 0x3c, 0x0a, 0x0e, 0x67, 0x6c, 0x6f, 0x62, - 0x61, 0x6c, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x15, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, - 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0d, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x46, 0x0a, 0x19, 0x4c, 0x61, 0x73, 0x74, 0x43, 0x6f, - 0x6d, 0x6d, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x50, 0x61, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x69, 0x74, - 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x22, 0xa4, - 0x02, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x61, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, - 0x74, 0x73, 0x46, 0x6f, 0x72, 0x54, 0x72, 0x65, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, - 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, - 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, - 0x6d, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, - 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x2d, 0x0a, 0x10, 0x6c, 0x69, 0x74, 0x65, - 0x72, 0x61, 0x6c, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x70, 0x65, 0x63, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x08, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0f, 0x6c, 0x69, 0x74, 0x65, 0x72, 0x61, 0x6c, 0x50, - 0x61, 0x74, 0x68, 0x73, 0x70, 0x65, 0x63, 0x12, 0x3c, 0x0a, 0x0e, 0x67, 0x6c, 0x6f, 0x62, 0x61, - 0x6c, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0d, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xd7, 0x01, 0x0a, 0x1e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x61, - 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x46, 0x6f, 0x72, 0x54, 0x72, 0x65, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, - 0x69, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x67, 0x69, 0x74, 0x61, - 0x6c, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x61, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, - 0x74, 0x73, 0x46, 0x6f, 0x72, 0x54, 0x72, 0x65, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x54, 0x72, 0x65, 0x65, 0x52, - 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x1a, 0x65, 0x0a, 0x0d, 0x43, 0x6f, 0x6d, 0x6d, - 0x69, 0x74, 0x46, 0x6f, 0x72, 0x54, 0x72, 0x65, 0x65, 0x12, 0x29, 0x0a, 0x06, 0x63, 0x6f, 0x6d, - 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, 0x69, 0x74, 0x61, - 0x6c, 0x79, 0x2e, 0x47, 0x69, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x06, 0x63, 0x6f, - 0x6d, 0x6d, 0x69, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x74, 0x68, 0x5f, 0x62, 0x79, 0x74, - 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x61, 0x74, 0x68, 0x42, 0x79, - 0x74, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x22, - 0x85, 0x02, 0x0a, 0x17, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x29, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, + 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x61, + 0x74, 0x68, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, + 0x22, 0x85, 0x01, 0x0a, 0x11, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, + 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, + 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, + 0x74, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, + 0x74, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x73, 0x22, 0x3f, 0x0a, 0x12, 0x46, 0x69, 0x6e, 0x64, + 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, + 0x0a, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, + 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x69, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, + 0x74, 0x52, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x22, 0x65, 0x0a, 0x17, 0x4c, 0x69, 0x73, + 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, 0x4f, 0x69, 0x64, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, + 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, + 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x6f, 0x69, 0x64, + 0x22, 0x47, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, + 0x79, 0x4f, 0x69, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x07, + 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, + 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x69, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, + 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x22, 0x74, 0x0a, 0x1b, 0x4c, 0x69, 0x73, + 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, 0x52, 0x65, 0x66, 0x4e, 0x61, 0x6d, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, + 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x66, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x66, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x22, + 0xce, 0x01, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, + 0x79, 0x52, 0x65, 0x66, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x52, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x72, 0x65, 0x66, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, 0x52, 0x65, 0x66, 0x4e, + 0x61, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, + 0x69, 0x74, 0x46, 0x6f, 0x72, 0x52, 0x65, 0x66, 0x52, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, + 0x52, 0x65, 0x66, 0x73, 0x1a, 0x54, 0x0a, 0x0c, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x46, 0x6f, + 0x72, 0x52, 0x65, 0x66, 0x12, 0x29, 0x0a, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x69, + 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x12, + 0x19, 0x0a, 0x08, 0x72, 0x65, 0x66, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x07, 0x72, 0x65, 0x66, 0x4e, 0x61, 0x6d, 0x65, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, + 0x22, 0x80, 0x02, 0x0a, 0x15, 0x46, 0x69, 0x6e, 0x64, 0x41, 0x6c, 0x6c, 0x43, 0x6f, 0x6d, 0x6d, + 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, + 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x0a, + 0x04, 0x73, 0x6b, 0x69, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x73, 0x6b, 0x69, + 0x70, 0x12, 0x39, 0x0a, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x23, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x41, 0x6c, + 0x6c, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, + 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x22, 0x25, 0x0a, 0x05, + 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, + 0x08, 0x0a, 0x04, 0x54, 0x4f, 0x50, 0x4f, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x41, 0x54, + 0x45, 0x10, 0x02, 0x22, 0x45, 0x0a, 0x16, 0x46, 0x69, 0x6e, 0x64, 0x41, 0x6c, 0x6c, 0x43, 0x6f, + 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, + 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, + 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x69, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, + 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x22, 0xec, 0x04, 0x0a, 0x12, 0x46, + 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, + 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x72, + 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, + 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, + 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x6f, + 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x05, + 0x20, 0x03, 0x28, 0x0c, 0x52, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x66, + 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x66, 0x6f, 0x6c, + 0x6c, 0x6f, 0x77, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x6d, 0x65, 0x72, 0x67, + 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x6b, 0x69, 0x70, 0x4d, 0x65, + 0x72, 0x67, 0x65, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, + 0x77, 0x61, 0x6c, 0x6b, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x57, 0x61, 0x6c, 0x6b, 0x12, 0x30, 0x0a, 0x05, 0x61, 0x66, 0x74, 0x65, 0x72, + 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x05, 0x61, 0x66, 0x74, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x06, 0x62, 0x65, 0x66, + 0x6f, 0x72, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x12, 0x10, 0x0a, + 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x12, + 0x21, 0x0a, 0x0c, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, + 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x66, 0x69, 0x72, 0x73, 0x74, 0x50, 0x61, 0x72, 0x65, + 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x18, 0x0d, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x06, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x12, 0x36, 0x0a, 0x05, 0x6f, 0x72, + 0x64, 0x65, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x67, 0x69, 0x74, 0x61, + 0x6c, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x05, 0x6f, 0x72, 0x64, + 0x65, 0x72, 0x12, 0x3c, 0x0a, 0x0e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x5f, 0x6f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x67, 0x69, 0x74, + 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x52, 0x0d, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x73, 0x18, 0x10, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x08, 0x74, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x73, 0x22, 0x1b, 0x0a, 0x05, + 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, + 0x08, 0x0a, 0x04, 0x54, 0x4f, 0x50, 0x4f, 0x10, 0x01, 0x22, 0x42, 0x0a, 0x13, 0x46, 0x69, 0x6e, + 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x2b, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x11, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x69, 0x74, 0x43, 0x6f, + 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x22, 0x6e, 0x0a, + 0x16, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, + 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, + 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0xe2, 0x01, + 0x0a, 0x17, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x09, 0x6c, 0x61, 0x6e, + 0x67, 0x75, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x67, + 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x4c, 0x61, 0x6e, 0x67, + 0x75, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x4c, 0x61, + 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x52, 0x09, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, + 0x73, 0x1a, 0x7f, 0x0a, 0x08, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x68, 0x61, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x02, + 0x52, 0x05, 0x73, 0x68, 0x61, 0x72, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x6c, 0x6f, 0x72, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x12, 0x1d, 0x0a, + 0x0a, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, + 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x62, 0x79, 0x74, + 0x65, 0x73, 0x22, 0x91, 0x01, 0x0a, 0x0f, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x61, 0x6d, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, + 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, + 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, + 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, + 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x22, 0x26, 0x0a, 0x10, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x61, + 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0xed, + 0x01, 0x0a, 0x18, 0x4c, 0x61, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x46, 0x6f, 0x72, + 0x50, 0x61, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, - 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, - 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, - 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x3c, 0x0a, 0x0e, 0x67, 0x6c, 0x6f, - 0x62, 0x61, 0x6c, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x15, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x6c, 0x6f, 0x62, 0x61, - 0x6c, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0d, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, - 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x47, 0x0a, 0x18, 0x43, 0x6f, 0x6d, 0x6d, 0x69, - 0x74, 0x73, 0x42, 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x69, - 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, - 0x22, 0x6f, 0x0a, 0x1f, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x53, 0x68, 0x61, 0x73, 0x57, 0x69, - 0x74, 0x68, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x29, 0x0a, 0x10, 0x6c, 0x69, 0x74, 0x65, 0x72, 0x61, 0x6c, + 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x70, 0x65, 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0f, 0x6c, 0x69, 0x74, 0x65, 0x72, 0x61, 0x6c, 0x50, 0x61, 0x74, 0x68, 0x73, 0x70, 0x65, 0x63, + 0x12, 0x3c, 0x0a, 0x0e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, + 0x79, 0x2e, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, + 0x0d, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x46, + 0x0a, 0x19, 0x4c, 0x61, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x50, + 0x61, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x06, 0x63, + 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, 0x69, + 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x69, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x06, + 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x22, 0xa4, 0x02, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x4c, + 0x61, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x46, 0x6f, 0x72, 0x54, 0x72, 0x65, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, + 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, + 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x61, + 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, + 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, + 0x12, 0x2d, 0x0a, 0x10, 0x6c, 0x69, 0x74, 0x65, 0x72, 0x61, 0x6c, 0x5f, 0x70, 0x61, 0x74, 0x68, + 0x73, 0x70, 0x65, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0f, + 0x6c, 0x69, 0x74, 0x65, 0x72, 0x61, 0x6c, 0x50, 0x61, 0x74, 0x68, 0x73, 0x70, 0x65, 0x63, 0x12, + 0x3c, 0x0a, 0x0e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, + 0x2e, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0d, + 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xd7, 0x01, + 0x0a, 0x1e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x61, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, + 0x73, 0x46, 0x6f, 0x72, 0x54, 0x72, 0x65, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x4e, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x34, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, + 0x61, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x46, 0x6f, 0x72, 0x54, 0x72, 0x65, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, + 0x46, 0x6f, 0x72, 0x54, 0x72, 0x65, 0x65, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, + 0x1a, 0x65, 0x0a, 0x0d, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x54, 0x72, 0x65, + 0x65, 0x12, 0x29, 0x0a, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x11, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x69, 0x74, 0x43, 0x6f, + 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x12, 0x1d, 0x0a, 0x0a, + 0x70, 0x61, 0x74, 0x68, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x09, 0x70, 0x61, 0x74, 0x68, 0x42, 0x79, 0x74, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x01, 0x10, + 0x02, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x22, 0x85, 0x02, 0x0a, 0x17, 0x43, 0x6f, 0x6d, 0x6d, + 0x69, 0x74, 0x73, 0x42, 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, - 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x12, 0x0a, - 0x04, 0x73, 0x68, 0x61, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x04, 0x73, 0x68, 0x61, - 0x73, 0x22, 0x36, 0x0a, 0x20, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x53, 0x68, 0x61, 0x73, 0x57, - 0x69, 0x74, 0x68, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x68, 0x61, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0c, 0x52, 0x04, 0x73, 0x68, 0x61, 0x73, 0x22, 0x76, 0x0a, 0x1d, 0x45, 0x78, 0x74, + 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1a, 0x0a, + 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, + 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, + 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x71, + 0x75, 0x65, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, + 0x79, 0x12, 0x3c, 0x0a, 0x0e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x5f, 0x6f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x67, 0x69, 0x74, 0x61, + 0x6c, 0x79, 0x2e, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x52, 0x0d, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, + 0x47, 0x0a, 0x18, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x07, 0x63, + 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, + 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x69, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, + 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x22, 0x6f, 0x0a, 0x1f, 0x46, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x53, 0x68, 0x61, 0x73, 0x57, 0x69, 0x74, 0x68, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x68, 0x61, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0c, 0x52, 0x04, 0x73, 0x68, 0x61, 0x73, 0x22, 0x36, 0x0a, 0x20, 0x46, 0x69, 0x6c, + 0x74, 0x65, 0x72, 0x53, 0x68, 0x61, 0x73, 0x57, 0x69, 0x74, 0x68, 0x53, 0x69, 0x67, 0x6e, 0x61, + 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x73, 0x68, 0x61, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x04, 0x73, 0x68, 0x61, + 0x73, 0x22, 0x76, 0x0a, 0x1d, 0x45, 0x78, 0x74, 0x72, 0x61, 0x63, 0x74, 0x43, 0x6f, 0x6d, 0x6d, + 0x69, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, + 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, + 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, + 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x49, 0x64, 0x22, 0x5f, 0x0a, 0x1e, 0x45, 0x78, 0x74, 0x72, 0x61, 0x63, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, - 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, - 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, - 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, - 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x69, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x49, - 0x64, 0x22, 0x5f, 0x0a, 0x1e, 0x45, 0x78, 0x74, 0x72, 0x61, 0x63, 0x74, 0x43, 0x6f, 0x6d, 0x6d, - 0x69, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, - 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x74, 0x65, 0x78, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x65, - 0x78, 0x74, 0x22, 0x75, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x53, - 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, - 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, - 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, - 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, - 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x49, 0x64, 0x73, 0x22, 0x79, 0x0a, 0x1b, 0x47, 0x65, 0x74, - 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x6d, - 0x69, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x6d, - 0x6d, 0x69, 0x74, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, - 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x74, 0x65, - 0x78, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, - 0x54, 0x65, 0x78, 0x74, 0x22, 0x73, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, - 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, - 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, - 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, - 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, - 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x49, 0x64, 0x73, 0x22, 0x52, 0x0a, 0x19, 0x47, 0x65, 0x74, - 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x69, - 0x74, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x72, 0x0a, - 0x18, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x45, 0x78, 0x69, - 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, - 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, - 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, - 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, - 0x6f, 0x72, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x09, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x73, 0x22, 0xaf, 0x01, 0x0a, 0x19, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4f, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x73, 0x45, 0x78, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x51, 0x0a, 0x09, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x68, 0x65, 0x63, - 0x6b, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x45, 0x78, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x45, 0x78, - 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x73, 0x1a, 0x3f, 0x0a, 0x11, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x45, 0x78, - 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, - 0x78, 0x69, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x78, 0x69, - 0x73, 0x74, 0x73, 0x32, 0xf1, 0x10, 0x0a, 0x0d, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x50, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, - 0x6d, 0x69, 0x74, 0x73, 0x12, 0x1a, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1b, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, - 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, - 0x97, 0x28, 0x02, 0x08, 0x02, 0x30, 0x01, 0x12, 0x59, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x41, - 0x6c, 0x6c, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x1d, 0x2e, 0x67, 0x69, 0x74, 0x61, - 0x6c, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x6c, 0x6c, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, - 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x6c, 0x6c, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, - 0x30, 0x01, 0x12, 0x5d, 0x0a, 0x10, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x49, 0x73, 0x41, 0x6e, - 0x63, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x12, 0x1f, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, - 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x49, 0x73, 0x41, 0x6e, 0x63, 0x65, 0x73, 0x74, 0x6f, 0x72, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, - 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x49, 0x73, 0x41, 0x6e, 0x63, 0x65, 0x73, 0x74, 0x6f, - 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, - 0x02, 0x12, 0x4a, 0x0a, 0x09, 0x54, 0x72, 0x65, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x18, - 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x54, 0x72, 0x65, 0x65, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, - 0x79, 0x2e, 0x54, 0x72, 0x65, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x30, 0x01, 0x12, 0x51, 0x0a, - 0x0c, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x1b, 0x2e, - 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x6d, - 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x67, 0x69, 0x74, - 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, - 0x12, 0x6c, 0x0a, 0x15, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x44, 0x69, 0x76, 0x65, 0x72, 0x67, 0x69, - 0x6e, 0x67, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x24, 0x2e, 0x67, 0x69, 0x74, 0x61, - 0x6c, 0x79, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x44, 0x69, 0x76, 0x65, 0x72, 0x67, 0x69, 0x6e, - 0x67, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x25, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x44, 0x69, - 0x76, 0x65, 0x72, 0x67, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x12, 0x59, - 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x54, 0x72, 0x65, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, - 0x12, 0x1d, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x72, 0x65, - 0x65, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1e, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x72, 0x65, 0x65, - 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x30, 0x01, 0x12, 0x4a, 0x0a, 0x09, 0x4c, 0x69, 0x73, - 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x18, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x19, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, - 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, - 0x02, 0x08, 0x02, 0x30, 0x01, 0x12, 0x4b, 0x0a, 0x0a, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6d, - 0x6d, 0x69, 0x74, 0x12, 0x19, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x46, 0x69, 0x6e, - 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, - 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x6d, - 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, - 0x08, 0x02, 0x12, 0x4e, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x53, 0x74, 0x61, 0x74, - 0x73, 0x12, 0x1a, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, - 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, - 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x53, 0x74, 0x61, - 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, - 0x08, 0x02, 0x12, 0x59, 0x0a, 0x0e, 0x46, 0x69, 0x6e, 0x64, 0x41, 0x6c, 0x6c, 0x43, 0x6f, 0x6d, - 0x6d, 0x69, 0x74, 0x73, 0x12, 0x1d, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x46, 0x69, - 0x6e, 0x64, 0x41, 0x6c, 0x6c, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x46, 0x69, 0x6e, - 0x64, 0x41, 0x6c, 0x6c, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x30, 0x01, 0x12, 0x50, 0x0a, - 0x0b, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x1a, 0x2e, 0x67, - 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, + 0x75, 0x72, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, + 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, + 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x69, 0x67, + 0x6e, 0x65, 0x64, 0x5f, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, + 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x65, 0x78, 0x74, 0x22, 0x75, 0x0a, 0x1a, 0x47, 0x65, + 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, + 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x69, 0x64, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x49, 0x64, + 0x73, 0x22, 0x79, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x53, 0x69, + 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x49, 0x64, 0x12, 0x1c, 0x0a, + 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, + 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x74, 0x65, 0x78, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x0a, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x54, 0x65, 0x78, 0x74, 0x22, 0x73, 0x0a, 0x18, + 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, + 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x69, 0x64, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x49, 0x64, + 0x73, 0x22, 0x52, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, + 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x72, 0x0a, 0x18, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x73, 0x45, 0x78, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, + 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x72, + 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x09, + 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xaf, 0x01, 0x0a, 0x19, 0x43, 0x68, + 0x65, 0x63, 0x6b, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x45, 0x78, 0x69, 0x73, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x09, 0x72, 0x65, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x67, 0x69, 0x74, + 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, + 0x45, 0x78, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, + 0x09, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x3f, 0x0a, 0x11, 0x52, 0x65, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x32, 0xf1, 0x10, 0x0a, 0x0d, + 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x50, 0x0a, + 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x1a, 0x2e, 0x67, + 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, - 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, + 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x30, 0x01, 0x12, - 0x5a, 0x0a, 0x0f, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, - 0x65, 0x73, 0x12, 0x1e, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, - 0x69, 0x74, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, - 0x69, 0x74, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x12, 0x47, 0x0a, 0x08, 0x52, - 0x61, 0x77, 0x42, 0x6c, 0x61, 0x6d, 0x65, 0x12, 0x17, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, - 0x2e, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x18, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x61, - 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, - 0x08, 0x02, 0x30, 0x01, 0x12, 0x60, 0x0a, 0x11, 0x4c, 0x61, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, - 0x69, 0x74, 0x46, 0x6f, 0x72, 0x50, 0x61, 0x74, 0x68, 0x12, 0x20, 0x2e, 0x67, 0x69, 0x74, 0x61, - 0x6c, 0x79, 0x2e, 0x4c, 0x61, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x46, 0x6f, 0x72, - 0x50, 0x61, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x67, 0x69, - 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, 0x61, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x46, - 0x6f, 0x72, 0x50, 0x61, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, - 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x12, 0x71, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x61, - 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x46, 0x6f, 0x72, 0x54, 0x72, 0x65, 0x65, - 0x12, 0x25, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x61, - 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x46, 0x6f, 0x72, 0x54, 0x72, 0x65, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x61, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, - 0x46, 0x6f, 0x72, 0x54, 0x72, 0x65, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x30, 0x01, 0x12, 0x5f, 0x0a, 0x10, 0x43, 0x6f, 0x6d, - 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1f, 0x2e, - 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, - 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, - 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x30, 0x01, 0x12, 0x5f, 0x0a, 0x10, 0x4c, 0x69, - 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, 0x4f, 0x69, 0x64, 0x12, 0x1f, - 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, - 0x69, 0x74, 0x73, 0x42, 0x79, 0x4f, 0x69, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x20, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, - 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, 0x4f, 0x69, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x30, 0x01, 0x12, 0x6b, 0x0a, 0x14, 0x4c, - 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, 0x52, 0x65, 0x66, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x23, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, 0x52, 0x65, 0x66, 0x4e, 0x61, 0x6d, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, - 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, 0x52, - 0x65, 0x66, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, - 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x30, 0x01, 0x12, 0x79, 0x0a, 0x18, 0x46, 0x69, 0x6c, 0x74, - 0x65, 0x72, 0x53, 0x68, 0x61, 0x73, 0x57, 0x69, 0x74, 0x68, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x73, 0x12, 0x27, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x46, 0x69, - 0x6c, 0x74, 0x65, 0x72, 0x53, 0x68, 0x61, 0x73, 0x57, 0x69, 0x74, 0x68, 0x53, 0x69, 0x67, 0x6e, - 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, - 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x53, 0x68, 0x61, - 0x73, 0x57, 0x69, 0x74, 0x68, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x28, - 0x01, 0x30, 0x01, 0x12, 0x68, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, - 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x22, 0x2e, 0x67, 0x69, 0x74, - 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x53, 0x69, 0x67, - 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, - 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, - 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x30, 0x01, 0x12, 0x62, 0x0a, - 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x73, 0x12, 0x20, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, - 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x65, - 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x52, + 0x59, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x6c, 0x6c, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, + 0x73, 0x12, 0x1d, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, + 0x6c, 0x6c, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1e, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x6c, + 0x6c, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x30, 0x01, 0x12, 0x5d, 0x0a, 0x10, 0x43, 0x6f, + 0x6d, 0x6d, 0x69, 0x74, 0x49, 0x73, 0x41, 0x6e, 0x63, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x12, 0x1f, + 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x49, 0x73, + 0x41, 0x6e, 0x63, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x20, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x49, + 0x73, 0x41, 0x6e, 0x63, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x12, 0x4a, 0x0a, 0x09, 0x54, 0x72, 0x65, + 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x18, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, + 0x54, 0x72, 0x65, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x19, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x54, 0x72, 0x65, 0x65, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, + 0x02, 0x08, 0x02, 0x30, 0x01, 0x12, 0x51, 0x0a, 0x0c, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x43, 0x6f, + 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x1b, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x12, 0x6c, 0x0a, 0x15, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x44, 0x69, 0x76, 0x65, 0x72, 0x67, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, + 0x73, 0x12, 0x24, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x44, 0x69, 0x76, 0x65, 0x72, 0x67, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, + 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x44, 0x69, 0x76, 0x65, 0x72, 0x67, 0x69, 0x6e, 0x67, 0x43, + 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, + 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x12, 0x59, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x54, 0x72, 0x65, + 0x65, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x1d, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, + 0x79, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x72, 0x65, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, + 0x2e, 0x47, 0x65, 0x74, 0x54, 0x72, 0x65, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x30, + 0x01, 0x12, 0x4a, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x18, + 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, + 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x30, 0x01, 0x12, 0x4b, 0x0a, + 0x0a, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x12, 0x19, 0x2e, 0x67, 0x69, + 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, + 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x12, 0x4e, 0x0a, 0x0b, 0x43, 0x6f, + 0x6d, 0x6d, 0x69, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x1a, 0x2e, 0x67, 0x69, 0x74, 0x61, + 0x6c, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, + 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x12, 0x59, 0x0a, 0x0e, 0x46, 0x69, + 0x6e, 0x64, 0x41, 0x6c, 0x6c, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x1d, 0x2e, 0x67, + 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x41, 0x6c, 0x6c, 0x43, 0x6f, 0x6d, + 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x67, 0x69, + 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x41, 0x6c, 0x6c, 0x43, 0x6f, 0x6d, 0x6d, + 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, + 0x02, 0x08, 0x02, 0x30, 0x01, 0x12, 0x50, 0x0a, 0x0b, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6d, + 0x6d, 0x69, 0x74, 0x73, 0x12, 0x1a, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x46, 0x69, + 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1b, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, + 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, + 0x97, 0x28, 0x02, 0x08, 0x02, 0x30, 0x01, 0x12, 0x5a, 0x0a, 0x0f, 0x43, 0x6f, 0x6d, 0x6d, 0x69, + 0x74, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x73, 0x12, 0x1e, 0x2e, 0x67, 0x69, 0x74, + 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, + 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x67, 0x69, 0x74, + 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, + 0x67, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, + 0x02, 0x08, 0x02, 0x12, 0x47, 0x0a, 0x08, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x61, 0x6d, 0x65, 0x12, + 0x17, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x61, 0x6d, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, + 0x79, 0x2e, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x30, 0x01, 0x12, 0x60, 0x0a, 0x11, + 0x4c, 0x61, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x50, 0x61, 0x74, + 0x68, 0x12, 0x20, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, 0x61, 0x73, 0x74, 0x43, + 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x50, 0x61, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, 0x61, 0x73, + 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x50, 0x61, 0x74, 0x68, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x12, 0x71, + 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x61, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, + 0x73, 0x46, 0x6f, 0x72, 0x54, 0x72, 0x65, 0x65, 0x12, 0x25, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, + 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x61, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, + 0x73, 0x46, 0x6f, 0x72, 0x54, 0x72, 0x65, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x26, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x61, 0x73, + 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x46, 0x6f, 0x72, 0x54, 0x72, 0x65, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x30, - 0x01, 0x12, 0x64, 0x0a, 0x11, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x73, 0x45, 0x78, 0x69, 0x73, 0x74, 0x12, 0x20, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, - 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x45, 0x78, 0x69, 0x73, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, - 0x79, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x45, 0x78, - 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, - 0x02, 0x08, 0x02, 0x28, 0x01, 0x30, 0x01, 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x6c, 0x61, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2d, 0x6f, 0x72, 0x67, - 0x2f, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2f, 0x76, 0x31, 0x35, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x70, 0x62, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x01, 0x12, 0x5f, 0x0a, 0x10, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1f, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, + 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, + 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, + 0x30, 0x01, 0x12, 0x5f, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, + 0x73, 0x42, 0x79, 0x4f, 0x69, 0x64, 0x12, 0x1f, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, 0x4f, 0x69, 0x64, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, 0x4f, 0x69, + 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, + 0x02, 0x30, 0x01, 0x12, 0x6b, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, + 0x74, 0x73, 0x42, 0x79, 0x52, 0x65, 0x66, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x2e, 0x67, 0x69, + 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x73, + 0x42, 0x79, 0x52, 0x65, 0x66, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x24, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, + 0x6d, 0x6d, 0x69, 0x74, 0x73, 0x42, 0x79, 0x52, 0x65, 0x66, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x30, 0x01, + 0x12, 0x79, 0x0a, 0x18, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x53, 0x68, 0x61, 0x73, 0x57, 0x69, + 0x74, 0x68, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x27, 0x2e, 0x67, + 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x53, 0x68, 0x61, 0x73, + 0x57, 0x69, 0x74, 0x68, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x46, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x53, 0x68, 0x61, 0x73, 0x57, 0x69, 0x74, 0x68, 0x53, 0x69, 0x67, + 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x28, 0x01, 0x30, 0x01, 0x12, 0x68, 0x0a, 0x13, 0x47, + 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x73, 0x12, 0x22, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, + 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, + 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, + 0x02, 0x08, 0x02, 0x30, 0x01, 0x12, 0x62, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x6d, + 0x69, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x12, 0x20, 0x2e, 0x67, 0x69, 0x74, + 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x67, + 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x30, 0x01, 0x12, 0x64, 0x0a, 0x11, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x45, 0x78, 0x69, 0x73, 0x74, 0x12, 0x20, + 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4f, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x73, 0x45, 0x78, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x21, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4f, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x45, 0x78, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x02, 0x28, 0x01, 0x30, 0x01, 0x42, + 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, + 0x74, 0x6c, 0x61, 0x62, 0x2d, 0x6f, 0x72, 0x67, 0x2f, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2f, + 0x76, 0x31, 0x35, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x67, 0x69, 0x74, + 0x61, 0x6c, 0x79, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proto/go/gitalypb/commit_grpc.pb.go b/proto/go/gitalypb/commit_grpc.pb.go index 4a32b22c5..4fb2773d2 100644 --- a/proto/go/gitalypb/commit_grpc.pb.go +++ b/proto/go/gitalypb/commit_grpc.pb.go @@ -49,7 +49,8 @@ type CommitServiceClient interface { FindAllCommits(ctx context.Context, in *FindAllCommitsRequest, opts ...grpc.CallOption) (CommitService_FindAllCommitsClient, error) // This comment is left unintentionally blank. FindCommits(ctx context.Context, in *FindCommitsRequest, opts ...grpc.CallOption) (CommitService_FindCommitsClient, error) - // This comment is left unintentionally blank. + // CommitLanguages detects the source code languages of the whole tree for a + // given commit. Returns an error in case no languages could be detected. CommitLanguages(ctx context.Context, in *CommitLanguagesRequest, opts ...grpc.CallOption) (*CommitLanguagesResponse, error) // This comment is left unintentionally blank. RawBlame(ctx context.Context, in *RawBlameRequest, opts ...grpc.CallOption) (CommitService_RawBlameClient, error) @@ -688,7 +689,8 @@ type CommitServiceServer interface { FindAllCommits(*FindAllCommitsRequest, CommitService_FindAllCommitsServer) error // This comment is left unintentionally blank. FindCommits(*FindCommitsRequest, CommitService_FindCommitsServer) error - // This comment is left unintentionally blank. + // CommitLanguages detects the source code languages of the whole tree for a + // given commit. Returns an error in case no languages could be detected. CommitLanguages(context.Context, *CommitLanguagesRequest) (*CommitLanguagesResponse, error) // This comment is left unintentionally blank. RawBlame(*RawBlameRequest, CommitService_RawBlameServer) error diff --git a/ruby/Gemfile b/ruby/Gemfile index 7037e215f..cbb9321fd 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' gem 'rugged', '~> 1.2' -gem 'github-linguist', '~> 7.12', require: 'linguist' +gem 'github-linguist', '~> 7.20.0', require: 'linguist' gem 'gitlab-markup', '~> 1.7.1' gem 'activesupport', '~> 6.1.4.7' gem 'rdoc', '~> 6.0' diff --git a/ruby/Gemfile.lock b/ruby/Gemfile.lock index f8688dbb5..65faf2b72 100644 --- a/ruby/Gemfile.lock +++ b/ruby/Gemfile.lock @@ -40,7 +40,7 @@ GEM dotenv (2.7.6) equalizer (0.0.11) erubi (1.10.0) - escape_utils (1.2.1) + escape_utils (1.2.2) factory_bot (5.0.2) activesupport (>= 4.2.0) faraday (1.0.1) @@ -48,11 +48,11 @@ GEM ffi (1.15.3) gemojione (3.3.0) json - github-linguist (7.12.1) + github-linguist (7.20.0) charlock_holmes (~> 0.7.7) escape_utils (~> 1.2.0) mini_mime (~> 1.0) - rugged (>= 0.25.1) + rugged (~> 1.0) github-markup (1.7.0) gitlab-gollum-lib (4.2.7.10.gitlab.2) gemojione (~> 3.2) @@ -110,7 +110,7 @@ GEM mime-types (3.3.1) mime-types-data (~> 3.2015) mime-types-data (3.2020.1104) - mini_mime (1.0.2) + mini_mime (1.1.2) mini_portile2 (2.8.0) minitest (5.15.0) msgpack (1.3.3) @@ -228,7 +228,7 @@ DEPENDENCIES activesupport (~> 6.1.4.7) factory_bot faraday (~> 1.0) - github-linguist (~> 7.12) + github-linguist (~> 7.20.0) gitlab-gollum-lib (~> 4.2.7.10.gitlab.2) gitlab-gollum-rugged_adapter (~> 0.4.4.4.gitlab.1) gitlab-labkit (~> 0.23) diff --git a/ruby/proto/gitaly/commit_pb.rb b/ruby/proto/gitaly/commit_pb.rb index c9b6e8e5f..efdd4d133 100644 --- a/ruby/proto/gitaly/commit_pb.rb +++ b/ruby/proto/gitaly/commit_pb.rb @@ -120,6 +120,7 @@ Google::Protobuf::DescriptorPool.generated_pool.build do optional :recursive, :bool, 4 optional :sort, :enum, 5, "gitaly.GetTreeEntriesRequest.SortBy" optional :pagination_params, :message, 6, "gitaly.PaginationParameter" + optional :skip_flat_paths, :bool, 7 end add_enum "gitaly.GetTreeEntriesRequest.SortBy" do value :DEFAULT, 0 diff --git a/ruby/proto/gitaly/commit_services_pb.rb b/ruby/proto/gitaly/commit_services_pb.rb index 17df1b4b8..c99e663fb 100644 --- a/ruby/proto/gitaly/commit_services_pb.rb +++ b/ruby/proto/gitaly/commit_services_pb.rb @@ -43,7 +43,8 @@ module Gitaly rpc :FindAllCommits, ::Gitaly::FindAllCommitsRequest, stream(::Gitaly::FindAllCommitsResponse) # This comment is left unintentionally blank. rpc :FindCommits, ::Gitaly::FindCommitsRequest, stream(::Gitaly::FindCommitsResponse) - # This comment is left unintentionally blank. + # CommitLanguages detects the source code languages of the whole tree for a + # given commit. Returns an error in case no languages could be detected. rpc :CommitLanguages, ::Gitaly::CommitLanguagesRequest, ::Gitaly::CommitLanguagesResponse # This comment is left unintentionally blank. rpc :RawBlame, ::Gitaly::RawBlameRequest, stream(::Gitaly::RawBlameResponse) diff --git a/ruby/proto/gitaly/version.rb b/ruby/proto/gitaly/version.rb index 9e216fd60..f937b357e 100644 --- a/ruby/proto/gitaly/version.rb +++ b/ruby/proto/gitaly/version.rb @@ -2,5 +2,5 @@ # (https://gitlab.com/gitlab-org/release-tools/), and should not be # modified. module Gitaly - VERSION = '15.1.0-rc1' + VERSION = '15.2.0-rc1' end |