summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.bazelrc34
-rw-r--r--.bazelversion2
-rw-r--r--Documentation/access-control.txt63
-rw-r--r--Documentation/cmd-hook-commit-msg.txt6
-rw-r--r--Documentation/config-gerrit.txt455
-rw-r--r--Documentation/config-labels.txt9
-rw-r--r--Documentation/config-project-config.txt736
-rw-r--r--Documentation/config-submit-requirements.txt521
-rw-r--r--Documentation/dev-bazel.txt48
-rw-r--r--Documentation/dev-community.txt2
-rw-r--r--Documentation/dev-contribution-opportunities.txt45
-rw-r--r--Documentation/dev-plugins.txt46
-rw-r--r--Documentation/dev-processes.txt25
-rw-r--r--Documentation/dev-starter-projects.txt15
-rw-r--r--Documentation/images/generated-suggested-edit-added.pngbin0 -> 65493 bytes
-rw-r--r--Documentation/images/generated-suggested-edit-preview.pngbin0 -> 109548 bytes
-rw-r--r--Documentation/intro-project-owner.txt34
-rw-r--r--Documentation/intro-user.txt7
-rw-r--r--Documentation/licenses.txt3
-rw-r--r--Documentation/metrics.txt7
-rw-r--r--Documentation/rest-api-accounts.txt12
-rw-r--r--Documentation/rest-api-changes.txt215
-rw-r--r--Documentation/rest-api-config.txt401
-rw-r--r--Documentation/rest-api-projects.txt94
-rw-r--r--Documentation/user-search.txt1
-rw-r--r--Documentation/user-suggest-edits.txt15
-rw-r--r--MODULE.bazel2
-rw-r--r--WORKSPACE49
-rw-r--r--contrib/bug-icon/1_bug_icon_16x16.ai2189
-rw-r--r--contrib/bug-icon/1_bug_icon_16x16.svg266
-rw-r--r--java/com/google/gerrit/acceptance/AbstractDaemonTest.java451
-rw-r--r--java/com/google/gerrit/acceptance/AbstractDynamicOptionsTest.java4
-rw-r--r--java/com/google/gerrit/acceptance/AbstractNotificationTest.java31
-rw-r--r--java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java6
-rw-r--r--java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java183
-rw-r--r--java/com/google/gerrit/acceptance/AccountCreator.java29
-rw-r--r--java/com/google/gerrit/acceptance/AccountIndexedCounter.java5
-rw-r--r--java/com/google/gerrit/acceptance/ChangeIndexedCounter.java4
-rw-r--r--java/com/google/gerrit/acceptance/DaemonTestRule.java42
-rw-r--r--java/com/google/gerrit/acceptance/EventRecorder.java3
-rw-r--r--java/com/google/gerrit/acceptance/ExtensionRegistry.java52
-rw-r--r--java/com/google/gerrit/acceptance/FakeGroupAuditService.java2
-rw-r--r--java/com/google/gerrit/acceptance/GerritServerDaemonTestRule.java96
-rw-r--r--java/com/google/gerrit/acceptance/GerritServerRestSession.java148
-rw-r--r--java/com/google/gerrit/acceptance/GerritServerTestRule.java285
-rw-r--r--java/com/google/gerrit/acceptance/GitUtil.java9
-rw-r--r--java/com/google/gerrit/acceptance/HttpSession.java2
-rw-r--r--java/com/google/gerrit/acceptance/InProcessProtocol.java8
-rw-r--r--java/com/google/gerrit/acceptance/ProjectResetter.java54
-rw-r--r--java/com/google/gerrit/acceptance/PushOneCommit.java92
-rw-r--r--java/com/google/gerrit/acceptance/RestResponse.java5
-rw-r--r--java/com/google/gerrit/acceptance/RestSession.java150
-rw-r--r--java/com/google/gerrit/acceptance/ServerTestRule.java104
-rw-r--r--java/com/google/gerrit/acceptance/SshSession.java2
-rw-r--r--java/com/google/gerrit/acceptance/SshSessionMina.java3
-rw-r--r--java/com/google/gerrit/acceptance/StandaloneSiteTest.java6
-rw-r--r--java/com/google/gerrit/acceptance/TestAccount.java2
-rw-r--r--java/com/google/gerrit/acceptance/TestConfigRule.java142
-rw-r--r--java/com/google/gerrit/acceptance/TimeSettingsTestRule.java84
-rw-r--r--java/com/google/gerrit/acceptance/config/BUILD1
-rw-r--r--java/com/google/gerrit/acceptance/config/package-info.java18
-rw-r--r--java/com/google/gerrit/acceptance/package-info.java18
-rw-r--r--java/com/google/gerrit/acceptance/rest/package-info.java18
-rw-r--r--java/com/google/gerrit/acceptance/ssh/TestSshCommandModule.java4
-rw-r--r--java/com/google/gerrit/acceptance/ssh/package-info.java18
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java7
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java23
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/account/package-info.java18
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java3
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java10
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/change/TestCommentCreation.java11
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/change/TestPatchsetCreation.java2
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/change/package-info.java18
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/group/BUILD1
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java9
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java8
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/group/package-info.java18
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/package-info.java18
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/project/BUILD1
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java5
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java13
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/project/package-info.java18
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java61
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java68
-rw-r--r--java/com/google/gerrit/acceptance/testsuite/request/package-info.java18
-rw-r--r--java/com/google/gerrit/asciidoctor/AsciiDoctor.java4
-rw-r--r--java/com/google/gerrit/asciidoctor/BUILD2
-rw-r--r--java/com/google/gerrit/asciidoctor/DocIndexer.java4
-rw-r--r--java/com/google/gerrit/asciidoctor/package-info.java18
-rw-r--r--java/com/google/gerrit/auth/ldap/LdapRealm.java5
-rw-r--r--java/com/google/gerrit/auth/ldap/package-info.java18
-rw-r--r--java/com/google/gerrit/auth/oauth/package-info.java18
-rw-r--r--java/com/google/gerrit/auth/openid/package-info.java18
-rw-r--r--java/com/google/gerrit/auth/package-info.java18
-rw-r--r--java/com/google/gerrit/common/BUILD1
-rw-r--r--java/com/google/gerrit/common/FileUtil.java2
-rw-r--r--java/com/google/gerrit/common/UsedAt.java1
-rw-r--r--java/com/google/gerrit/common/auth/openid/package-info.java18
-rw-r--r--java/com/google/gerrit/common/data/package-info.java18
-rw-r--r--java/com/google/gerrit/common/data/testing/BUILD1
-rw-r--r--java/com/google/gerrit/common/data/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/common/package-info.java18
-rw-r--r--java/com/google/gerrit/entities/AccessSection.java6
-rw-r--r--java/com/google/gerrit/entities/Account.java33
-rw-r--r--java/com/google/gerrit/entities/AccountGroupByIdAudit.java2
-rw-r--r--java/com/google/gerrit/entities/AccountGroupMemberAudit.java3
-rw-r--r--java/com/google/gerrit/entities/CachedProjectConfig.java11
-rw-r--r--java/com/google/gerrit/entities/Comment.java23
-rw-r--r--java/com/google/gerrit/entities/FixReplacement.java18
-rw-r--r--java/com/google/gerrit/entities/FixSuggestion.java17
-rw-r--r--java/com/google/gerrit/entities/HumanComment.java5
-rw-r--r--java/com/google/gerrit/entities/LabelType.java5
-rw-r--r--java/com/google/gerrit/entities/NotifyConfig.java3
-rw-r--r--java/com/google/gerrit/entities/Permission.java6
-rw-r--r--java/com/google/gerrit/entities/PermissionRule.java4
-rw-r--r--java/com/google/gerrit/entities/Project.java3
-rw-r--r--java/com/google/gerrit/entities/RobotComment.java9
-rw-r--r--java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java2
-rw-r--r--java/com/google/gerrit/entities/SubscribeSection.java3
-rw-r--r--java/com/google/gerrit/entities/converter/AccountInputProtoConverter.java92
-rw-r--r--java/com/google/gerrit/entities/converter/ApplyPatchInputProtoConverter.java55
-rw-r--r--java/com/google/gerrit/entities/converter/ChangeInputProtoConverter.java186
-rw-r--r--java/com/google/gerrit/entities/converter/ChangeProtoConverter.java7
-rw-r--r--java/com/google/gerrit/entities/converter/HumanCommentProtoConverter.java145
-rw-r--r--java/com/google/gerrit/entities/converter/MergeInputProtoConverter.java70
-rw-r--r--java/com/google/gerrit/entities/converter/NotifyInfoProtoConverter.java50
-rw-r--r--java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java3
-rw-r--r--java/com/google/gerrit/entities/converter/package-info.java18
-rw-r--r--java/com/google/gerrit/entities/package-info.java18
-rw-r--r--java/com/google/gerrit/exceptions/BUILD1
-rw-r--r--java/com/google/gerrit/exceptions/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/annotations/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/api/access/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/api/accounts/AccountApi.java10
-rw-r--r--java/com/google/gerrit/extensions/api/accounts/Accounts.java3
-rw-r--r--java/com/google/gerrit/extensions/api/accounts/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/api/changes/ChangeApi.java34
-rw-r--r--java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java18
-rw-r--r--java/com/google/gerrit/extensions/api/changes/ChangeEditIdentityType.java20
-rw-r--r--java/com/google/gerrit/extensions/api/changes/Changes.java2
-rw-r--r--java/com/google/gerrit/extensions/api/changes/CommentApi.java2
-rw-r--r--java/com/google/gerrit/extensions/api/changes/DraftApi.java2
-rw-r--r--java/com/google/gerrit/extensions/api/changes/NotifyHandling.java18
-rw-r--r--java/com/google/gerrit/extensions/api/changes/RecipientType.java16
-rw-r--r--java/com/google/gerrit/extensions/api/changes/ReviewInput.java5
-rw-r--r--java/com/google/gerrit/extensions/api/changes/RevisionApi.java10
-rw-r--r--java/com/google/gerrit/extensions/api/changes/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/api/config/ExperimentApi.java30
-rw-r--r--java/com/google/gerrit/extensions/api/config/Server.java35
-rw-r--r--java/com/google/gerrit/extensions/api/config/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/api/groups/Groups.java3
-rw-r--r--java/com/google/gerrit/extensions/api/groups/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/api/lfs/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/api/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/api/plugins/Plugins.java3
-rw-r--r--java/com/google/gerrit/extensions/api/plugins/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/api/projects/BranchApi.java2
-rw-r--r--java/com/google/gerrit/extensions/api/projects/ConfigValue.java7
-rw-r--r--java/com/google/gerrit/extensions/api/projects/LabelApi.java3
-rw-r--r--java/com/google/gerrit/extensions/api/projects/ProjectApi.java24
-rw-r--r--java/com/google/gerrit/extensions/api/projects/Projects.java3
-rw-r--r--java/com/google/gerrit/extensions/api/projects/SubmitRequirementApi.java3
-rw-r--r--java/com/google/gerrit/extensions/api/projects/TagApi.java2
-rw-r--r--java/com/google/gerrit/extensions/api/projects/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/auth/oauth/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/client/ChangeStatus.java16
-rw-r--r--java/com/google/gerrit/extensions/client/Comment.java10
-rw-r--r--java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java2
-rw-r--r--java/com/google/gerrit/extensions/client/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/common/ChangeInfo.java1
-rw-r--r--java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java5
-rw-r--r--java/com/google/gerrit/extensions/common/CommentInfo.java5
-rw-r--r--java/com/google/gerrit/extensions/common/CommitMessageInfo.java37
-rw-r--r--java/com/google/gerrit/extensions/common/CommitMessageInput.java2
-rw-r--r--java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java1
-rw-r--r--java/com/google/gerrit/extensions/common/ExperimentInfo.java20
-rw-r--r--java/com/google/gerrit/extensions/common/FixReplacementInfo.java17
-rw-r--r--java/com/google/gerrit/extensions/common/FixSuggestionInfo.java17
-rw-r--r--java/com/google/gerrit/extensions/common/ListTagSortOption.java20
-rw-r--r--java/com/google/gerrit/extensions/common/RobotCommentInfo.java3
-rw-r--r--java/com/google/gerrit/extensions/common/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/common/testing/BUILD1
-rw-r--r--java/com/google/gerrit/extensions/common/testing/CommentInfoSubject.java12
-rw-r--r--java/com/google/gerrit/extensions/common/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/conditions/BooleanCondition.java6
-rw-r--r--java/com/google/gerrit/extensions/conditions/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/config/DownloadScheme.java5
-rw-r--r--java/com/google/gerrit/extensions/config/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/events/GerritEvent.java4
-rw-r--r--java/com/google/gerrit/extensions/events/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/registration/DynamicItem.java4
-rw-r--r--java/com/google/gerrit/extensions/registration/DynamicSet.java4
-rw-r--r--java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java2
-rw-r--r--java/com/google/gerrit/extensions/registration/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/restapi/BinaryResult.java7
-rw-r--r--java/com/google/gerrit/extensions/restapi/Response.java2
-rw-r--r--java/com/google/gerrit/extensions/restapi/RestApiModule.java19
-rw-r--r--java/com/google/gerrit/extensions/restapi/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/restapi/testing/BUILD1
-rw-r--r--java/com/google/gerrit/extensions/restapi/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/systemstatus/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/validators/package-info.java18
-rw-r--r--java/com/google/gerrit/extensions/webui/UiAction.java8
-rw-r--r--java/com/google/gerrit/extensions/webui/package-info.java18
-rw-r--r--java/com/google/gerrit/git/BUILD2
-rw-r--r--java/com/google/gerrit/git/RefUpdateUtil.java34
-rw-r--r--java/com/google/gerrit/git/package-info.java18
-rw-r--r--java/com/google/gerrit/git/testing/BUILD1
-rw-r--r--java/com/google/gerrit/git/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/gpg/BUILD1
-rw-r--r--java/com/google/gerrit/gpg/Fingerprint.java2
-rw-r--r--java/com/google/gerrit/gpg/GerritPublicKeyChecker.java2
-rw-r--r--java/com/google/gerrit/gpg/PublicKeyChecker.java5
-rw-r--r--java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java3
-rw-r--r--java/com/google/gerrit/gpg/api/package-info.java18
-rw-r--r--java/com/google/gerrit/gpg/package-info.java18
-rw-r--r--java/com/google/gerrit/gpg/server/PostGpgKeys.java10
-rw-r--r--java/com/google/gerrit/gpg/server/package-info.java18
-rw-r--r--java/com/google/gerrit/gpg/testing/BUILD1
-rw-r--r--java/com/google/gerrit/gpg/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/httpd/AllRequestFilter.java5
-rw-r--r--java/com/google/gerrit/httpd/GitOverHttpServlet.java4
-rw-r--r--java/com/google/gerrit/httpd/RequestContextFilter.java3
-rw-r--r--java/com/google/gerrit/httpd/UrlModule.java4
-rw-r--r--java/com/google/gerrit/httpd/auth/become/package-info.java18
-rw-r--r--java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java2
-rw-r--r--java/com/google/gerrit/httpd/auth/container/package-info.java18
-rw-r--r--java/com/google/gerrit/httpd/auth/ldap/package-info.java18
-rw-r--r--java/com/google/gerrit/httpd/auth/oauth/BUILD1
-rw-r--r--java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java4
-rw-r--r--java/com/google/gerrit/httpd/auth/oauth/package-info.java18
-rw-r--r--java/com/google/gerrit/httpd/auth/openid/BUILD1
-rw-r--r--java/com/google/gerrit/httpd/auth/openid/LoginForm.java4
-rw-r--r--java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java4
-rw-r--r--java/com/google/gerrit/httpd/auth/openid/package-info.java18
-rw-r--r--java/com/google/gerrit/httpd/auth/restapi/BUILD1
-rw-r--r--java/com/google/gerrit/httpd/auth/restapi/package-info.java18
-rw-r--r--java/com/google/gerrit/httpd/gitweb/GitwebServlet.java7
-rw-r--r--java/com/google/gerrit/httpd/gitweb/package-info.java18
-rw-r--r--java/com/google/gerrit/httpd/init/BUILD1
-rw-r--r--java/com/google/gerrit/httpd/init/SiteInitializer.java16
-rw-r--r--java/com/google/gerrit/httpd/init/WebAppInitializer.java10
-rw-r--r--java/com/google/gerrit/httpd/init/package-info.java18
-rw-r--r--java/com/google/gerrit/httpd/package-info.java18
-rw-r--r--java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java1
-rw-r--r--java/com/google/gerrit/httpd/plugins/package-info.java18
-rw-r--r--java/com/google/gerrit/httpd/raw/AuthorizationCheckServlet.java16
-rw-r--r--java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java2
-rw-r--r--java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java1
-rw-r--r--java/com/google/gerrit/httpd/raw/StaticModule.java17
-rw-r--r--java/com/google/gerrit/httpd/raw/package-info.java18
-rw-r--r--java/com/google/gerrit/httpd/resources/package-info.java18
-rw-r--r--java/com/google/gerrit/httpd/restapi/RestApiServlet.java28
-rw-r--r--java/com/google/gerrit/httpd/restapi/package-info.java18
-rw-r--r--java/com/google/gerrit/httpd/template/package-info.java18
-rw-r--r--java/com/google/gerrit/index/Index.java12
-rw-r--r--java/com/google/gerrit/index/IndexCollection.java2
-rw-r--r--java/com/google/gerrit/index/IndexDefinition.java6
-rw-r--r--java/com/google/gerrit/index/IndexedField.java3
-rw-r--r--java/com/google/gerrit/index/Schema.java10
-rw-r--r--java/com/google/gerrit/index/SiteIndexer.java10
-rw-r--r--java/com/google/gerrit/index/package-info.java18
-rw-r--r--java/com/google/gerrit/index/project/BUILD1
-rw-r--r--java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java3
-rw-r--r--java/com/google/gerrit/index/project/package-info.java18
-rw-r--r--java/com/google/gerrit/index/query/FilteredSource.java5
-rw-r--r--java/com/google/gerrit/index/query/InternalQuery.java8
-rw-r--r--java/com/google/gerrit/index/query/QueryProcessor.java24
-rw-r--r--java/com/google/gerrit/index/query/package-info.java18
-rw-r--r--java/com/google/gerrit/index/query/testing/BUILD1
-rw-r--r--java/com/google/gerrit/index/query/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/index/testing/AbstractFakeIndex.java7
-rw-r--r--java/com/google/gerrit/index/testing/BUILD1
-rw-r--r--java/com/google/gerrit/index/testing/FakeIndexModule.java8
-rw-r--r--java/com/google/gerrit/index/testing/FakeIndexVersionManager.java7
-rw-r--r--java/com/google/gerrit/index/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/jgit/BUILD1
-rw-r--r--java/com/google/gerrit/jgit/diff/package-info.java18
-rw-r--r--java/com/google/gerrit/json/BUILD1
-rw-r--r--java/com/google/gerrit/json/package-info.java18
-rw-r--r--java/com/google/gerrit/launcher/BUILD3
-rw-r--r--java/com/google/gerrit/launcher/GerritLauncher.java7
-rw-r--r--java/com/google/gerrit/launcher/package-info.java18
-rw-r--r--java/com/google/gerrit/lifecycle/BUILD1
-rw-r--r--java/com/google/gerrit/lifecycle/package-info.java18
-rw-r--r--java/com/google/gerrit/lucene/AbstractLuceneIndex.java37
-rw-r--r--java/com/google/gerrit/lucene/BUILD6
-rw-r--r--java/com/google/gerrit/lucene/GerritIndexWriterConfig.java9
-rw-r--r--java/com/google/gerrit/lucene/LuceneChangeIndex.java39
-rw-r--r--java/com/google/gerrit/lucene/LuceneIndexModule.java12
-rw-r--r--java/com/google/gerrit/lucene/LuceneVersionManager.java7
-rw-r--r--java/com/google/gerrit/lucene/package-info.java18
-rw-r--r--java/com/google/gerrit/mail/BUILD1
-rw-r--r--java/com/google/gerrit/mail/MailMessage.java4
-rw-r--r--java/com/google/gerrit/mail/package-info.java18
-rw-r--r--java/com/google/gerrit/metrics/Description.java7
-rw-r--r--java/com/google/gerrit/metrics/MetricMaker.java4
-rw-r--r--java/com/google/gerrit/metrics/Timer0.java16
-rw-r--r--java/com/google/gerrit/metrics/TimerContext.java3
-rw-r--r--java/com/google/gerrit/metrics/dropwizard/BUILD1
-rw-r--r--java/com/google/gerrit/metrics/dropwizard/BucketedCallback.java4
-rw-r--r--java/com/google/gerrit/metrics/dropwizard/package-info.java18
-rw-r--r--java/com/google/gerrit/metrics/package-info.java18
-rw-r--r--java/com/google/gerrit/metrics/proc/package-info.java18
-rw-r--r--java/com/google/gerrit/pgm/BUILD1
-rw-r--r--java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java4
-rw-r--r--java/com/google/gerrit/pgm/Daemon.java13
-rw-r--r--java/com/google/gerrit/pgm/Init.java6
-rw-r--r--java/com/google/gerrit/pgm/JythonShell.java79
-rw-r--r--java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java4
-rw-r--r--java/com/google/gerrit/pgm/Ls.java3
-rw-r--r--java/com/google/gerrit/pgm/Reindex.java35
-rw-r--r--java/com/google/gerrit/pgm/SwitchSecureStore.java3
-rw-r--r--java/com/google/gerrit/pgm/WarDistribution.java3
-rw-r--r--java/com/google/gerrit/pgm/http/jetty/BUILD1
-rw-r--r--java/com/google/gerrit/pgm/http/jetty/package-info.java18
-rw-r--r--java/com/google/gerrit/pgm/init/AccountsOnInitNoteDbImpl.java1
-rw-r--r--java/com/google/gerrit/pgm/init/BUILD1
-rw-r--r--java/com/google/gerrit/pgm/init/BaseInit.java3
-rw-r--r--java/com/google/gerrit/pgm/init/InitAdminUser.java5
-rw-r--r--java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java2
-rw-r--r--java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java2
-rw-r--r--java/com/google/gerrit/pgm/init/api/GitRepositoryManagerOnInit.java3
-rw-r--r--java/com/google/gerrit/pgm/init/api/Section.java9
-rw-r--r--java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java6
-rw-r--r--java/com/google/gerrit/pgm/init/api/package-info.java18
-rw-r--r--java/com/google/gerrit/pgm/init/index/IndexModuleOnInit.java12
-rw-r--r--java/com/google/gerrit/pgm/init/index/lucene/package-info.java18
-rw-r--r--java/com/google/gerrit/pgm/init/index/package-info.java18
-rw-r--r--java/com/google/gerrit/pgm/init/package-info.java18
-rw-r--r--java/com/google/gerrit/pgm/package-info.java18
-rw-r--r--java/com/google/gerrit/pgm/rules/package-info.java18
-rw-r--r--java/com/google/gerrit/pgm/util/BUILD1
-rw-r--r--java/com/google/gerrit/pgm/util/BatchProgramModule.java8
-rw-r--r--java/com/google/gerrit/pgm/util/LogFileManager.java (renamed from java/com/google/gerrit/pgm/util/LogFileCompressor.java)110
-rw-r--r--java/com/google/gerrit/pgm/util/SiteProgram.java5
-rw-r--r--java/com/google/gerrit/pgm/util/package-info.java18
-rw-r--r--java/com/google/gerrit/prettify/BUILD1
-rw-r--r--java/com/google/gerrit/prettify/common/package-info.java18
-rw-r--r--java/com/google/gerrit/prettify/common/testing/BUILD1
-rw-r--r--java/com/google/gerrit/prettify/common/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/proto/BUILD1
-rw-r--r--java/com/google/gerrit/proto/package-info.java18
-rw-r--r--java/com/google/gerrit/proto/testing/BUILD1
-rw-r--r--java/com/google/gerrit/proto/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/server/BUILD2
-rw-r--r--java/com/google/gerrit/server/ChangeDraftUpdate.java29
-rw-r--r--java/com/google/gerrit/server/ChangeDraftUpdateExecutor.java129
-rw-r--r--java/com/google/gerrit/server/ChangeMessagesUtil.java3
-rw-r--r--java/com/google/gerrit/server/ChangeUtil.java3
-rw-r--r--java/com/google/gerrit/server/CmdLineParserModule.java3
-rw-r--r--java/com/google/gerrit/server/CommentVerifier.java52
-rw-r--r--java/com/google/gerrit/server/CommentsUtil.java116
-rw-r--r--java/com/google/gerrit/server/DeleteZombieComments.java4
-rw-r--r--java/com/google/gerrit/server/PublishCommentUtil.java4
-rw-r--r--java/com/google/gerrit/server/ReviewerStatusUpdate.java19
-rw-r--r--java/com/google/gerrit/server/Sequence.java3
-rw-r--r--java/com/google/gerrit/server/Sequences.java24
-rw-r--r--java/com/google/gerrit/server/StarredChangesReader.java43
-rw-r--r--java/com/google/gerrit/server/StarredChangesWriter.java13
-rw-r--r--java/com/google/gerrit/server/account/AbstractRealm.java4
-rw-r--r--java/com/google/gerrit/server/account/AccountCacheImpl.java46
-rw-r--r--java/com/google/gerrit/server/account/AccountConfig.java3
-rw-r--r--java/com/google/gerrit/server/account/AccountLimits.java3
-rw-r--r--java/com/google/gerrit/server/account/AccountManager.java12
-rw-r--r--java/com/google/gerrit/server/account/AccountProperties.java1
-rw-r--r--java/com/google/gerrit/server/account/AccountResolver.java2
-rw-r--r--java/com/google/gerrit/server/account/AccountState.java16
-rw-r--r--java/com/google/gerrit/server/account/AccountsUpdate.java2
-rw-r--r--java/com/google/gerrit/server/account/CachedAccountDetails.java11
-rw-r--r--java/com/google/gerrit/server/account/DefaultRealm.java4
-rw-r--r--java/com/google/gerrit/server/account/Emails.java7
-rw-r--r--java/com/google/gerrit/server/account/GroupCacheImpl.java3
-rw-r--r--java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java34
-rw-r--r--java/com/google/gerrit/server/account/GroupMembers.java3
-rw-r--r--java/com/google/gerrit/server/account/IncludingGroupMembership.java2
-rw-r--r--java/com/google/gerrit/server/account/ListGroupMembership.java2
-rw-r--r--java/com/google/gerrit/server/account/SetInactiveFlag.java3
-rw-r--r--java/com/google/gerrit/server/account/UniversalGroupBackend.java2
-rw-r--r--java/com/google/gerrit/server/account/VersionedAccountDestinations.java1
-rw-r--r--java/com/google/gerrit/server/account/VersionedAccountQueries.java1
-rw-r--r--java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java2
-rw-r--r--java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java3
-rw-r--r--java/com/google/gerrit/server/account/externalids/package-info.java18
-rw-r--r--java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java5
-rw-r--r--java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNotes.java4
-rw-r--r--java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsConsistencyCheckerNoteDbImpl.java12
-rw-r--r--java/com/google/gerrit/server/account/externalids/storage/notedb/OnlineExternalIdCaseSensivityMigrator.java8
-rw-r--r--java/com/google/gerrit/server/account/externalids/storage/notedb/package-info.java18
-rw-r--r--java/com/google/gerrit/server/account/externalids/testing/BUILD1
-rw-r--r--java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java5
-rw-r--r--java/com/google/gerrit/server/account/externalids/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/server/account/package-info.java18
-rw-r--r--java/com/google/gerrit/server/account/storage/notedb/AccountNoteDbReadStorageModule.java3
-rw-r--r--java/com/google/gerrit/server/account/storage/notedb/AccountNoteDbWriteStorageModule.java5
-rw-r--r--java/com/google/gerrit/server/account/storage/notedb/AccountsUpdateNoteDbImpl.java19
-rw-r--r--java/com/google/gerrit/server/account/storage/notedb/package-info.java18
-rw-r--r--java/com/google/gerrit/server/api/BUILD1
-rw-r--r--java/com/google/gerrit/server/api/accounts/AccountApiImpl.java48
-rw-r--r--java/com/google/gerrit/server/api/accounts/EmailApiImpl.java6
-rw-r--r--java/com/google/gerrit/server/api/accounts/package-info.java18
-rw-r--r--java/com/google/gerrit/server/api/changes/AttentionSetApiImpl.java3
-rw-r--r--java/com/google/gerrit/server/api/changes/ChangeApiImpl.java53
-rw-r--r--java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java48
-rw-r--r--java/com/google/gerrit/server/api/changes/ChangesImpl.java4
-rw-r--r--java/com/google/gerrit/server/api/changes/DraftApiImpl.java3
-rw-r--r--java/com/google/gerrit/server/api/changes/FileApiImpl.java6
-rw-r--r--java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java9
-rw-r--r--java/com/google/gerrit/server/api/changes/RevisionApiImpl.java7
-rw-r--r--java/com/google/gerrit/server/api/changes/RevisionReviewerApiImpl.java6
-rw-r--r--java/com/google/gerrit/server/api/changes/package-info.java18
-rw-r--r--java/com/google/gerrit/server/api/config/ConfigModule.java2
-rw-r--r--java/com/google/gerrit/server/api/config/ExperimentApiImpl.java49
-rw-r--r--java/com/google/gerrit/server/api/config/ServerImpl.java50
-rw-r--r--java/com/google/gerrit/server/api/config/package-info.java18
-rw-r--r--java/com/google/gerrit/server/api/groups/GroupApiImpl.java27
-rw-r--r--java/com/google/gerrit/server/api/groups/package-info.java18
-rw-r--r--java/com/google/gerrit/server/api/package-info.java18
-rw-r--r--java/com/google/gerrit/server/api/plugins/PluginApiImpl.java9
-rw-r--r--java/com/google/gerrit/server/api/plugins/package-info.java18
-rw-r--r--java/com/google/gerrit/server/api/projects/BranchApiImpl.java6
-rw-r--r--java/com/google/gerrit/server/api/projects/DashboardApiImpl.java7
-rw-r--r--java/com/google/gerrit/server/api/projects/LabelApiImpl.java6
-rw-r--r--java/com/google/gerrit/server/api/projects/ProjectApiImpl.java36
-rw-r--r--java/com/google/gerrit/server/api/projects/SubmitRequirementApiImpl.java6
-rw-r--r--java/com/google/gerrit/server/api/projects/TagApiImpl.java6
-rw-r--r--java/com/google/gerrit/server/api/projects/package-info.java18
-rw-r--r--java/com/google/gerrit/server/approval/ApprovalCopier.java36
-rw-r--r--java/com/google/gerrit/server/approval/ApprovalsUtil.java22
-rw-r--r--java/com/google/gerrit/server/approval/package-info.java18
-rw-r--r--java/com/google/gerrit/server/approval/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/server/args4j/ListTagSortOptionHandler.java54
-rw-r--r--java/com/google/gerrit/server/args4j/package-info.java18
-rw-r--r--java/com/google/gerrit/server/audit/BUILD1
-rw-r--r--java/com/google/gerrit/server/audit/group/package-info.java18
-rw-r--r--java/com/google/gerrit/server/audit/package-info.java18
-rw-r--r--java/com/google/gerrit/server/auth/openid/package-info.java18
-rw-r--r--java/com/google/gerrit/server/auth/package-info.java18
-rw-r--r--java/com/google/gerrit/server/avatar/package-info.java18
-rw-r--r--java/com/google/gerrit/server/cache/CacheBinding.java8
-rw-r--r--java/com/google/gerrit/server/cache/CacheMetrics.java3
-rw-r--r--java/com/google/gerrit/server/cache/CacheModule.java4
-rw-r--r--java/com/google/gerrit/server/cache/CacheProvider.java8
-rw-r--r--java/com/google/gerrit/server/cache/PersistentCacheBinding.java10
-rw-r--r--java/com/google/gerrit/server/cache/h2/BUILD1
-rw-r--r--java/com/google/gerrit/server/cache/h2/H2CacheFactory.java35
-rw-r--r--java/com/google/gerrit/server/cache/h2/H2CacheImpl.java26
-rw-r--r--java/com/google/gerrit/server/cache/h2/package-info.java18
-rw-r--r--java/com/google/gerrit/server/cache/mem/BUILD1
-rw-r--r--java/com/google/gerrit/server/cache/mem/package-info.java18
-rw-r--r--java/com/google/gerrit/server/cache/package-info.java18
-rw-r--r--java/com/google/gerrit/server/cache/serialize/BUILD1
-rw-r--r--java/com/google/gerrit/server/cache/serialize/entities/BUILD1
-rw-r--r--java/com/google/gerrit/server/cache/serialize/entities/package-info.java18
-rw-r--r--java/com/google/gerrit/server/cache/serialize/package-info.java18
-rw-r--r--java/com/google/gerrit/server/cache/testing/BUILD1
-rw-r--r--java/com/google/gerrit/server/cache/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/server/cancellation/BUILD1
-rw-r--r--java/com/google/gerrit/server/cancellation/RequestStateContext.java2
-rw-r--r--java/com/google/gerrit/server/cancellation/package-info.java18
-rw-r--r--java/com/google/gerrit/server/change/AbandonUtil.java28
-rw-r--r--java/com/google/gerrit/server/change/ActionJson.java62
-rw-r--r--java/com/google/gerrit/server/change/AddReviewersOp.java4
-rw-r--r--java/com/google/gerrit/server/change/AddToAttentionSetOp.java15
-rw-r--r--java/com/google/gerrit/server/change/ArchiveFormatInternal.java4
-rw-r--r--java/com/google/gerrit/server/change/AttentionSetUpdateCondition.java30
-rw-r--r--java/com/google/gerrit/server/change/ChangeCleanupRunner.java18
-rw-r--r--java/com/google/gerrit/server/change/ChangeFinder.java8
-rw-r--r--java/com/google/gerrit/server/change/ChangeInserter.java13
-rw-r--r--java/com/google/gerrit/server/change/ChangeJson.java485
-rw-r--r--java/com/google/gerrit/server/change/ChangeKindCacheImpl.java8
-rw-r--r--java/com/google/gerrit/server/change/ChangeResource.java10
-rw-r--r--java/com/google/gerrit/server/change/CommentThread.java2
-rw-r--r--java/com/google/gerrit/server/change/CommentsValidator.java251
-rw-r--r--java/com/google/gerrit/server/change/ConsistencyChecker.java2
-rw-r--r--java/com/google/gerrit/server/change/DeleteChangeOp.java3
-rw-r--r--java/com/google/gerrit/server/change/DeleteReviewerOp.java4
-rw-r--r--java/com/google/gerrit/server/change/DeleteReviewersUtil.java4
-rw-r--r--java/com/google/gerrit/server/change/EmailNewPatchSet.java3
-rw-r--r--java/com/google/gerrit/server/change/EmailReviewComments.java3
-rw-r--r--java/com/google/gerrit/server/change/FilterIncludedIn.java52
-rw-r--r--java/com/google/gerrit/server/change/GetRelatedChangesUtil.java3
-rw-r--r--java/com/google/gerrit/server/change/IncludedIn.java27
-rw-r--r--java/com/google/gerrit/server/change/LabelsJson.java116
-rw-r--r--java/com/google/gerrit/server/change/ParentDataProvider.java6
-rw-r--r--java/com/google/gerrit/server/change/PatchSetInserter.java28
-rw-r--r--java/com/google/gerrit/server/change/ReaddOwnerUtil.java21
-rw-r--r--java/com/google/gerrit/server/change/RebaseChangeOp.java79
-rw-r--r--java/com/google/gerrit/server/change/RebaseUtil.java48
-rw-r--r--java/com/google/gerrit/server/change/RelatedChangesSorter.java4
-rw-r--r--java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java14
-rw-r--r--java/com/google/gerrit/server/change/ReviewerModifier.java10
-rw-r--r--java/com/google/gerrit/server/change/RevisionJson.java43
-rw-r--r--java/com/google/gerrit/server/change/RevisionResource.java30
-rw-r--r--java/com/google/gerrit/server/change/SetHashtagsOp.java3
-rw-r--r--java/com/google/gerrit/server/change/WalkSorter.java21
-rw-r--r--java/com/google/gerrit/server/change/package-info.java18
-rw-r--r--java/com/google/gerrit/server/change/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/server/comment/package-info.java18
-rw-r--r--java/com/google/gerrit/server/config/AdministrateServerGroupsProvider.java3
-rw-r--r--java/com/google/gerrit/server/config/CachedPreferences.java52
-rw-r--r--java/com/google/gerrit/server/config/ConfigUpdatedEvent.java13
-rw-r--r--java/com/google/gerrit/server/config/ConfigUtil.java46
-rw-r--r--java/com/google/gerrit/server/config/ExperimentResource.java33
-rw-r--r--java/com/google/gerrit/server/config/GerritGlobalModule.java12
-rw-r--r--java/com/google/gerrit/server/config/GerritInstanceNameProvider.java12
-rw-r--r--java/com/google/gerrit/server/config/GerritServerConfigReloader.java10
-rw-r--r--java/com/google/gerrit/server/config/GitwebCgiConfig.java5
-rw-r--r--java/com/google/gerrit/server/config/GroupSetProvider.java3
-rw-r--r--java/com/google/gerrit/server/config/IndexResource.java34
-rw-r--r--java/com/google/gerrit/server/config/IndexVersionResource.java42
-rw-r--r--java/com/google/gerrit/server/config/PreferencesParserUtil.java165
-rw-r--r--java/com/google/gerrit/server/config/ProjectConfigEntry.java2
-rw-r--r--java/com/google/gerrit/server/config/RepositoryConfig.java2
-rw-r--r--java/com/google/gerrit/server/config/UserPreferencesConverter.java21
-rw-r--r--java/com/google/gerrit/server/config/VersionedDefaultPreferences.java28
-rw-r--r--java/com/google/gerrit/server/config/package-info.java18
-rw-r--r--java/com/google/gerrit/server/data/BUILD1
-rw-r--r--java/com/google/gerrit/server/data/package-info.java18
-rw-r--r--java/com/google/gerrit/server/diff/package-info.java18
-rw-r--r--java/com/google/gerrit/server/documentation/package-info.java18
-rw-r--r--java/com/google/gerrit/server/edit/ChangeEditModifier.java125
-rw-r--r--java/com/google/gerrit/server/edit/ChangeEditUtil.java8
-rw-r--r--java/com/google/gerrit/server/edit/CommitModification.java11
-rw-r--r--java/com/google/gerrit/server/edit/package-info.java18
-rw-r--r--java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java15
-rw-r--r--java/com/google/gerrit/server/edit/tree/TreeCreator.java46
-rw-r--r--java/com/google/gerrit/server/edit/tree/package-info.java18
-rw-r--r--java/com/google/gerrit/server/events/CommitReceivedEvent.java12
-rw-r--r--java/com/google/gerrit/server/events/EventFactory.java3
-rw-r--r--java/com/google/gerrit/server/events/EventTypes.java2
-rw-r--r--java/com/google/gerrit/server/events/StreamEventsApiListener.java18
-rw-r--r--java/com/google/gerrit/server/events/package-info.java18
-rw-r--r--java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java35
-rw-r--r--java/com/google/gerrit/server/experiments/package-info.java18
-rw-r--r--java/com/google/gerrit/server/extensions/events/package-info.java18
-rw-r--r--java/com/google/gerrit/server/extensions/webui/package-info.java18
-rw-r--r--java/com/google/gerrit/server/fixes/package-info.java18
-rw-r--r--java/com/google/gerrit/server/fixes/testing/BUILD1
-rw-r--r--java/com/google/gerrit/server/fixes/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/server/git/ChangesByProjectCache.java4
-rw-r--r--java/com/google/gerrit/server/git/ChangesByProjectCacheImpl.java14
-rw-r--r--java/com/google/gerrit/server/git/GarbageCollection.java4
-rw-r--r--java/com/google/gerrit/server/git/GarbageCollectionQueue.java2
-rw-r--r--java/com/google/gerrit/server/git/GroupCollector.java4
-rw-r--r--java/com/google/gerrit/server/git/InMemoryInserter.java3
-rw-r--r--java/com/google/gerrit/server/git/MergeUtil.java72
-rw-r--r--java/com/google/gerrit/server/git/MultiProgressMonitor.java4
-rw-r--r--java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java7
-rw-r--r--java/com/google/gerrit/server/git/WorkQueue.java14
-rw-r--r--java/com/google/gerrit/server/git/meta/VersionedConfigFile.java (renamed from java/com/google/gerrit/server/schema/VersionedAccountPreferences.java)56
-rw-r--r--java/com/google/gerrit/server/git/meta/VersionedMetaData.java6
-rw-r--r--java/com/google/gerrit/server/git/meta/package-info.java18
-rw-r--r--java/com/google/gerrit/server/git/package-info.java18
-rw-r--r--java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java10
-rw-r--r--java/com/google/gerrit/server/git/receive/BUILD1
-rw-r--r--java/com/google/gerrit/server/git/receive/BranchCommitValidator.java17
-rw-r--r--java/com/google/gerrit/server/git/receive/ReceiveCommits.java738
-rw-r--r--java/com/google/gerrit/server/git/receive/ReceiveCommitsResult.java3
-rw-r--r--java/com/google/gerrit/server/git/receive/ReplaceOp.java5
-rw-r--r--java/com/google/gerrit/server/git/receive/package-info.java18
-rw-r--r--java/com/google/gerrit/server/git/receive/testing/BUILD1
-rw-r--r--java/com/google/gerrit/server/git/receive/testing/TestRefAdvertiser.java4
-rw-r--r--java/com/google/gerrit/server/git/receive/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/server/git/validators/CommentSizeValidator.java2
-rw-r--r--java/com/google/gerrit/server/git/validators/CommitValidationListener.java5
-rw-r--r--java/com/google/gerrit/server/git/validators/CommitValidators.java61
-rw-r--r--java/com/google/gerrit/server/git/validators/MergeValidators.java2
-rw-r--r--java/com/google/gerrit/server/git/validators/RefOperationValidators.java2
-rw-r--r--java/com/google/gerrit/server/git/validators/package-info.java18
-rw-r--r--java/com/google/gerrit/server/group/db/GroupConfig.java2
-rw-r--r--java/com/google/gerrit/server/group/db/GroupsUpdate.java3
-rw-r--r--java/com/google/gerrit/server/group/db/package-info.java18
-rw-r--r--java/com/google/gerrit/server/group/db/testing/BUILD1
-rw-r--r--java/com/google/gerrit/server/group/db/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/server/group/package-info.java18
-rw-r--r--java/com/google/gerrit/server/group/testing/BUILD1
-rw-r--r--java/com/google/gerrit/server/group/testing/TestGroupBackend.java2
-rw-r--r--java/com/google/gerrit/server/group/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/server/index/AbstractIndexModule.java7
-rw-r--r--java/com/google/gerrit/server/index/IndexModule.java23
-rw-r--r--java/com/google/gerrit/server/index/IndexUtils.java19
-rw-r--r--java/com/google/gerrit/server/index/IndexVersionReindexer.java56
-rw-r--r--java/com/google/gerrit/server/index/OnlineReindexer.java7
-rw-r--r--java/com/google/gerrit/server/index/SingleVersionModule.java8
-rw-r--r--java/com/google/gerrit/server/index/VersionManager.java9
-rw-r--r--java/com/google/gerrit/server/index/account/AccountIndexer.java2
-rw-r--r--java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java5
-rw-r--r--java/com/google/gerrit/server/index/account/ReindexAccountsAfterRefUpdate.java55
-rw-r--r--java/com/google/gerrit/server/index/account/StalenessChecker.java3
-rw-r--r--java/com/google/gerrit/server/index/account/package-info.java18
-rw-r--r--java/com/google/gerrit/server/index/change/AllChangesIndexer.java88
-rw-r--r--java/com/google/gerrit/server/index/change/ChangeField.java20
-rw-r--r--java/com/google/gerrit/server/index/change/ChangeIndexDefinition.java20
-rw-r--r--java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java5
-rw-r--r--java/com/google/gerrit/server/index/change/ChangeIndexer.java173
-rw-r--r--java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java12
-rw-r--r--java/com/google/gerrit/server/index/change/ReindexChangesAfterRefUpdate.java (renamed from java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java)39
-rw-r--r--java/com/google/gerrit/server/index/change/StalenessChecker.java39
-rw-r--r--java/com/google/gerrit/server/index/change/package-info.java18
-rw-r--r--java/com/google/gerrit/server/index/group/AllGroupsIndexer.java5
-rw-r--r--java/com/google/gerrit/server/index/group/GroupIndexer.java2
-rw-r--r--java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java5
-rw-r--r--java/com/google/gerrit/server/index/group/package-info.java18
-rw-r--r--java/com/google/gerrit/server/index/options/package-info.java18
-rw-r--r--java/com/google/gerrit/server/index/package-info.java18
-rw-r--r--java/com/google/gerrit/server/index/project/StalenessChecker.java3
-rw-r--r--java/com/google/gerrit/server/index/project/package-info.java18
-rw-r--r--java/com/google/gerrit/server/ioutil/BUILD1
-rw-r--r--java/com/google/gerrit/server/ioutil/package-info.java18
-rw-r--r--java/com/google/gerrit/server/logging/BUILD1
-rw-r--r--java/com/google/gerrit/server/logging/CallerFinder.java18
-rw-r--r--java/com/google/gerrit/server/logging/LoggingContext.java3
-rw-r--r--java/com/google/gerrit/server/logging/Metadata.java43
-rw-r--r--java/com/google/gerrit/server/logging/MutableTags.java2
-rw-r--r--java/com/google/gerrit/server/logging/PerformanceLogRecord.java13
-rw-r--r--java/com/google/gerrit/server/logging/PerformanceLogger.java7
-rw-r--r--java/com/google/gerrit/server/logging/TraceContext.java6
-rw-r--r--java/com/google/gerrit/server/logging/package-info.java18
-rw-r--r--java/com/google/gerrit/server/mail/package-info.java18
-rw-r--r--java/com/google/gerrit/server/mail/receive/MailProcessor.java24
-rw-r--r--java/com/google/gerrit/server/mail/receive/package-info.java18
-rw-r--r--java/com/google/gerrit/server/mail/send/ChangeEmailImpl.java23
-rw-r--r--java/com/google/gerrit/server/mail/send/CommentChangeEmailDecoratorImpl.java2
-rw-r--r--java/com/google/gerrit/server/mail/send/MessageIdGenerator.java96
-rw-r--r--java/com/google/gerrit/server/mail/send/MessageIdGeneratorImpl.java103
-rw-r--r--java/com/google/gerrit/server/mail/send/OutgoingEmail.java22
-rw-r--r--java/com/google/gerrit/server/mail/send/ProjectWatch.java2
-rw-r--r--java/com/google/gerrit/server/mail/send/SmtpEmailSender.java9
-rw-r--r--java/com/google/gerrit/server/mail/send/package-info.java18
-rw-r--r--java/com/google/gerrit/server/mime/package-info.java18
-rw-r--r--java/com/google/gerrit/server/notedb/AbstractChangeNotes.java3
-rw-r--r--java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java33
-rw-r--r--java/com/google/gerrit/server/notedb/ChangeDraftNotesUpdate.java163
-rw-r--r--java/com/google/gerrit/server/notedb/ChangeNoteUtil.java2
-rw-r--r--java/com/google/gerrit/server/notedb/ChangeNotes.java7
-rw-r--r--java/com/google/gerrit/server/notedb/ChangeNotesCache.java2
-rw-r--r--java/com/google/gerrit/server/notedb/ChangeNotesParser.java29
-rw-r--r--java/com/google/gerrit/server/notedb/ChangeNotesState.java50
-rw-r--r--java/com/google/gerrit/server/notedb/ChangeUpdate.java54
-rw-r--r--java/com/google/gerrit/server/notedb/CommitRewriter.java4
-rw-r--r--java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java3
-rw-r--r--java/com/google/gerrit/server/notedb/DraftCommentsNotesReader.java18
-rw-r--r--java/com/google/gerrit/server/notedb/NoteDbDraftCommentsModule.java36
-rw-r--r--java/com/google/gerrit/server/notedb/NoteDbModule.java11
-rw-r--r--java/com/google/gerrit/server/notedb/NoteDbStarredChangesModule.java28
-rw-r--r--java/com/google/gerrit/server/notedb/NoteDbUpdateExecutor.java94
-rw-r--r--java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java160
-rw-r--r--java/com/google/gerrit/server/notedb/OpenRepo.java8
-rw-r--r--java/com/google/gerrit/server/notedb/RepoSequence.java8
-rw-r--r--java/com/google/gerrit/server/notedb/StarredChangesUtilNoteDbImpl.java13
-rw-r--r--java/com/google/gerrit/server/notedb/package-info.java18
-rw-r--r--java/com/google/gerrit/server/package-info.java (renamed from java/com/google/gerrit/server/StarredChangesUtil.java)7
-rw-r--r--java/com/google/gerrit/server/patch/AutoMerger.java92
-rw-r--r--java/com/google/gerrit/server/patch/BaseCommitUtil.java52
-rw-r--r--java/com/google/gerrit/server/patch/DiffOperations.java46
-rw-r--r--java/com/google/gerrit/server/patch/DiffOperationsForCommitValidation.java84
-rw-r--r--java/com/google/gerrit/server/patch/DiffOperationsImpl.java197
-rw-r--r--java/com/google/gerrit/server/patch/GitPositionTransformer.java3
-rw-r--r--java/com/google/gerrit/server/patch/PatchList.java2
-rw-r--r--java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java123
-rw-r--r--java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheKey.java2
-rw-r--r--java/com/google/gerrit/server/patch/diff/ModifiedFilesLoader.java268
-rw-r--r--java/com/google/gerrit/server/patch/diff/package-info.java18
-rw-r--r--java/com/google/gerrit/server/patch/filediff/AllDiffsEvaluator.java10
-rw-r--r--java/com/google/gerrit/server/patch/filediff/EditTransformer.java4
-rw-r--r--java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java2
-rw-r--r--java/com/google/gerrit/server/patch/filediff/package-info.java18
-rw-r--r--java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheImpl.java60
-rw-r--r--java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheKey.java2
-rw-r--r--java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesLoader.java117
-rw-r--r--java/com/google/gerrit/server/patch/gitdiff/package-info.java18
-rw-r--r--java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java3
-rw-r--r--java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java5
-rw-r--r--java/com/google/gerrit/server/patch/gitfilediff/package-info.java18
-rw-r--r--java/com/google/gerrit/server/patch/package-info.java18
-rw-r--r--java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java4
-rw-r--r--java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java10
-rw-r--r--java/com/google/gerrit/server/permissions/package-info.java18
-rw-r--r--java/com/google/gerrit/server/plugincontext/package-info.java18
-rw-r--r--java/com/google/gerrit/server/plugins/AutoRegisterModules.java2
-rw-r--r--java/com/google/gerrit/server/plugins/CopyConfigModule.java51
-rw-r--r--java/com/google/gerrit/server/plugins/JarPluginProvider.java3
-rw-r--r--java/com/google/gerrit/server/plugins/JarScanner.java5
-rw-r--r--java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java7
-rw-r--r--java/com/google/gerrit/server/plugins/PluginLoader.java11
-rw-r--r--java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java3
-rw-r--r--java/com/google/gerrit/server/plugins/package-info.java18
-rw-r--r--java/com/google/gerrit/server/project/ChildProjects.java7
-rw-r--r--java/com/google/gerrit/server/project/CommentLinkProvider.java4
-rw-r--r--java/com/google/gerrit/server/project/ContributorAgreementsChecker.java9
-rw-r--r--java/com/google/gerrit/server/project/LabelConfigValidator.java260
-rw-r--r--java/com/google/gerrit/server/project/ProjectConfig.java6
-rw-r--r--java/com/google/gerrit/server/project/ProjectCreator.java18
-rw-r--r--java/com/google/gerrit/server/project/ProjectLevelConfig.java45
-rw-r--r--java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java62
-rw-r--r--java/com/google/gerrit/server/project/PrologRulesWarningValidator.java20
-rw-r--r--java/com/google/gerrit/server/project/Reachable.java2
-rw-r--r--java/com/google/gerrit/server/project/RefPatternMatcher.java2
-rw-r--r--java/com/google/gerrit/server/project/SubmitRequirementConfigValidator.java11
-rw-r--r--java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java10
-rw-r--r--java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java61
-rw-r--r--java/com/google/gerrit/server/project/SubmitRuleEvaluator.java35
-rw-r--r--java/com/google/gerrit/server/project/package-info.java18
-rw-r--r--java/com/google/gerrit/server/project/testing/BUILD5
-rw-r--r--java/com/google/gerrit/server/project/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/server/query/account/AccountQueryProcessor.java16
-rw-r--r--java/com/google/gerrit/server/query/account/InternalAccountQuery.java5
-rw-r--r--java/com/google/gerrit/server/query/account/package-info.java18
-rw-r--r--java/com/google/gerrit/server/query/approval/ApprovalContext.java15
-rw-r--r--java/com/google/gerrit/server/query/approval/ApprovalQueryBuilder.java2
-rw-r--r--java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java29
-rw-r--r--java/com/google/gerrit/server/query/approval/package-info.java18
-rw-r--r--java/com/google/gerrit/server/query/change/ChangeData.java88
-rw-r--r--java/com/google/gerrit/server/query/change/ChangePredicates.java26
-rw-r--r--java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java57
-rw-r--r--java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java16
-rw-r--r--java/com/google/gerrit/server/query/change/ConflictsPredicate.java2
-rw-r--r--java/com/google/gerrit/server/query/change/EqualsLabelPredicates.java82
-rw-r--r--java/com/google/gerrit/server/query/change/GroupPredicate.java4
-rw-r--r--java/com/google/gerrit/server/query/change/InternalChangeQuery.java13
-rw-r--r--java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java6
-rw-r--r--java/com/google/gerrit/server/query/change/LabelPredicate.java8
-rw-r--r--java/com/google/gerrit/server/query/change/MagicLabelPredicates.java14
-rw-r--r--java/com/google/gerrit/server/query/change/OrSource.java5
-rw-r--r--java/com/google/gerrit/server/query/change/OutputStreamQuery.java5
-rw-r--r--java/com/google/gerrit/server/query/change/package-info.java18
-rw-r--r--java/com/google/gerrit/server/query/group/GroupQueryBuilder.java9
-rw-r--r--java/com/google/gerrit/server/query/group/GroupQueryProcessor.java16
-rw-r--r--java/com/google/gerrit/server/query/group/InternalGroupQuery.java4
-rw-r--r--java/com/google/gerrit/server/query/group/package-info.java18
-rw-r--r--java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java16
-rw-r--r--java/com/google/gerrit/server/query/project/package-info.java18
-rw-r--r--java/com/google/gerrit/server/quota/DefaultQuotaBackend.java6
-rw-r--r--java/com/google/gerrit/server/quota/package-info.java18
-rw-r--r--java/com/google/gerrit/server/restapi/access/package-info.java18
-rw-r--r--java/com/google/gerrit/server/restapi/account/CreateEmail.java4
-rw-r--r--java/com/google/gerrit/server/restapi/account/DeleteDraftCommentsUtil.java16
-rw-r--r--java/com/google/gerrit/server/restapi/account/GetAgreements.java4
-rw-r--r--java/com/google/gerrit/server/restapi/account/GetExternalIds.java4
-rw-r--r--java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java3
-rw-r--r--java/com/google/gerrit/server/restapi/account/PutPreferred.java4
-rw-r--r--java/com/google/gerrit/server/restapi/account/QueryAccounts.java6
-rw-r--r--java/com/google/gerrit/server/restapi/account/package-info.java18
-rw-r--r--java/com/google/gerrit/server/restapi/change/ApplyPatch.java9
-rw-r--r--java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java23
-rw-r--r--java/com/google/gerrit/server/restapi/change/ChangeEdits.java96
-rw-r--r--java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java2
-rw-r--r--java/com/google/gerrit/server/restapi/change/CherryPickChange.java4
-rw-r--r--java/com/google/gerrit/server/restapi/change/CommentJson.java52
-rw-r--r--java/com/google/gerrit/server/restapi/change/CreateChange.java193
-rw-r--r--java/com/google/gerrit/server/restapi/change/CreateDraftComment.java6
-rw-r--r--java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java3
-rw-r--r--java/com/google/gerrit/server/restapi/change/Fixes.java14
-rw-r--r--java/com/google/gerrit/server/restapi/change/GetBlame.java15
-rw-r--r--java/com/google/gerrit/server/restapi/change/GetDiff.java5
-rw-r--r--java/com/google/gerrit/server/restapi/change/GetMessage.java53
-rw-r--r--java/com/google/gerrit/server/restapi/change/ListChangeComments.java2
-rw-r--r--java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java2
-rw-r--r--java/com/google/gerrit/server/restapi/change/ListRobotComments.java2
-rw-r--r--java/com/google/gerrit/server/restapi/change/Mergeable.java4
-rw-r--r--java/com/google/gerrit/server/restapi/change/PostReview.java447
-rw-r--r--java/com/google/gerrit/server/restapi/change/PostReviewOp.java207
-rw-r--r--java/com/google/gerrit/server/restapi/change/PostReviewers.java4
-rw-r--r--java/com/google/gerrit/server/restapi/change/PutDraftComment.java3
-rw-r--r--java/com/google/gerrit/server/restapi/change/PutMessage.java36
-rw-r--r--java/com/google/gerrit/server/restapi/change/QueryChanges.java4
-rw-r--r--java/com/google/gerrit/server/restapi/change/Rebase.java2
-rw-r--r--java/com/google/gerrit/server/restapi/change/RebaseChain.java2
-rw-r--r--java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java214
-rw-r--r--java/com/google/gerrit/server/restapi/change/RevertSubmission.java8
-rw-r--r--java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java2
-rw-r--r--java/com/google/gerrit/server/restapi/change/Reviewers.java4
-rw-r--r--java/com/google/gerrit/server/restapi/change/ReviewersUtil.java4
-rw-r--r--java/com/google/gerrit/server/restapi/change/RevisionReviewers.java4
-rw-r--r--java/com/google/gerrit/server/restapi/change/RobotComments.java1
-rw-r--r--java/com/google/gerrit/server/restapi/change/Submit.java17
-rw-r--r--java/com/google/gerrit/server/restapi/change/SubmittedTogether.java9
-rw-r--r--java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java3
-rw-r--r--java/com/google/gerrit/server/restapi/change/package-info.java18
-rw-r--r--java/com/google/gerrit/server/restapi/config/ConfigRestApiModule.java20
-rw-r--r--java/com/google/gerrit/server/restapi/config/ExperimentsCollection.java65
-rw-r--r--java/com/google/gerrit/server/restapi/config/GetExperiment.java44
-rw-r--r--java/com/google/gerrit/server/restapi/config/GetIndex.java31
-rw-r--r--java/com/google/gerrit/server/restapi/config/GetIndexVersion.java35
-rw-r--r--java/com/google/gerrit/server/restapi/config/GetServerInfo.java5
-rw-r--r--java/com/google/gerrit/server/restapi/config/GetSummary.java6
-rw-r--r--java/com/google/gerrit/server/restapi/config/IndexCollection.java71
-rw-r--r--java/com/google/gerrit/server/restapi/config/IndexInfo.java63
-rw-r--r--java/com/google/gerrit/server/restapi/config/IndexVersionsCollection.java83
-rw-r--r--java/com/google/gerrit/server/restapi/config/ListExperiments.java88
-rw-r--r--java/com/google/gerrit/server/restapi/config/ListIndexVersions.java37
-rw-r--r--java/com/google/gerrit/server/restapi/config/ListIndexes.java49
-rw-r--r--java/com/google/gerrit/server/restapi/config/PostCaches.java6
-rw-r--r--java/com/google/gerrit/server/restapi/config/ReindexIndexVersion.java49
-rw-r--r--java/com/google/gerrit/server/restapi/config/ReloadConfig.java7
-rw-r--r--java/com/google/gerrit/server/restapi/config/SnapshotIndex.java62
-rw-r--r--java/com/google/gerrit/server/restapi/config/SnapshotIndexVersion.java54
-rw-r--r--java/com/google/gerrit/server/restapi/config/SnapshotIndexes.java72
-rw-r--r--java/com/google/gerrit/server/restapi/config/SnapshotInfo.java19
-rw-r--r--java/com/google/gerrit/server/restapi/config/package-info.java18
-rw-r--r--java/com/google/gerrit/server/restapi/group/CreateGroup.java3
-rw-r--r--java/com/google/gerrit/server/restapi/group/GroupJson.java3
-rw-r--r--java/com/google/gerrit/server/restapi/group/ListGroups.java10
-rw-r--r--java/com/google/gerrit/server/restapi/group/ListMembers.java20
-rw-r--r--java/com/google/gerrit/server/restapi/group/QueryGroups.java7
-rw-r--r--java/com/google/gerrit/server/restapi/group/package-info.java18
-rw-r--r--java/com/google/gerrit/server/restapi/package-info.java18
-rw-r--r--java/com/google/gerrit/server/restapi/project/CreateAccessChange.java165
-rw-r--r--java/com/google/gerrit/server/restapi/project/CreateLabel.java3
-rw-r--r--java/com/google/gerrit/server/restapi/project/CreateProject.java4
-rw-r--r--java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java4
-rw-r--r--java/com/google/gerrit/server/restapi/project/GarbageCollect.java3
-rw-r--r--java/com/google/gerrit/server/restapi/project/GetHead.java6
-rw-r--r--java/com/google/gerrit/server/restapi/project/IndexChanges.java9
-rw-r--r--java/com/google/gerrit/server/restapi/project/ListBranches.java3
-rw-r--r--java/com/google/gerrit/server/restapi/project/ListDashboards.java2
-rw-r--r--java/com/google/gerrit/server/restapi/project/ListLabels.java5
-rw-r--r--java/com/google/gerrit/server/restapi/project/ListTags.java35
-rw-r--r--java/com/google/gerrit/server/restapi/project/PostLabels.java9
-rw-r--r--java/com/google/gerrit/server/restapi/project/PutConfig.java11
-rw-r--r--java/com/google/gerrit/server/restapi/project/QueryProjects.java7
-rw-r--r--java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java215
-rw-r--r--java/com/google/gerrit/server/restapi/project/SetAccess.java106
-rw-r--r--java/com/google/gerrit/server/restapi/project/SetAccessUtil.java7
-rw-r--r--java/com/google/gerrit/server/restapi/project/SetLabel.java3
-rw-r--r--java/com/google/gerrit/server/restapi/project/TagSorter.java46
-rw-r--r--java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java4
-rw-r--r--java/com/google/gerrit/server/restapi/project/package-info.java18
-rw-r--r--java/com/google/gerrit/server/rules/DefaultSubmitRule.java6
-rw-r--r--java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java7
-rw-r--r--java/com/google/gerrit/server/rules/PrologSubmitRuleUtil.java15
-rw-r--r--java/com/google/gerrit/server/rules/package-info.java18
-rw-r--r--java/com/google/gerrit/server/rules/prolog/BUILD1
-rw-r--r--java/com/google/gerrit/server/rules/prolog/PredicateClassLoader.java5
-rw-r--r--java/com/google/gerrit/server/rules/prolog/PrologRule.java17
-rw-r--r--java/com/google/gerrit/server/rules/prolog/PrologSubmitRuleUtilImpl.java18
-rw-r--r--java/com/google/gerrit/server/rules/prolog/package-info.java18
-rw-r--r--java/com/google/gerrit/server/schema/AllProjectsInput.java2
-rw-r--r--java/com/google/gerrit/server/schema/BUILD1
-rw-r--r--java/com/google/gerrit/server/schema/MigrateLabelConfigToCopyCondition.java5
-rw-r--r--java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java7
-rw-r--r--java/com/google/gerrit/server/schema/package-info.java18
-rw-r--r--java/com/google/gerrit/server/schema/testing/BUILD1
-rw-r--r--java/com/google/gerrit/server/schema/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/server/securestore/package-info.java18
-rw-r--r--java/com/google/gerrit/server/securestore/testing/BUILD1
-rw-r--r--java/com/google/gerrit/server/securestore/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/server/ssh/package-info.java18
-rw-r--r--java/com/google/gerrit/server/submit/EmailMerge.java3
-rw-r--r--java/com/google/gerrit/server/submit/GitModules.java6
-rw-r--r--java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java6
-rw-r--r--java/com/google/gerrit/server/submit/MergeOp.java333
-rw-r--r--java/com/google/gerrit/server/submit/RebaseSorter.java23
-rw-r--r--java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java226
-rw-r--r--java/com/google/gerrit/server/submit/SubmitStrategy.java2
-rw-r--r--java/com/google/gerrit/server/submit/SubmoduleCommits.java2
-rw-r--r--java/com/google/gerrit/server/submit/SubmoduleOp.java11
-rw-r--r--java/com/google/gerrit/server/submit/SubscriptionGraph.java11
-rw-r--r--java/com/google/gerrit/server/submit/UpdateOrderCalculator.java4
-rw-r--r--java/com/google/gerrit/server/submit/package-info.java18
-rw-r--r--java/com/google/gerrit/server/submitrequirement/predicate/package-info.java18
-rw-r--r--java/com/google/gerrit/server/tools/package-info.java18
-rw-r--r--java/com/google/gerrit/server/update/BatchUpdate.java161
-rw-r--r--java/com/google/gerrit/server/update/BatchUpdates.java199
-rw-r--r--java/com/google/gerrit/server/update/RepoView.java5
-rw-r--r--java/com/google/gerrit/server/update/RetryHelper.java45
-rw-r--r--java/com/google/gerrit/server/update/RetryableAction.java21
-rw-r--r--java/com/google/gerrit/server/update/SubmissionExecutor.java10
-rw-r--r--java/com/google/gerrit/server/update/context/package-info.java18
-rw-r--r--java/com/google/gerrit/server/update/package-info.java18
-rw-r--r--java/com/google/gerrit/server/util/AttentionSetEmail.java3
-rw-r--r--java/com/google/gerrit/server/util/LabelVote.java2
-rw-r--r--java/com/google/gerrit/server/util/ManualRequestContext.java3
-rw-r--r--java/com/google/gerrit/server/util/RequestScopePropagator.java3
-rw-r--r--java/com/google/gerrit/server/util/git/BUILD1
-rw-r--r--java/com/google/gerrit/server/util/git/package-info.java18
-rw-r--r--java/com/google/gerrit/server/util/package-info.java18
-rw-r--r--java/com/google/gerrit/server/util/time/BUILD1
-rw-r--r--java/com/google/gerrit/server/util/time/package-info.java18
-rw-r--r--java/com/google/gerrit/server/validators/package-info.java18
-rw-r--r--java/com/google/gerrit/server/version/BUILD1
-rw-r--r--java/com/google/gerrit/server/version/package-info.java18
-rw-r--r--java/com/google/gerrit/sshd/BUILD1
-rw-r--r--java/com/google/gerrit/sshd/CommandModule.java6
-rw-r--r--java/com/google/gerrit/sshd/PluginCommandModule.java13
-rw-r--r--java/com/google/gerrit/sshd/SingleCommandPluginModule.java13
-rw-r--r--java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java1
-rw-r--r--java/com/google/gerrit/sshd/SshKeyCacheImpl.java2
-rw-r--r--java/com/google/gerrit/sshd/SshModule.java9
-rw-r--r--java/com/google/gerrit/sshd/SshScope.java5
-rw-r--r--java/com/google/gerrit/sshd/commands/CheckProjectAccessCommand.java12
-rw-r--r--java/com/google/gerrit/sshd/commands/CreateAccountCommand.java4
-rw-r--r--java/com/google/gerrit/sshd/commands/CreateGroupCommand.java8
-rw-r--r--java/com/google/gerrit/sshd/commands/DefaultCommandModule.java4
-rw-r--r--java/com/google/gerrit/sshd/commands/ExternalIdCommandsModule.java3
-rw-r--r--java/com/google/gerrit/sshd/commands/FlushCaches.java6
-rw-r--r--java/com/google/gerrit/sshd/commands/IndexChangesCommand.java3
-rw-r--r--java/com/google/gerrit/sshd/commands/IndexChangesInProjectCommand.java3
-rw-r--r--java/com/google/gerrit/sshd/commands/IndexCommandsModule.java1
-rw-r--r--java/com/google/gerrit/sshd/commands/KillCommand.java4
-rw-r--r--java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java7
-rw-r--r--java/com/google/gerrit/sshd/commands/ReloadConfig.java5
-rw-r--r--java/com/google/gerrit/sshd/commands/RenameGroupCommand.java4
-rw-r--r--java/com/google/gerrit/sshd/commands/ReviewCommand.java50
-rw-r--r--java/com/google/gerrit/sshd/commands/SequenceCommandsModule.java3
-rw-r--r--java/com/google/gerrit/sshd/commands/SetAccountCommand.java38
-rw-r--r--java/com/google/gerrit/sshd/commands/SetHeadCommand.java3
-rw-r--r--java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java7
-rw-r--r--java/com/google/gerrit/sshd/commands/SetMembersCommand.java16
-rw-r--r--java/com/google/gerrit/sshd/commands/SetParentCommand.java4
-rw-r--r--java/com/google/gerrit/sshd/commands/SetProjectCommand.java3
-rw-r--r--java/com/google/gerrit/sshd/commands/SetReviewersCommand.java41
-rw-r--r--java/com/google/gerrit/sshd/commands/SetTopicCommand.java3
-rw-r--r--java/com/google/gerrit/sshd/plugin/LfsPluginAuthCommand.java1
-rw-r--r--java/com/google/gerrit/sshd/plugin/package-info.java18
-rw-r--r--java/com/google/gerrit/testing/BUILD3
-rw-r--r--java/com/google/gerrit/testing/FakeAccountCache.java1
-rw-r--r--java/com/google/gerrit/testing/FakeEmailSender.java8
-rw-r--r--java/com/google/gerrit/testing/GerritJUnit.java3
-rw-r--r--java/com/google/gerrit/testing/InMemoryModule.java19
-rw-r--r--java/com/google/gerrit/testing/InMemoryRepositoryManager.java5
-rw-r--r--java/com/google/gerrit/testing/InMemoryTestEnvironment.java7
-rw-r--r--java/com/google/gerrit/testing/TestCommentHelper.java36
-rw-r--r--java/com/google/gerrit/testing/package-info.java18
-rw-r--r--java/com/google/gerrit/truth/BUILD1
-rw-r--r--java/com/google/gerrit/truth/package-info.java18
-rw-r--r--java/com/google/gerrit/util/cli/BUILD1
-rw-r--r--java/com/google/gerrit/util/cli/CmdLineParser.java2
-rw-r--r--java/com/google/gerrit/util/cli/package-info.java18
-rw-r--r--java/com/google/gerrit/util/http/BUILD1
-rw-r--r--java/com/google/gerrit/util/http/package-info.java18
-rw-r--r--java/com/google/gerrit/util/logging/BUILD1
-rw-r--r--java/com/google/gerrit/util/logging/package-info.java18
-rw-r--r--java/com/google/gerrit/util/ssl/BUILD3
-rw-r--r--java/com/google/gerrit/util/ssl/package-info.java18
-rw-r--r--java/gerrit/BUILD1
-rw-r--r--java/gerrit/package-info.java18
-rw-r--r--java/org/apache/commons/net/BUILD1
-rw-r--r--java/org/apache/commons/net/smtp/package-info.java18
-rw-r--r--javatests/com/google/gerrit/acceptance/ProjectResetterTest.java37
-rw-r--r--javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java664
-rw-r--r--javatests/com/google/gerrit/acceptance/api/accounts/AccountIndexerIT.java3
-rw-r--r--javatests/com/google/gerrit/acceptance/api/accounts/AccountListenersIT.java32
-rw-r--r--javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java36
-rw-r--r--javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java13
-rw-r--r--javatests/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java18
-rw-r--r--javatests/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java6
-rw-r--r--javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java32
-rw-r--r--javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java19
-rw-r--r--javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java16
-rw-r--r--javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java41
-rw-r--r--javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java160
-rw-r--r--javatests/com/google/gerrit/acceptance/api/change/CopiedApprovalsInChangeMessageIT.java1
-rw-r--r--javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsToFollowUpPatchSetsIT.java1
-rw-r--r--javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java77
-rw-r--r--javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java12
-rw-r--r--javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java1
-rw-r--r--javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java719
-rw-r--r--javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java1
-rw-r--r--javatests/com/google/gerrit/acceptance/api/change/RevertIT.java8
-rw-r--r--javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java13
-rw-r--r--javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementCustomRuleIT.java20
-rw-r--r--javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java6
-rw-r--r--javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java36
-rw-r--r--javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java42
-rw-r--r--javatests/com/google/gerrit/acceptance/api/config/GetExperimentIT.java81
-rw-r--r--javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java115
-rw-r--r--javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java10
-rw-r--r--javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java35
-rw-r--r--javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java2
-rw-r--r--javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java4
-rw-r--r--javatests/com/google/gerrit/acceptance/api/project/AccessIT.java10
-rw-r--r--javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java2
-rw-r--r--javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java8
-rw-r--r--javatests/com/google/gerrit/acceptance/api/project/CommitIT.java51
-rw-r--r--javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java4
-rw-r--r--javatests/com/google/gerrit/acceptance/api/project/ProjectConfigIT.java235
-rw-r--r--javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java5
-rw-r--r--javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java6
-rw-r--r--javatests/com/google/gerrit/acceptance/api/project/SubmitRequirementsAPIIT.java31
-rw-r--r--javatests/com/google/gerrit/acceptance/api/revision/BUILD5
-rw-r--r--javatests/com/google/gerrit/acceptance/api/revision/CommentWithFixIT.java1397
-rw-r--r--javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java43
-rw-r--r--javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java16
-rw-r--r--javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java41
-rw-r--r--javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java268
-rw-r--r--javatests/com/google/gerrit/acceptance/git/AbstractGitOverHttpServlet.java2
-rw-r--r--javatests/com/google/gerrit/acceptance/git/AbstractImplicitMergeTest.java142
-rw-r--r--javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java83
-rw-r--r--javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java4
-rw-r--r--javatests/com/google/gerrit/acceptance/git/DirectPushRefUpdateContextIT.java1
-rw-r--r--javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckOnReceiveIT.java (renamed from javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java)23
-rw-r--r--javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitByCherryPickOrRebaseAlwaysIT.java93
-rw-r--r--javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitExperimentsIT.java335
-rw-r--r--javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitIT.java322
-rw-r--r--javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java13
-rw-r--r--javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java2
-rw-r--r--javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java4
-rw-r--r--javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java7
-rw-r--r--javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java45
-rw-r--r--javatests/com/google/gerrit/acceptance/pgm/InitIT.java2
-rw-r--r--javatests/com/google/gerrit/acceptance/pgm/StartStopDaemonIT.java37
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java15
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java3
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/account/CreateAccountIT.java2
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java20
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java11
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java5
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/auth/AuthenticationCheckIT.java18
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java6
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java21
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java67
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java9
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java90
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java32
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java400
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/change/ChangeIncludedInIT.java47
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java4
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java7
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java92
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java36
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java54
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java4
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java4
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java11
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java36
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java10
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/config/BUILD1
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java3
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/config/GetTaskIT.java4
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/config/IndexSnapshotsIT.java228
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/config/KillTaskIT.java3
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/config/ListTasksIT.java2
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java3
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java1
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java3
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java4
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/project/DeleteLabelIT.java3
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/project/GetBranchIT.java9
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java5
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/project/ListLabelsIT.java3
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java3
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/project/ProjectAssert.java3
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/project/SuggestBranchReviewersIT.java6
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java28
-rw-r--r--javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java3
-rw-r--r--javatests/com/google/gerrit/acceptance/server/change/ApprovalCopierIT.java22
-rw-r--r--javatests/com/google/gerrit/acceptance/server/change/BUILD3
-rw-r--r--javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java136
-rw-r--r--javatests/com/google/gerrit/acceptance/server/change/CommentsUtil.java29
-rw-r--r--javatests/com/google/gerrit/acceptance/server/experiments/ExperimentFeaturesIT.java5
-rw-r--r--javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsChangeIdValidationIT.java55
-rw-r--r--javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java3
-rw-r--r--javatests/com/google/gerrit/acceptance/server/index/BUILD7
-rw-r--r--javatests/com/google/gerrit/acceptance/server/index/ReindexIndexVersionIT.java87
-rw-r--r--javatests/com/google/gerrit/acceptance/server/index/change/LuceneChangeIndexerIT.java137
-rw-r--r--javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java90
-rw-r--r--javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java6
-rw-r--r--javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java11
-rw-r--r--javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java4
-rw-r--r--javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java1
-rw-r--r--javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java5
-rw-r--r--javatests/com/google/gerrit/acceptance/server/project/ProjectCacheIT.java1
-rw-r--r--javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java25
-rw-r--r--javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java8
-rw-r--r--javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java8
-rw-r--r--javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsValidationIT.java3
-rw-r--r--javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java10
-rw-r--r--javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java1
-rw-r--r--javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java1
-rw-r--r--javatests/com/google/gerrit/acceptance/server/rules/prolog/PrologRuleEvaluatorIT.java2
-rw-r--r--javatests/com/google/gerrit/acceptance/server/rules/prolog/RulesIT.java31
-rw-r--r--javatests/com/google/gerrit/acceptance/server/util/TaskListenerIT.java8
-rw-r--r--javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java9
-rw-r--r--javatests/com/google/gerrit/acceptance/ssh/CustomIndexIT.java7
-rw-r--r--javatests/com/google/gerrit/acceptance/ssh/SshDaemonIT.java7
-rw-r--r--javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java11
-rw-r--r--javatests/com/google/gerrit/acceptance/ssh/StreamEventsIT.java2
-rw-r--r--javatests/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImplTest.java9
-rw-r--r--javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java1
-rw-r--r--javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java5
-rw-r--r--javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java16
-rw-r--r--javatests/com/google/gerrit/common/data/ParameterizedStringTest.java4
-rw-r--r--javatests/com/google/gerrit/entities/LabelFunctionTest.java11
-rw-r--r--javatests/com/google/gerrit/entities/PatchSetTest.java24
-rw-r--r--javatests/com/google/gerrit/entities/SubmitRecordTest.java10
-rw-r--r--javatests/com/google/gerrit/entities/converter/AccountInputProtoConverterTest.java105
-rw-r--r--javatests/com/google/gerrit/entities/converter/ApplyPatchInputProtoConverterTest.java64
-rw-r--r--javatests/com/google/gerrit/entities/converter/BUILD1
-rw-r--r--javatests/com/google/gerrit/entities/converter/ChangeInputProtoConverterTest.java272
-rw-r--r--javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java13
-rw-r--r--javatests/com/google/gerrit/entities/converter/HumanCommentProtoConverterTest.java130
-rw-r--r--javatests/com/google/gerrit/entities/converter/MergeInputProtoConverterTest.java91
-rw-r--r--javatests/com/google/gerrit/entities/converter/NotifyInfoProtoConverterTest.java68
-rw-r--r--javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java3
-rw-r--r--javatests/com/google/gerrit/git/ObjectIdsTest.java60
-rw-r--r--javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java6
-rw-r--r--javatests/com/google/gerrit/gpg/PublicKeyCheckerTest.java2
-rw-r--r--javatests/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java2
-rw-r--r--javatests/com/google/gerrit/httpd/gitweb/GitwebServletTest.java2
-rw-r--r--javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java2
-rw-r--r--javatests/com/google/gerrit/httpd/raw/IndexPreloadingUtilTest.java1
-rw-r--r--javatests/com/google/gerrit/httpd/raw/StaticModuleTest.java33
-rw-r--r--javatests/com/google/gerrit/index/IndexUpgradeTest.java3
-rw-r--r--javatests/com/google/gerrit/index/SchemaUtilTest.java5
-rw-r--r--javatests/com/google/gerrit/index/query/AndPredicateTest.java4
-rw-r--r--javatests/com/google/gerrit/index/query/OrPredicateTest.java4
-rw-r--r--javatests/com/google/gerrit/index/query/QueryBuilderTest.java3
-rw-r--r--javatests/com/google/gerrit/index/query/QueryProcessorTest.java151
-rw-r--r--javatests/com/google/gerrit/integration/git/GitProtocolV2IT.java18
-rw-r--r--javatests/com/google/gerrit/integration/git/NoAccessSameAsNotFoundIT.java2
-rw-r--r--javatests/com/google/gerrit/integration/git/PushToRefsUsersIT.java2
-rw-r--r--javatests/com/google/gerrit/integration/git/UploadArchiveIT.java6
-rw-r--r--javatests/com/google/gerrit/integration/ssh/NoShellIT.java2
-rw-r--r--javatests/com/google/gerrit/mail/MailHeaderParserTest.java6
-rw-r--r--javatests/com/google/gerrit/metrics/FieldSanitizeProjectNameTest.java4
-rw-r--r--javatests/com/google/gerrit/metrics/dropwizard/DropWizardMetricMakerTest.java10
-rw-r--r--javatests/com/google/gerrit/pgm/BUILD1
-rw-r--r--javatests/com/google/gerrit/pgm/util/LogFileManagerTest.java58
-rw-r--r--javatests/com/google/gerrit/server/BUILD2
-rw-r--r--javatests/com/google/gerrit/server/IdentifiedUserTest.java4
-rw-r--r--javatests/com/google/gerrit/server/account/AccountCacheTest.java37
-rw-r--r--javatests/com/google/gerrit/server/account/AccountResolverTest.java1
-rw-r--r--javatests/com/google/gerrit/server/account/WatchConfigTest.java12
-rw-r--r--javatests/com/google/gerrit/server/cancellation/RequestStateContextTest.java1
-rw-r--r--javatests/com/google/gerrit/server/change/LabelNormalizerTest.java6
-rw-r--r--javatests/com/google/gerrit/server/change/WalkSorterTest.java132
-rw-r--r--javatests/com/google/gerrit/server/config/CachedPreferencesTest.java21
-rw-r--r--javatests/com/google/gerrit/server/config/GerritInstanceNameProviderTest.java65
-rw-r--r--javatests/com/google/gerrit/server/config/RepositoryConfigTest.java4
-rw-r--r--javatests/com/google/gerrit/server/config/ScheduleConfigTest.java1
-rw-r--r--javatests/com/google/gerrit/server/config/SitePathsTest.java4
-rw-r--r--javatests/com/google/gerrit/server/config/UserPreferencesConverterTest.java45
-rw-r--r--javatests/com/google/gerrit/server/fixes/fixCalculator/EmptyContentTest.java8
-rw-r--r--javatests/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java12
-rw-r--r--javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java52
-rw-r--r--javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java1
-rw-r--r--javatests/com/google/gerrit/server/git/receive/ReceivePackRefCacheTest.java9
-rw-r--r--javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java2
-rw-r--r--javatests/com/google/gerrit/server/group/db/GroupConfigTest.java20
-rw-r--r--javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java2
-rw-r--r--javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java13
-rw-r--r--javatests/com/google/gerrit/server/index/IndexedFieldTest.java20
-rw-r--r--javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java2
-rw-r--r--javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java3
-rw-r--r--javatests/com/google/gerrit/server/ioutil/RegexListSearcherTest.java7
-rw-r--r--javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java3
-rw-r--r--javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java3
-rw-r--r--javatests/com/google/gerrit/server/mail/send/HumanCommentFormatterTest.java63
-rw-r--r--javatests/com/google/gerrit/server/mail/send/MailSoySauceLoaderTest.java4
-rw-r--r--javatests/com/google/gerrit/server/mail/send/MailSoySauceModuleTest.java4
-rw-r--r--javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java13
-rw-r--r--javatests/com/google/gerrit/server/notedb/ChangeNoteJsonTest.java48
-rw-r--r--javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java16
-rw-r--r--javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java23
-rw-r--r--javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java51
-rw-r--r--javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java65
-rw-r--r--javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java11
-rw-r--r--javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java68
-rw-r--r--javatests/com/google/gerrit/server/notedb/IntBlobTest.java28
-rw-r--r--javatests/com/google/gerrit/server/notedb/OpenRepoTest.java21
-rw-r--r--javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java4
-rw-r--r--javatests/com/google/gerrit/server/patch/DiffOperationsTest.java321
-rw-r--r--javatests/com/google/gerrit/server/patch/IntraLineLoaderTest.java2
-rw-r--r--javatests/com/google/gerrit/server/permissions/DefaultPermissionsMappingTest.java2
-rw-r--r--javatests/com/google/gerrit/server/permissions/RefControlTest.java6
-rw-r--r--javatests/com/google/gerrit/server/project/ProjectConfigTest.java3
-rw-r--r--javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java24
-rw-r--r--javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java44
-rw-r--r--javatests/com/google/gerrit/server/query/account/BUILD3
-rw-r--r--javatests/com/google/gerrit/server/query/account/FakeQueryAccountsTest.java4
-rw-r--r--javatests/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java4
-rw-r--r--javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java1327
-rw-r--r--javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java85
-rw-r--r--javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java49
-rw-r--r--javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java29
-rw-r--r--javatests/com/google/gerrit/server/query/group/BUILD3
-rw-r--r--javatests/com/google/gerrit/server/query/group/FakeQueryGroupsTest.java5
-rw-r--r--javatests/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java5
-rw-r--r--javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java26
-rw-r--r--javatests/com/google/gerrit/server/query/project/BUILD4
-rw-r--r--javatests/com/google/gerrit/server/query/project/FakeQueryProjectsTest.java4
-rw-r--r--javatests/com/google/gerrit/server/query/project/LuceneQueryProjectsTest.java4
-rw-r--r--javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java4
-rw-r--r--javatests/com/google/gerrit/server/restapi/project/TagSorterTest.java83
-rw-r--r--javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java5
-rw-r--r--javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java19
-rw-r--r--javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionCheckTest.java6
-rw-r--r--javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java4
-rw-r--r--javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java3
-rw-r--r--javatests/com/google/gerrit/server/submit/SubmoduleCommitsTest.java1
-rw-r--r--javatests/com/google/gerrit/server/update/BUILD2
-rw-r--r--javatests/com/google/gerrit/server/update/BatchUpdateTest.java253
-rw-r--r--javatests/com/google/gerrit/server/update/RepoViewTest.java1
-rw-r--r--javatests/com/google/gerrit/server/update/context/RefUpdateContextTest.java1
-rw-r--r--javatests/com/google/gerrit/testing/BUILD1
-rw-r--r--javatests/com/google/gerrit/testing/IndexVersionsTest.java3
-rw-r--r--lib/errorprone/BUILD1
-rw-r--r--lib/lucene/BUILD33
-rwxr-xr-xlib/nongoogle_test.sh2
-rw-r--r--package.json8
m---------plugins/delete-project0
m---------plugins/hooks0
-rw-r--r--plugins/package.json24
m---------plugins/plugin-manager0
m---------plugins/replication0
m---------plugins/reviewnotes0
m---------plugins/singleusergroup0
m---------plugins/webhooks0
-rw-r--r--plugins/yarn.lock826
-rw-r--r--polygerrit-ui/README.md18
-rw-r--r--polygerrit-ui/app/api/checks.ts3
-rw-r--r--polygerrit-ui/app/api/plugin.ts1
-rw-r--r--polygerrit-ui/app/api/rest-api.ts29
-rw-r--r--polygerrit-ui/app/api/suggestions.ts38
-rw-r--r--polygerrit-ui/app/constants/constants.ts1
-rw-r--r--polygerrit-ui/app/constants/reporting.ts10
-rw-r--r--polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts157
-rw-r--r--polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.ts97
-rw-r--r--polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts2
-rw-r--r--polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts2
-rw-r--r--polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts5
-rw-r--r--polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts38
-rw-r--r--polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow_test.ts2
-rw-r--r--polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts45
-rw-r--r--polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts29
-rw-r--r--polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow_test.ts71
-rw-r--r--polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts55
-rw-r--r--polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts27
-rw-r--r--polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts252
-rw-r--r--polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts165
-rw-r--r--polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts102
-rw-r--r--polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts48
-rw-r--r--polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts11
-rw-r--r--polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts2
-rw-r--r--polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts35
-rw-r--r--polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts24
-rw-r--r--polygerrit-ui/app/elements/change/gr-comments-summary/gr-comments-summary.ts24
-rw-r--r--polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts5
-rw-r--r--polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts6
-rw-r--r--polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts3
-rw-r--r--polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts21
-rw-r--r--polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts4
-rw-r--r--polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts90
-rw-r--r--polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts1
-rw-r--r--polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts16
-rw-r--r--polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts77
-rw-r--r--polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts5
-rw-r--r--polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.ts2
-rw-r--r--polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts28
-rw-r--r--polygerrit-ui/app/elements/checks/gr-checks-results.ts83
-rw-r--r--polygerrit-ui/app/elements/checks/gr-diff-check-result.ts1
-rw-r--r--polygerrit-ui/app/elements/checks/gr-diff-check-result_test.ts53
-rw-r--r--polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts12
-rw-r--r--polygerrit-ui/app/elements/core/gr-router/gr-page.ts20
-rw-r--r--polygerrit-ui/app/elements/core/gr-router/gr-page_test.ts51
-rw-r--r--polygerrit-ui/app/elements/core/gr-router/gr-router.ts88
-rw-r--r--polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts40
-rw-r--r--polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts6
-rw-r--r--polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts12
-rw-r--r--polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts2
-rw-r--r--polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts10
-rw-r--r--polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts9
-rw-r--r--polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts20
-rw-r--r--polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts154
-rw-r--r--polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts14
-rw-r--r--polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts8
-rw-r--r--polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts9
-rw-r--r--polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts2
-rw-r--r--polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts2
-rw-r--r--polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.ts21
-rw-r--r--polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts39
-rw-r--r--polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.ts12
-rw-r--r--polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts58
-rw-r--r--polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts3
-rw-r--r--polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts7
-rw-r--r--polygerrit-ui/app/elements/shared/gr-button/gr-button.ts1
-rw-r--r--polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts4
-rw-r--r--polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts395
-rw-r--r--polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts221
-rw-r--r--polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts9
-rw-r--r--polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts6
-rw-r--r--polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts2
-rw-r--r--polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts23
-rw-r--r--polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts17
-rw-r--r--polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts119
-rw-r--r--polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts46
-rw-r--r--polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts194
-rw-r--r--polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts19
-rw-r--r--polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts24
-rw-r--r--polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts6
-rw-r--r--polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts6
-rw-r--r--polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.ts3
-rw-r--r--polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts11
-rw-r--r--polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.ts9
-rw-r--r--polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts73
-rw-r--r--polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts3
-rw-r--r--polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts3
-rw-r--r--polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts5
-rw-r--r--polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.ts25
-rw-r--r--polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts417
-rw-r--r--polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts311
-rw-r--r--polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts127
-rw-r--r--polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts31
-rw-r--r--polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts14
-rw-r--r--polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts90
-rw-r--r--polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.ts172
-rw-r--r--polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts2
-rw-r--r--polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts2
-rw-r--r--polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts60
-rw-r--r--polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts10
-rw-r--r--polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section.ts94
-rw-r--r--polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section_test.ts103
-rw-r--r--polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts150
-rw-r--r--polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls_test.ts46
-rw-r--r--polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts41
-rw-r--r--polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts80
-rw-r--r--polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts72
-rw-r--r--polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts127
-rw-r--r--polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts7
-rw-r--r--polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts12
-rw-r--r--polygerrit-ui/app/embed/diff/gr-diff-model/gr-diff-model.ts37
-rw-r--r--polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts206
-rw-r--r--polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts130
-rw-r--r--polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element.ts50
-rw-r--r--polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element_test.ts1375
-rw-r--r--polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts20
-rw-r--r--polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts997
-rw-r--r--polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts149
-rw-r--r--polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts152
-rw-r--r--polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts60
-rw-r--r--polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts30
-rw-r--r--polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts4
-rw-r--r--polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts17
-rw-r--r--polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts2
-rw-r--r--polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts3
-rw-r--r--polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts8
-rw-r--r--polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts16
-rw-r--r--polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts2
-rw-r--r--polygerrit-ui/app/models/change/files-model.ts7
-rw-r--r--polygerrit-ui/app/models/checks/checks-util.ts13
-rw-r--r--polygerrit-ui/app/models/checks/checks-util_test.ts25
-rw-r--r--polygerrit-ui/app/models/comments/comments-model.ts183
-rw-r--r--polygerrit-ui/app/models/plugins/plugins-model.ts8
-rw-r--r--polygerrit-ui/app/models/user/user-model.ts9
-rw-r--r--polygerrit-ui/app/package.json10
-rw-r--r--polygerrit-ui/app/rules.bzl2
-rw-r--r--polygerrit-ui/app/services/flags/flags.ts1
-rw-r--r--polygerrit-ui/app/services/gr-auth/gr-auth.ts28
-rw-r--r--polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts176
-rw-r--r--polygerrit-ui/app/services/gr-auth/gr-auth_mock.ts25
-rw-r--r--polygerrit-ui/app/services/gr-auth/gr-auth_test.ts172
-rw-r--r--polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts14
-rw-r--r--polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts1827
-rw-r--r--polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts959
-rw-r--r--polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts92
-rw-r--r--polygerrit-ui/app/services/scheduler/retry-scheduler.ts10
-rw-r--r--polygerrit-ui/app/services/shortcuts/shortcuts-service.ts6
-rw-r--r--polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts43
-rw-r--r--polygerrit-ui/app/test/test-data-generators.ts26
-rw-r--r--polygerrit-ui/app/test/test-utils.ts5
-rw-r--r--polygerrit-ui/app/types/common.ts38
-rw-r--r--polygerrit-ui/app/types/events.ts1
-rw-r--r--polygerrit-ui/app/utils/change-util.ts32
-rw-r--r--polygerrit-ui/app/utils/change-util_test.ts12
-rw-r--r--polygerrit-ui/app/utils/comment-util.ts12
-rw-r--r--polygerrit-ui/app/utils/display-name-util.ts2
-rw-r--r--polygerrit-ui/app/utils/file-util.ts8
-rw-r--r--polygerrit-ui/app/utils/file-util_test.ts20
-rw-r--r--polygerrit-ui/app/utils/label-util.ts41
-rw-r--r--polygerrit-ui/app/utils/label-util_test.ts26
-rw-r--r--polygerrit-ui/app/utils/location-util.ts22
-rw-r--r--polygerrit-ui/app/utils/url-util.ts11
-rw-r--r--polygerrit-ui/app/utils/url-util_test.ts1
-rw-r--r--polygerrit-ui/app/workers/service-worker-class.ts18
-rw-r--r--polygerrit-ui/app/yarn.lock76
-rw-r--r--polygerrit-ui/yarn.lock1649
-rw-r--r--proto/cache.proto14
-rw-r--r--proto/entities.proto180
-rw-r--r--resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy6
-rwxr-xr-xresources/com/google/gerrit/server/commit-msg_test.sh28
-rw-r--r--resources/com/google/gerrit/server/mail/ChangeFooter.soy2
-rw-r--r--resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy4
-rw-r--r--resources/com/google/gerrit/server/mail/ChangeHeader.soy4
-rw-r--r--resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy4
-rw-r--r--resources/com/google/gerrit/server/mail/ChangeSubject.soy2
-rw-r--r--resources/com/google/gerrit/server/mail/NewChange.soy2
-rw-r--r--resources/com/google/gerrit/server/mail/NewChangeHtml.soy2
-rw-r--r--resources/com/google/gerrit/server/mail/ReplacePatchSet.soy4
-rw-r--r--resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy2
-rw-r--r--resources/com/google/gerrit/server/mime/mime-types.properties7
-rwxr-xr-xresources/com/google/gerrit/server/tools/root/hooks/commit-msg20
-rw-r--r--tools/BUILD27
-rw-r--r--tools/bzl/plugin.bzl6
-rw-r--r--tools/defs.bzl13
-rw-r--r--tools/deps.bzl29
-rwxr-xr-xtools/eclipse/project.py3
-rw-r--r--tools/maven/gerrit-acceptance-framework_pom.xml2
-rw-r--r--tools/maven/gerrit-extension-api_pom.xml2
-rw-r--r--tools/maven/gerrit-plugin-api_pom.xml2
-rw-r--r--tools/maven/gerrit-war_pom.xml2
-rw-r--r--tools/nongoogle.bzl88
-rw-r--r--tools/platforms/Dockerfile21
-rw-r--r--tools/remote-bazelrc10
-rw-r--r--tools/rules_nodejs-5.8.4-node_versions.bzl.patch17
-rwxr-xr-xtools/setup_gjf.sh5
-rw-r--r--tools/util.py2
-rw-r--r--version.bzl2
-rw-r--r--yarn.lock629
1411 files changed, 40024 insertions, 14694 deletions
diff --git a/.bazelrc b/.bazelrc
index a8f1210dcd..96620783d1 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -1,3 +1,7 @@
+# TODO(davido): Migrate all dependencies from WORKSPACE to MODULE.bazel
+# https://issues.gerritcodereview.com/issues/303819949
+common --noenable_bzlmod
+
build --workspace_status_command="python3 ./tools/workspace_status.py"
build --repository_cache=~/.gerritcodereview/bazel-cache/repository
build --action_env=PATH
@@ -47,6 +51,29 @@ build:remote11_gcp --config=remote11
build:remote11_bb --config=config_bb
build:remote11_bb --config=build_java11_shared
+# Builds using remotejdk_21, executes using remotejdk_21 or local_jdk
+build:build_java21_shared --java_language_version=21
+build:build_java21_shared --java_runtime_version=remotejdk_21
+build:build_java21_shared --tool_java_language_version=21
+build:build_java21_shared --tool_java_runtime_version=remotejdk_21
+
+build:java21 --config=build_java21_shared
+
+# Builds and executes on RBE using remotejdk_21
+build:remote21 --config=config_gcp
+build:remote21 --config=build_java21_shared
+
+# Define remote21 configuration alias
+build:remote21_gcp --config=remote21
+
+# Builds and executes on BuildBuddy RBE using remotejdk_11
+build:remote21_bb --config=config_bb
+build:remote21_bb --config=build_java21_shared
+
+# Enable modern C++ features
+build --cxxopt=-std=c++17
+build --host_cxxopt=-std=c++17
+
# Enable strict_action_env flag to. For more information on this feature see
# https://groups.google.com/forum/#!topic/bazel-discuss/_VmRfMyyHBk.
# This will be the new default behavior at some point (and the flag was flipped
@@ -58,9 +85,8 @@ build --announce_rc
test --build_tests_only
test --test_output=errors
-# This option is the default for Bazel 7 that is used on master since
-# change Ie7cb3003d, so this additional config should be removed when
-# merging to master.
-test --incompatible_sandbox_hermetic_tmp
import %workspace%/tools/remote-bazelrc
+
+# User-specific .bazelrc
+try-import %workspace%/user.bazelrc
diff --git a/.bazelversion b/.bazelversion
index 91e4a9f262..66ce77b7ea 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-6.3.2
+7.0.0
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 19a19ddddc..2e90cffc1c 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -264,6 +264,7 @@ Then the effective range permitted to be used by the user is
`-2..+2`, as the user's membership of `Foo Leads` effectively grant
them access to the entire reference space, thanks to the wildcard.
+[[exclusive]]
Gerrit also supports exclusive reference-level access control.
It is possible to configure Gerrit to grant an exclusive ref level
@@ -443,13 +444,13 @@ ref.
[[category_create]]
-=== Create Reference
+=== Create (aka Create Reference)
-The create reference category controls whether it is possible to
-create new references, branches or tags. This implies that the
-reference must not already exist, it's not a destructive permission
-in that you can't overwrite or remove any previously existing
-references (and also discard any commits in the process).
+The create category controls whether it is possible to create new
+references, branches or tags. This implies that the reference must not
+already exist, it's not a destructive permission in that you can't
+overwrite or remove any previously existing references (and also
+discard any commits in the process).
It's probably most common to either permit the creation of a single
branch in many gits (by granting permission on a parent project), or
@@ -484,9 +485,9 @@ git branches are case sensitive, so that sandbox branches containing
`${username}` are still reachable by the users.
[[category_delete]]
-=== Delete Reference
+=== Delete (aka Delete Reference)
-The delete reference category controls whether it is possible to delete
+The delete category controls whether it is possible to delete
references, branches or tags. It doesn't allow any other update of
references.
@@ -550,29 +551,46 @@ Review server.
[[category_owner]]
=== Owner
-The `Owner` category controls which groups can modify the project's
-configuration. Users who are members of an owner group can:
+The `Owner` category on `refs/*` controls which groups own the project,
+i.e. the users who are members of an owner group are called the
+`project owners`.
-* Change the project description
-* Grant/revoke any access rights, including `Owner`
+Project owners can change the link:config-project-config.html[project
+configuration], including:
-To get SSH branch access project owners must grant an access right to a group
-they are a member of, just like for any other user.
+* Granting/revoking any access rights (including the `Owner` access
+ right)
+* Changing the project description
+* Changing the link to the parent project
+* Changing the project options
+* Changing link:config-labels.html[labels]
+* Changing link:config-submit-requirements.html[submit requirements]
-Ownership over a particular branch subspace may be delegated by
-entering a branch pattern. To delegate control over all branches
-that begin with `qa/` to the QA group, add `Owner` category
-for reference `+refs/heads/qa/*+`. Members of the QA group can
-further refine access, but only for references that begin with
-`refs/heads/qa/`. See <<project_owners,project owners>> to find
-out more about this role.
+[NOTE]
+Access rights that are assigned to the magic
+link:#project_owners[Project Owners] group are resolved to the users
+that are project owners by having the `Owner` permission assigned on
+`refs/*`.
+
+[NOTE]
+To get branch access via Git project owners must grant an access right
+to a group they are a member of, just like for any other user.
+[NOTE]
For the `All-Projects` root project any `Owner` access right on
'refs/*' is ignored since this permission would allow users to edit the
global capabilities, which is the same as being able to administrate
the Gerrit server (e.g. the user could assign the `Administrate Server`
capability to the own account).
+Ownership over a particular branch subspace may be delegated by
+entering a branch pattern. E.g. to delegate control over all branches
+that begin with `qa/` to the QA group, add the `Owner` category
+for reference `+refs/heads/qa/*+`. Members of the QA group can
+further refine access, but only for references that begin with
+`refs/heads/qa/`. See <<project_owners,project owners>> to find
+out more about this role.
+
[[category_push]]
=== Push
@@ -1618,6 +1636,7 @@ Here is how these play out in a link:config-project-config.html[project.config]
Access to refs can be blocked, allowed or denied.
+[[block-rule]]
==== BLOCK
For blocking access, all rules marked BLOCK are tested, and if one
@@ -1643,6 +1662,7 @@ Such additions not only bypass BLOCK rules, but they will also grant
permissions when they are processed in the ALLOW/DENY processing, as
described in the next subsection.
+[[allow-rule]]
==== ALLOW
For allowing access, all ALLOW/DENY rules that might apply to a ref
@@ -1656,6 +1676,7 @@ All-Projects.
This ordering lets project owners apply permissions specific to their
project, overwriting the site defaults specified in All-Projects.
+[[deny-rule]]
==== DENY
DENY is processed together with ALLOW.
diff --git a/Documentation/cmd-hook-commit-msg.txt b/Documentation/cmd-hook-commit-msg.txt
index e547822b7e..ccb6d58d96 100644
--- a/Documentation/cmd-hook-commit-msg.txt
+++ b/Documentation/cmd-hook-commit-msg.txt
@@ -56,6 +56,12 @@ change viewed on the web.
The `Change-Id` will not be added if `gerrit.createChangeId` is set
to `false` in the git config.
+The `Change-Id` will not be added to temporary commits created by
+`git commit --fixup` or `git commit --squash`, as well as commits
+with a subject line that begins with a lowercase word followed by
+an exclamation mark (e.g., `nopush!`). To override this behavior,
+set `gerrit.createChangeId` to `always` in the git config.
+
If `gerrit.reviewUrl` is set to the base URL of the Gerrit server that
changes are uploaded to (e.g. `https://gerrit-review.googlesource.com/`)
in the git config, then instead of adding a `Change-Id` trailer, a `Link`
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index fb5904bc81..b276cdf4f6 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -116,7 +116,11 @@ If `SAME_GROUP`, only users who are also members of a group the
current user is a member of are visible.
+
If `VISIBLE_GROUP`, only users who are members of at least one group
-that is visible to the current user are visible.
+that is visible to the current user are visible. To make an account
+visible to all users (e.g. because it is a service account) when
+`VISIBLE_GROUP` is used, create a `Public Users` group that has the
+`Make group visible to all registered users` option enabled and add the
+account as a group member.
+
If `NONE`, no users other than the current user are visible.
+
@@ -526,10 +530,10 @@ If not set, HTTP request's domain is used.
[[auth.cookieSecure]]auth.cookieSecure::
+
-Sets "secure" flag of the authentication cookie. If true, cookies
+Sets "secure" flag of the authentication cookie. If `true`, cookies
will be transmitted only over HTTPS protocol.
+
-By default, false.
+By default, `false`.
[[auth.emailFormat]]auth.emailFormat::
+
@@ -558,7 +562,7 @@ in project.config and create agreement files under
`'$site_path'/static`, so users can actually complete one or
more agreements.
+
-By default this is false (no agreements are used).
+By default this is `false` (no agreements are used).
+
To enable the actual usage of contributor agreement the project
specific config option in the `project.config` must be set:
@@ -567,14 +571,14 @@ receive.requireContributorAgreement].
[[auth.trustContainerAuth]]auth.trustContainerAuth::
+
-If true then it is the responsibility of the container hosting
+If `true` then it is the responsibility of the container hosting
Gerrit to authenticate users. In this case Gerrit will blindly trust
the container.
+
This parameter only affects git over http traffic. If set to false
then Gerrit will do the authentication (using Basic authentication).
+
-By default this is set to false.
+By default this is set to `false`.
[[auth.gitBasicAuthPolicy]]auth.gitBasicAuthPolicy::
@@ -652,7 +656,7 @@ have a non-lower-case username.
+
This parameter only affects git over http and git over SSH traffic.
+
-By default this is set to false.
+By default this is set to `false`.
[[auth.userNameCaseInsensitive]]auth.userNameCaseInsensitive::
+
@@ -679,29 +683,29 @@ accounts exist the migration tool will fail, since the newly computed
note name would be identical and thus conflict. These duplicates thus
have to be deleted manually by deleting the respective external ID.
+
-For newly initialized sites this option defaults to true.
+For newly initialized sites this option defaults to `true`.
+
-Default is false.
+Default is `false`.
[[auth.userNameCaseInsensitiveMigrationMode]]auth.userNameCaseInsensitiveMigrationMode::
+
-Setting migration mode to true allows to fallback to case sensitive
+Setting migration mode to `true` allows to fallback to case sensitive
behaviour if the migrated external ID cannot be found. This allows to
trigger the migration while Gerrit process is running.
+
-Default is false.
+Default is `false`.
[[auth.enableRunAs]]auth.enableRunAs::
+
-If true HTTP REST APIs will accept the `X-Gerrit-RunAs` HTTP request
+If `true` HTTP REST APIs will accept the `X-Gerrit-RunAs` HTTP request
header from any users granted the link:access-control.html#capability_runAs[Run As]
capability. The header and capability permit the authenticated user
to impersonate another account.
+
-If false the feature is disabled and cannot be re-enabled without
+If `false` the feature is disabled and cannot be re-enabled without
editing gerrit.config and restarting the server.
+
-Default is true.
+Default is `true`.
[[auth.allowRegisterNewEmail]]auth.allowRegisterNewEmail::
+
@@ -711,13 +715,13 @@ In addition for the HTTP authentication type
link:#auth.httpemailheader[auth.httpemailheader] must *not* be set to
enable registration of new email addresses.
+
-By default, true.
+By default, `true`.
[[auth.autoUpdateAccountActiveStatus]]auth.autoUpdateAccountActiveStatus::
+
Whether to allow automatic synchronization of an account's inactive flag upon login.
+
-If set to true, upon login, if the authentication back-end reports the account as active,
+If set to `true`, upon login, if the authentication back-end reports the account as active,
the account's inactive flag in NoteDb will be updated to be active.
+
If the authentication back-end reports the account as inactive, the account's flag will be
@@ -725,12 +729,12 @@ updated to be inactive and the login attempt will be blocked. Users enabling thi
should ensure that their authentication back-end is supported. Currently, only
strict 'LDAP' authentication is supported.
+
-In addition, if this parameter is not set, or false, the corresponding scheduled
+In addition, if this parameter is not set, or `false`, the corresponding scheduled
task to deactivate inactive Gerrit accounts will also be disabled. If this
-parameter is set to true, users should also consider configuring the
+parameter is set to `true`, users should also consider configuring the
link:#accountDeactivation[accountDeactivation] section appropriately.
+
-By default, false.
+By default, `false`.
[[auth.skipFullRefEvaluationIfAllRefsAreVisible]]auth.skipFullRefEvaluationIfAllRefsAreVisible::
+
@@ -740,7 +744,7 @@ user has READ permission for all refs.
The full ref filtering would filter out refs for pending edits, private changes
and auto merge commits.
+
-By default, true.
+By default, `true`.
[[cache]]
=== Section cache
@@ -777,7 +781,7 @@ Default is unset, no disk cache.
Whether to enable the computation of disk statistics of persistent caches.
This computation is expensive and requires a long time on larger installations.
+
-By default, false.
+By default, `false`.
[[cache.h2CacheSize]]cache.h2CacheSize::
+
@@ -800,13 +804,13 @@ Common unit suffixes of 'k', 'm', or 'g' are supported.
[[cache.h2AutoServer]]cache.h2AutoServer::
+
-If set to true, enable H2 autoserver mode for the H2-backed persistent cache
+If set to `true`, enable H2 autoserver mode for the H2-backed persistent cache
databases.
+
See link:http://www.h2database.com/html/features.html#auto_mixed_mode[here,role=external,window=_blank]
for detail.
+
-Default is false.
+Default is `false`.
[[cache.openFiles]]cache.openFiles::
+
@@ -1310,7 +1314,7 @@ necessary. To maintain backwards compatibility with prior versions,
this setting will fallback to `cache.diff.intraline` if not set in the
configuration.
+
-Default is true, enabled.
+Default is `true`, enabled.
[[cache.projects.loadOnStartup]]cache.projects.loadOnStartup::
+
@@ -1320,12 +1324,12 @@ The cache is loaded concurrently. Admins should ensure that the cache
size set under <<cache.name.memoryLimit,cache.projects.memoryLimit>>
is not smaller than the number of repos.
+
-Default is false, disabled.
+Default is `false`, disabled.
[[cache.projects.loadThreads]]cache.projects.loadThreads::
+
Only relevant if <<cache.projects.loadOnStartup,cache.projects.loadOnStartup>>
-is true.
+is `true`.
+
The number of threads to allocate for loading the cache at startup. These
threads will die out after the cache is loaded.
@@ -1348,6 +1352,33 @@ the project_list cache warmer.
Default is 00:00 if the project_list cache warmer is enabled.
+[[cachePruning]]
+=== Section cachePruning
+
+[[cachePruning.pruneOnStartup]]cachePruning.pruneOnStartup::
++
+Whether to asynchronously prune all cache when starting Gerrit.
++
+Defaults to `true`.
+
+[[cachePruning.startTime]]cachePruning.startTime::
++
+The link:#schedule-configuration-startTime[start time] for running
+cache pruning.
++
+Defaults to `01:00`.
+
+[[cachePruning.interval]]cachePruning.interval::
++
+The link:#schedule-configuration-interval[interval] for running
+cache pruning.
++
+Defaults to `1d`.
+
+link:#schedule-configuration-examples[Schedule examples] can be found
+in the link:#schedule-configuration[Schedule Configuration] section.
+
+
[[capability]]
=== Section capability
@@ -1380,7 +1411,7 @@ automatically be added to the administrator group and hence get the
`administrateServer` capability assigned. This is useful to bootstrap
the link:config-accounts.html[account data].
+
-Default is true.
+Default is `true`.
[[change]]
@@ -1388,9 +1419,9 @@ Default is true.
[[change.allowBlame]]change.allowBlame::
+
-Allow blame on side by side diff. If set to false, blame cannot be used.
+Allow blame on side by side diff. If set to `false`, blame cannot be used.
+
-Default is true.
+Default is `true`.
[[change.cacheAutomerge]]change.cacheAutomerge::
+
@@ -1400,13 +1431,13 @@ cached in the change repository, or if only the diff is cached in the persistent
diff caches (`"git_modified_files"`, `modified_files`, `"git_file_diff"`,
`"file_diff"`).
+
-If true, automerge results are stored in the repository under
+If `true`, automerge results are stored in the repository under
`refs/cache-automerge/*`; the results of diffing the change against its
-automerge base are stored in the diff caches. If false, no extra data is
+automerge base are stored in the diff caches. If `false`, no extra data is
stored in the repository, only the diff caches. This can result in slight
performance improvements by reducing the number of refs in the repo.
+
-Default is true.
+Default is `true`.
[[change.commentSizeLimit]]change.commentSizeLimit::
+
@@ -1434,9 +1465,9 @@ The default limit is 3MiB.
[[change.disablePrivateChanges]]change.disablePrivateChanges::
+
-If set to true, users are not allowed to create private changes.
+If set to `true`, users are not allowed to create private changes.
+
-The default is false.
+The default is `false`.
[[change.maxComments]]change.maxComments::
+
@@ -1527,12 +1558,12 @@ Default is `NEVER`.
This setting determines when Gerrit renders conflict changes section on change
screen and also supports `conflicts` predicate. This computation is expensive,
computing ConflictsPredicate has a runtime complexity of O(nˆ2) with n number
-of open changes on a branch. When set to false GUI will silently ignore the
+of open changes on a branch. When set to `false` GUI will silently ignore the
error message and leave the conflict changes section on change screen empty.
See also implications on rendering of conflict changes section in configuration
section:link:#change.mergeabilityComputationBehavior[change.mergeabilityComputationBehavior].
-Default is true.
+Default is `true`.
[[change.maxSubmittableAtOnce]]change.maxSubmittableAtOnce::
+
@@ -1555,29 +1586,43 @@ into the new branch, although these changes have not been moved to that
branch (see details in
link:https://issues.gerritcodereview.com/issues/40009784[issue 40009784]).
+
-By default true.
+By default `true`.
[[change.enableRobotComments]]change.enableRobotComments::
+
Are robot comments enabled in the Gerrit UI? This setting allows phasing out
robot comments.
+
-By default true.
+By default `true`.
[[change.propagateSubmitRequirementErrors]]change.propagateSubmitRequirementErrors::
+
-If a SubmitRequirement evaluation for a given change results in an
-ERROR status, abort the REST response with an HTTP 500 error.
-+
-The ERROR status can occur if a SubmitRequirement uses a
-plugin-provided predicate (and the plugin is not available), due to
-bugs, or due to bypassing the validation that normally happens when
-updating `refs/meta/config`.
-+
-Enabling this flag makes gerrit unusuable under such conditions, so
-it is generally not recommended. However, this makes the
-application-specific ERROR status into a generic HTTP error, and can
-thus be acted on by automated deployment and monitoring infrastructure.
+If set, requests that access the submit requirements of a change fail with an
+HTTP 500 error if the change has a submit requirement with a non-parseable
+expression that would otherwise result in an
+link:config-submit-requirements#status-error[ERROR] status for the submit
+requirement.
++
+Submit requirement expressions can become non-parseable due to bypassing the
+validation that normally happens when updating the project configuration in
+the `refs/meta/config` branch, or due to bugs in Gerrit.
++
+A special case are expressions that use plugin-provided predicates. If any
+plugin that provides a predicate fails to load (e.g. due to an error in the
+plugin) the predicate can no longer be resolved and expressions that are using
+it can no longer be parsed. This is an error that requires the attention of the
+team that operates Gerrit, but in order to get notified when this happens the
+operation team would need to setup custom monitoring that observes whether
+link:config-submit-requirements#status-error[ERROR] statuses are returned for
+submit requirements. Instead this config option can be used to make
+non-parseable submit requirement expressions cause HTTP 500 errors which
+triggers the automatic alerting for errors that Gerrit operation teams usually
+have in place. This allows the operation team to react quickly when this
+happens.
++
+The drawback of enabling this option is that it causes requests to fail rather
+than handling parsing errors gracefully, which can make Gerrit for impacted
+users unusable.
[[change.robotCommentSizeLimit]]change.robotCommentSizeLimit::
+
@@ -1590,18 +1635,18 @@ The default limit is 1MiB.
[[change.sendNewPatchsetEmails]]change.sendNewPatchsetEmails::
+
-When false, emails will not be sent to owners, reviewers, and cc for
+When `false`, emails will not be sent to owners, reviewers, and cc for
creating a new patchset unless they are project watchers or have starred
the change.
+
-Default is true.
+Default is `true`.
[[change.showAssigneeInChangesTable]]change.showAssigneeInChangesTable::
+
-Show assignee field in changes table. If set to false, assignees will
+Show assignee field in changes table. If set to `false`, assignees will
not be visible in changes table.
+
-Default is false.
+Default is `false`.
[[change.strictLabels]]change.strictLabels::
+
@@ -1609,7 +1654,7 @@ Reject invalid label votes: invalid labels or invalid values. This
configuration option is provided for backwards compatibility and may
be removed in future gerrit versions.
+
-Default is false.
+Default is `false`.
[[change.submitLabel]]change.submitLabel::
+
@@ -1654,7 +1699,7 @@ Defaults to "Submit whole topic"
[[change.submitTopicTooltip]]change.submitTopicTooltip::
+
-If `change.submitWholeTopic` is configured to true and a change has a
+If `change.submitWholeTopic` is configured to `true` and a change has a
topic, this configuration determines the tooltip for the submit button
instead of `change.submitTooltip`. The variable `${topicSize}` is available
for the number of changes in the same topic to be submitted. The number of
@@ -1669,7 +1714,7 @@ changes related by topic)".
Determines if the submit button submits the whole topic instead of
just the current change.
+
-Default is false.
+Default is `false`.
[[change.updateDelay]]change.updateDelay::
+
@@ -1698,7 +1743,7 @@ common parents will be shown as well.
This setting takes effect when generating the automerge, which happens on upload.
Changing the setting leaves existing changes unaffected.
+
-Default is false.
+Default is `false`.
[[change.maxFileSizeDiff]]change.maxFileSizeDiff::
+
@@ -1707,20 +1752,20 @@ link:rest-api-changes.html#get-diff[file diff] request will fail.
+
If not set or set to zero, no limits are applied on file sizes.
-[[change.skipCurrentRulesEvaluationOnClosedChanges]]
+[[change.skipCurrentRulesEvaluationOnClosedChanges]]change.skipCurrentRulesEvaluationOnClosedChanges::
+
-If false, Gerrit will always take latest project configuration to
+If `false`, Gerrit will always take latest project configuration to
compute submit labels. This means that, closed changes (either merged
or abandoned) will be evaluated against the latest configuration which
may produce different results. Especially for merged changes, they may
look like they didn't meet the submit requirements.
+
-When true, evaluation will be skipped and Gerrit will show the
+When `true`, evaluation will be skipped and Gerrit will show the
exact status of submit labels when change was submitted. Post-review
votes will only be allowed on labels that were configured when change
was closed.
+
-Default it false.
+Default is `false`.
[[changeCleanup]]
=== Section changeCleanup
@@ -1851,6 +1896,10 @@ to changes which reference it. The second configuration 'bugzilla'
will hyperlink terms such as 'bug 42' to an external bug tracker,
supplying the argument record number '42' for display.
+Before matching is done the relevant contents are html-escaped. If 'match' needs
+to contain `&`, `<`, `>`, `"` or `'`, replace them with `&amp;`, `&gt;`,
+`&lt;`, `&quot;` and `&apos;` respectively.
+
commentlinks supports link:#reloadConfig[configuration reloads]. Though a
link:cmd-flush-caches.html[flush-caches] of "projects" is needed for the
commentlinks to be immediately available in the UI.
@@ -1927,7 +1976,7 @@ Whether the comment link is enabled. A child project may override a
section in a parent or the site-wide config that is disabled by
specifying `enabled = true`.
+
-By default, true.
+By default, `true`.
+
Note that the names and contents of disabled sections are visible even
to anonymous users via the
@@ -1986,7 +2035,7 @@ options.
[[container.replica]]container.replica::
+
-Used on Gerrit replica installations. If set to true the Gerrit JVM is
+Used on Gerrit replica installations. If set to `true` the Gerrit JVM is
called with the '--replica' switch, enabling replica mode. If no value is
set (or any other value), Gerrit defaults to primary mode enabling write
operations.
@@ -2119,18 +2168,18 @@ Common unit suffixes of 'k', 'm', or 'g' are supported.
[[core.packedGitMmap]]core.packedGitMmap::
+
-When true, JGit will use `mmap()` rather than `malloc()+read()`
+When `true`, JGit will use `mmap()` rather than `malloc()+read()`
to load data from pack files. The use of mmap can be problematic
on some JVMs as the garbage collector must deduce that a memory
mapped segment is no longer in use before a call to `munmap()`
can be made by the JVM native code.
+
In server applications (such as Gerrit) that need to access many
-pack files, setting this to true risks artificially running out
+pack files, setting this to `true` risks artificially running out
of virtual address space, as the garbage collector cannot reclaim
unused mapped spaces fast enough.
+
-Default on JGit is false. Although potentially slower, it yields
+Default on JGit is `false`. Although potentially slower, it yields
much more predictable behavior.
[[core.asyncLoggingBufferSize]]core.asyncLoggingBufferSize::
@@ -2152,7 +2201,7 @@ link:http://codicesoftware.blogspot.com/2011/09/merge-recursive-strategy.html[
blog,role=external,window=_blank], the recursive merge produces better results if the two commits
that are merged have more than one common predecessor.
+
-Default is true.
+Default is `true`.
[[core.repositoryCacheCleanupDelay]]core.repositoryCacheCleanupDelay::
+
@@ -2189,7 +2238,7 @@ refs are checked and potentially read at least once per request
(lazily) if needed. This helps reduce the overhead of checking if
the packed-refs file is outdated.
+
-Default is true.
+Default is `true`.
[[dashboard]]
=== Section dashboard
@@ -2354,7 +2403,7 @@ through Web-UI should run in aggressive mode or not. Aggressive garbage
collections are more expensive but may lead to significantly smaller
repositories.
+
-Valid values are "true" and "false," default is "false".
+Valid values are "`true`" and "`false`," default is "`false`".
[[gc.startTime]]gc.startTime::
+
@@ -2450,7 +2499,7 @@ link:rest-api-accounts.html#list-gpg-keys[REST API endpoints] and web UI
for editing GPG keys. If disabled, GPG keys can only be added by
administrators with direct git access to All-Users.
+
-Defaults to true.
+Defaults to `true`.
[[gerrit.installCommitMsgHookCommand]]gerrit.installCommitMsgHookCommand::
+
@@ -2539,14 +2588,14 @@ Example:
Enable rendering of project list from the secondary index instead
of purely relying on the in-memory cache.
+
-By default false.
+By default `false`.
+
[NOTE]
-The in-memory cache (set to false) rendering provides an **unlimited list** as a result
+The in-memory cache (set to `false`) rendering provides an **unlimited list** as a result
of the list project API, causing the full list of projects to be
returned as a result of the link:rest-api-projects.html[/projects/] REST API
or the link:cmd-ls-projects.html[gerrit ls-projects] SSH command.
-When the rendering from the secondary index (set to true),
+When the rendering from the secondary index (set to `true`),
the **list is limited** by the global capability
link:access-control.html#capability_queryLimit[queryLimit]
which is defaulted to 500 entries.
@@ -2578,7 +2627,7 @@ should set this to the URL of their issue tracker, if necessary.
Record actual peer IP address in ref log entry for identified user.
-Defaults to false.
+Defaults to `false`.
[[gerrit.secureStoreClass]]gerrit.secureStoreClass::
+
@@ -2593,9 +2642,9 @@ If not specified, the default no-op implementation is used.
[[gerrit.canLoadInIFrame]]gerrit.canLoadInIFrame::
+
For security reasons Gerrit will always jump out of iframe.
-Setting this option to true will prevent this behavior.
+Setting this option to `true` will prevent this behavior.
+
-By default false.
+By default `false`.
[[gerrit.xframeOption]]gerrit.xframeOption::
+
@@ -2609,7 +2658,7 @@ Available values:
1. ALLOW - The page can be displayed in a frame.
2. SAMEORIGIN - The page can only be displayed in a frame on the same origin as the page itself.
+
-If link:#gerrit.canLoadInIFrame is set to false this option is ignored and the
+If link:#gerrit.canLoadInIFrame is set to `false` this option is ignored and the
`X-Frame-Options` header is always set to `DENY`.
Setting this option to `ALLOW` will cause the `X-Frame-Options` header to be omitted
the the page can be displayed in a frame.
@@ -2651,17 +2700,17 @@ then its next version N+1 is v3.3 (All-Projects:refs/meta/version=184).
Allow Gerrit to start even if the underlying schema version has been bumped to
the next Gerrit version.
+
-Set to true if Gerrit is installed in
+Set to `true` if Gerrit is installed in
[high-availability configuration](https://gerrit.googlesource.com/plugins/high-availability/+/refs/heads/master/README.md)
during the rolling upgrade to the next version.
+
-By default false.
+By default `false`.
+
The rolling upgrade process, at high level, assumes that Gerrit is installed
on two or more nodes sharing the repositories over NFS. The upgrade is composed
of the following steps:
+
-1. Set gerrit.experimentalRollingUpgrade to true on all Gerrit masters
+1. Set gerrit.experimentalRollingUpgrade to `true` on all Gerrit masters
2. Set the first master unhealthy
3. Shutdown the first master and [upgrade](install.html#init) to the next version
4. Startup the first master, wait for the online reindex to complete (where applicable)
@@ -2679,7 +2728,7 @@ the rolling upgrade is possible or not and the associated constraints.
ServerId of the repositories imported from other Gerrit servers. Changes coming
associated with the imported serverIds are indexed and displayed in the UI
but they are not searchable by `changeNumber` therefore the
-`index.cacheQueryResultsByChangeNum` must also be set to false.
+`index.cacheQueryResultsByChangeNum` must also be set to `false`.
Imported changes are still discoverable in any other ways, for example:
project:someproject branch:main changeId:I78a7add1fe2597cad788c833d8f771f09b54cf33
@@ -2851,7 +2900,7 @@ Valid values are `true` and `false`. The default is `true`.
[[groups.auditLog.ignoreRecordsFromUnidentifiedUsers]]groups.auditLog.ignoreRecordsFromUnidentifiedUsers::
+
Controls whether AuditLogReader should ignore commits created by unidentified users.
-If true, then AuditLogReader ignores commits in the refs/groups/* made by unidentified users (i.e.
+If `true`, then AuditLogReader ignores commits in the refs/groups/* made by unidentified users (i.e.
when the author of a commit can't be parsed as account id).
+
The current version of Gerrit writes identified users as authors for new refs/groups/* commits.
@@ -2859,9 +2908,9 @@ However, some old versions used a server identity as the author (e.g. "Gerrit Co
<server@googlesource.com>") for such commits. Such string can't be converted to account id but
usually the commit shouldn't be ignored.
+
-By default, false.
+By default, `false`.
+
-Setting it to true may lead to some unexpected results in audit log and must be set carefully.
+Setting it to `true` may lead to some unexpected results in audit log and must be set carefully.
[[groups.includeExternalUsersInRegisteredUsersGroup]]groups.includeExternalUsersInRegisteredUsersGroup::
+
@@ -2873,14 +2922,14 @@ This setting only makes sense if you run custom code (e.g. from a plugin
or a custom authentication backend). By default, Gerrit core always requires
users to register and doesn't use external users.
+
-By default, true.
+By default, `true`.
[[groups.newGroupsVisibleToAll]]groups.newGroupsVisibleToAll::
+
Controls whether newly created groups should be by default visible to
all registered users.
+
-By default, false.
+By default, `false`.
[[groups.uuid.name]]groups.<uuid>.name::
+
@@ -2995,7 +3044,7 @@ appear in the http.proxy property above.
[[http.addUserAsRequestAttribute]]http.addUserAsRequestAttribute::
+
-If true, 'User' attribute will be added to the request attributes so it
+If `true`, 'User' attribute will be added to the request attributes so it
can be accessed outside the request scope (will be set to username or id
if username not configured).
+
@@ -3010,15 +3059,15 @@ print user in the httpd_log.
Pattern to print user in Tomcat AccessLog.
+
-Default value is true.
+Default value is `true`.
[[http.addUserAsResponseHeader]]http.addUserAsResponseHeader::
+
-If true, the header 'User' will be added to the list of response headers so it
+If `true`, the header 'User' will be added to the list of response headers so it
can be accessed from a reverse proxy for logging purposes.
+
-Default value is false.
+Default value is `false`.
[[httpd]]
=== Section httpd
@@ -3147,12 +3196,12 @@ By default, `\http://*:8080`.
[[httpd.reuseAddress]]httpd.reuseAddress::
+
-If true, permits the daemon to bind to the port even if the port
-is already in use. If false, the daemon ensures the port is not
+If `true`, permits the daemon to bind to the port even if the port
+is already in use. If `false`, the daemon ensures the port is not
in use before starting. Busy sites may need to set this to true
to permit fast restarts.
+
-By default, true.
+By default, `true`.
[[httpd.gracefulStopTimeout]]httpd.gracefulStopTimeout::
+
@@ -3171,11 +3220,11 @@ By default, 0 seconds (immediate shutdown).
[[httpd.inheritChannel]]httpd.inheritChannel::
+
-If true, permits the daemon to inherit its server socket channel
-from fd0/1(stdin/stdout). When set to true, the server can be socket
+If `true`, permits the daemon to inherit its server socket channel
+from fd0/1(stdin/stdout). When set to `true`, the server can be socket
activated via systemd or xinetd.
+
-By default, false.
+By default, `false`.
[[httpd.requestHeaderSize]]httpd.requestHeaderSize::
+
@@ -3246,8 +3295,8 @@ link:logs.html#_httpd_log[here].
`log4j.appender` with the name `httpd_log` can be configured to overwrite
programmatic configuration.
+
-By default, true if httpd.listenUrl uses http:// or https://,
-and false if httpd.listenUrl uses proxy-http:// or proxy-https://.
+By default, `true` if httpd.listenUrl uses http:// or https://,
+and `false` if httpd.listenUrl uses proxy-http:// or proxy-https://.
[[httpd.acceptorThreads]]httpd.acceptorThreads::
+
@@ -3399,7 +3448,7 @@ bundled with the .war will be used instead.
+
Enable (or disable) registration of Jetty MBeans for Java JMX.
+
-By default, false.
+By default, `false`.
[[index]]
=== Section index
@@ -3443,7 +3492,7 @@ This improves the performance of queries that are returning Changes duplicates.
It needs to be turned off when having Changes imported from other servers
because of the potential conflicts of change numbers.
+
-Defaults to true.
+Defaults to `true`.
[[index.onlineUpgrade]]index.onlineUpgrade::
+
@@ -3452,10 +3501,10 @@ running. This is recommended as it prevents additional downtime during
Gerrit version upgrades (avoiding the need for an offline reindex step
using Reindex), but can add additional server load during the upgrade.
+
-If set to false, there is no way to upgrade the index schema to take
+If set to `false`, there is no way to upgrade the index schema to take
advantage of new search features without restarting the server.
+
-Defaults to true.
+Defaults to `true`.
[[index.excludeProjectFromChangeReindex]]index.excludeProjectFromChangeReindex::
+
@@ -3466,6 +3515,21 @@ projects.
Excluded projects can later be reindexed by for example using the
link:cmd-index-changes-in-project.html[index changes in project command].
+[[index.reuseExistingDocuments]]index.reuseExistingDocuments::
++
+Whether to reuse index documents that already exist during reindexing.
++
+Currently, only supported by the changes index.
++
+This feature is useful, if the Gerrit server has to be restarted
+during an ongoing index online upgrade, since this would cause
+a complete reindexing otherwise that might take an extensive time.
++
+Each existing document in the index will be checked for staleness
+and reindexed if found to be stale.
++
+Defaults to false.
+
[[index.paginationType]]index.paginationType::
+
The pagination type to use when index queries are repeated to
@@ -3583,11 +3647,11 @@ Defaults to 1024.
[[index.autoReindexIfStale]]index.autoReindexIfStale::
+
Whether to automatically check if a document became stale in the index
-immediately after indexing it. If false, there is a race condition during two
+immediately after indexing it. If `false`, there is a race condition during two
simultaneous writes that may cause one of the writes to not be reflected in the
index. The check to avoid this does consume some resources.
+
-Defaults to false.
+Defaults to `false`.
[[index.indexChangesAsync]]index.indexChangesAsync::
+
@@ -3597,7 +3661,7 @@ This has an advantage of faster UI (because indexing latency does not contribute
to the write request latency) and disadvantage that the indexing result might not be
immediately available after the write request.
+
-Defaults to false.
+Defaults to `false`.
[[index.scheduledIndexer]]
==== Subsection index.scheduledIndexer
@@ -3739,7 +3803,7 @@ so merges do not fall behind. See the
link:https://lucene.apache.org/core/5_5_0/core/org/apache/lucene/index/ConcurrentMergeScheduler.html#enableAutoIOThrottle()[
Lucene documentation,role=external,window=_blank] for further details.
+
-Defaults to true (throttling enabled).
+Defaults to `true` (throttling enabled).
During offline reindexing, setting ramBufferSize greater than the size
of index (size of specific index folder under <site_dir>/index) and
@@ -3801,7 +3865,7 @@ case is to re-trigger CI build from the change screen by adding a comment with
specific content, e.g.: `recheck`. Jenkins Gerrit Trigger plugin and Zuul CI
depend on this feature to trigger change verification.
+
-By default, true.
+By default, `true`.
[[event.stream-events.enableRefUpdatedEvents]]event.stream-events.enableRefUpdatedEvents::
+
@@ -3811,7 +3875,7 @@ This allows event listeners to react on a ref update.
Please consider switching to `batch-ref-updated` event which provides better control on grouping and
preserving order of the ref updates.
+
-By default, true.
+By default, `true`.
[[event.stream-events.enableBatchRefUpdatedEvents]]event.stream-events.enableBatchRefUpdatedEvents::
+
@@ -3819,9 +3883,9 @@ Enable streaming of `batch-ref-updated` event which represents group of
refs updated during a single batch ref update operation.
Single ref updates are also streamed as a `batch-ref-updated` events with a single ref specified.
This allows event listeners to react on all ref updated events and disable individual `ref-updated`
-events by setting <<event.stream-events.enableRefUpdatedEvents, event.stream-events.enableRefUpdatedEvents>> to false.
+events by setting <<event.stream-events.enableRefUpdatedEvents, event.stream-events.enableRefUpdatedEvents>> to `false`.
+
-By default, false.
+By default, `false`.
[[event.stream-events.enableDraftCommentEvents]]event.stream-events.enableDraftCommentEvents::
+
@@ -3832,7 +3896,7 @@ NOTE: Due to the nature of drafts, the amount of `ref-updated` events created on
The extra amount of events depends on the usage pattern of the installation. It is worth evaluating
the amount of extra events produced before enabling this flag by counting the calls to the draft APIs.
+
-By default, false.
+By default, `false`.
[[experiments]]
=== Section experiments
@@ -3905,7 +3969,7 @@ Please note that projects rarely used and thus not cached may be
temporarily inaccessible by users even with LDAP membership and grants
referenced in the ACLs.
+
-By default, true.
+By default, `true`.
[[ldap.server]]ldap.server::
+
@@ -3924,31 +3988,31 @@ See https://issues.gerritcodereview.com/issues/40010644[issue 40010644].
[[ldap.startTls]]ldap.startTls::
+
-If true, Gerrit will perform StartTLS extended operation.
+If `true`, Gerrit will perform StartTLS extended operation.
+
-By default, false, StartTLS will not be enabled.
+By default, `false`, StartTLS will not be enabled.
[[ldap.supportAnonymous]]ldap.supportAnonymous::
+
-If false, Gerrit will provide credentials only at connection open, this is
+If `false`, Gerrit will provide credentials only at connection open, this is
required for some `LDAP` implementations that do not allow anonymous bind
for StartTLS or for reauthentication.
+
-By default, true.
+By default, `true`.
[[ldap.sslVerify]]ldap.sslVerify::
+
-If false and ldap.server is an `ldaps://` style URL or `ldap.startTls`
-is true, Gerrit will not verify the server certificate when it connects
+If `false` and ldap.server is an `ldaps://` style URL or `ldap.startTls`
+is `true`, Gerrit will not verify the server certificate when it connects
to perform a query.
+
-By default, true, requiring the certificate to be verified.
+By default, `true`, requiring the certificate to be verified.
[[ldap.groupsVisibleToAll]]ldap.groupsVisibleToAll::
+
-If true, LDAP groups are visible to all registered users.
+If `true`, LDAP groups are visible to all registered users.
+
-By default, false, LDAP groups are visible only to administrators and
+By default, `false`, LDAP groups are visible only to administrators and
group members.
[[ldap.username]]ldap.username::
@@ -4185,7 +4249,7 @@ By default, unset.
+
Converts the local username, that is used to login into the Gerrit
Web UI, to lower case before doing the LDAP authentication. By setting
-this parameter to true, a case insensitive login to the Gerrit Web UI
+this parameter to `true`, a case insensitive login to the Gerrit Web UI
can be achieved.
+
If set, it must be ensured that the local usernames for all existing
@@ -4198,7 +4262,7 @@ Please be aware that the conversion of the local usernames to lower
case can't be undone. For newly created accounts the local username
will be directly stored in lower case.
+
-By default, unset/false.
+By default, unset/`false`.
[[ldap.authentication]]ldap.authentication::
+
@@ -4217,7 +4281,7 @@ KerberosLogin {
required
useTicketCache=true
doNotPrompt=true
- renewTGT=true;
+ renewTGT=`true`;
};
----
@@ -4236,7 +4300,7 @@ have local administrator privileges.
+
_(Optional)_ Enable the LDAP connection pooling or not.
+
-If it is true, the LDAP service provider maintains a pool of (possibly)
+If it is `true`, the LDAP service provider maintains a pool of (possibly)
previously used connections and assigns them to a Context instance as
needed. When a Context instance is done with a connection (closed or
garbage collected), the connection is returned to the pool for future use.
@@ -4245,7 +4309,7 @@ For details, see link:http://docs.oracle.com/javase/tutorial/jndi/ldap/pool.html
LDAP connection management (Pool),role=external,window=_blank] and link:http://docs.oracle.com/javase/tutorial/jndi/ldap/config.html[
LDAP connection management (Configuration),role=external,window=_blank]
+
-By default, false.
+By default, `false`.
[[ldap.connectTimeout]]ldap.connectTimeout::
+
@@ -4291,7 +4355,7 @@ By default unset.
[[log.jsonLogging]]log.jsonLogging::
+
-If set to true, enables error, ssh and http logging in JSON format (file names:
+If set to `true`, enables error, ssh and http logging in JSON format (file names:
`logs/error_log.json`, `logs/sshd_log.json` and `logs/httpd_log.json`).
+
The option only applies to Gerrit built-in loggers. It is ignored when a log4j
@@ -4299,11 +4363,11 @@ configuration is specified via
link:#container.javaOptions[container.javaOptions], for example
`-Dlog4j.configuration=file://etc/log4j.properties`.
+
-Defaults to false.
+Defaults to `false`.
[[log.textLogging]]log.textLogging::
+
-If set to true, enables error logging in regular plain text format. Can only be disabled
+If set to `true`, enables error logging in regular plain text format. Can only be disabled
if `jsonLogging` is enabled.
+
The option only applies to Gerrit built-in loggers. It is ignored when a log4j
@@ -4311,20 +4375,47 @@ configuration is specified via
link:#container.javaOptions[container.javaOptions], for example
`-Dlog4j.configuration=file://etc/log4j.properties`.
+
-Defaults to true.
+Defaults to `true`.
[[log.compress]]log.compress::
+
-If set to true, log files are compressed at server startup and then daily at 11pm
+If set to `true`, log files are compressed at server startup and then daily at 11pm
(in the server's local time zone).
+
-Defaults to true.
+Defaults to `true`.
[[log.rotate]]log.rotate::
+
-If set to true, log files are rotated daily at midnight (GMT).
+If set to `true`, log files are rotated daily at midnight (GMT).
+
-Defaults to true.
+Defaults to `true`.
+
+[[log.daysToKeep]]log.timeToKeep::
++
+Time that logs should be kept until they are being deleted. Values should use common
+suffixes to express their setting:
++
+* d, day, days
+* w, week, weeks (`1 week` is treated as `7 days`)
+* mon, month, months (`1 month` is treated as `30 days`)
+* y, year, years (`1 year` is treated as `365 days`)
++
+The minimum granularity is days. Using a smaller time unit will result in deletion of
+all old logs, as if `0d` would have been configured.
++
+Actively used logs will never be deleted. Thus, this feature only works in combination
+with enabled link:#log.rotate[log.rotate]. Log deletion happens at server startup and
+then daily at 11pm (in the server's local time zone).
++
+Depending on the filesystem the following file times will be used, in order of priority:
++
+* Time of file creation
+* Time when the file was last modified
+* Date added to the filename as part of log file rotation. Time will be set to `00:00:00Z`.
++
+If none of the above is available, the log file won't be deleted.
++
+Defaults to `-1`, i.e. being disabled.
[[metrics]]
=== Section metrics
@@ -4371,13 +4462,13 @@ time-based reservoir types.
[[mimetype.name.safe]]mimetype.<name>.safe::
+
-If set to true, files with the MIME type `<name>` will be sent as
+If set to `true`, files with the MIME type `<name>` will be sent as
direct downloads to the user's browser, rather than being wrapped up
inside of zipped archives. The type name may be a complete type
name, e.g. `image/gif`, a generic media type, e.g. `+image/*+`,
or the wildcard `+*/*+` to match all types.
+
-By default, false for all MIME types.
+By default, `false` for all MIME types.
Common examples:
----
@@ -4442,16 +4533,16 @@ their contact information manually.
[[oauth.allowEditFullName]]oauth.allowEditFullName::
+
-If true, the full name can be edited in the contact information.
+If `true`, the full name can be edited in the contact information.
+
-Default is false.
+Default is `false`.
[[oauth.allowRegisterNewEmail]]oauth.allowRegisterNewEmail::
+
-If true, additional email addresses can be registered in the contact
+If `true`, additional email addresses can be registered in the contact
information.
+
-Default is false.
+Default is `false`.
[[operator-alias]]
=== Section operator alias
@@ -4497,7 +4588,7 @@ memory resources.
[[pack.deltacompression]]pack.deltacompression::
+
-If true, delta compression between objects is enabled. This may
+If `true`, delta compression between objects is enabled. This may
result in a smaller overall transfer for the client, but requires
more server memory and CPU time.
+
@@ -4530,8 +4621,8 @@ Default is 1 minute.
[[plugins.allowRemoteAdmin]]plugins.allowRemoteAdmin::
+
Enable remote installation, enable and disable of plugins over HTTP
-and SSH. If set to true Administrators can install new plugins
-remotely, or disable existing plugins. Defaults to false.
+and SSH. If set to `true` Administrators can install new plugins
+remotely, or disable existing plugins. Defaults to `false`.
[[plugins.mandatory]]plugins.mandatory::
+
@@ -4597,34 +4688,34 @@ Default is 5 minutes.
[[receive.checkMagicRefs]]receive.checkMagicRefs::
+
-If true, Gerrit will verify the destination repository has
+If `true`, Gerrit will verify the destination repository has
no references under the magic 'refs/for' branch namespace. Names under
these locations confuse clients when trying to upload code reviews so
Gerrit requires them to be empty.
+
-If false Gerrit skips the sanity check and assumes administrators
+If `false` Gerrit skips the sanity check and assumes administrators
have ensured the repository does not contain any magic references.
-Setting to false to skip the check can decrease latency during push.
+Setting to `false` to skip the check can decrease latency during push.
+
-Default is true.
+Default is `true`.
[[receive.allowProjectOwnersToChangeParent]]receive.allowProjectOwnersToChangeParent::
+
-If true, Gerrit will allow project owners to change the parent of a project.
+If `true`, Gerrit will allow project owners to change the parent of a project.
+
By default only Gerrit administrators are allowed to change the parent
of a project. By allowing project owners to change parents, it may
allow the owner to circumvent certain enforced rules (like important
BLOCK rules).
+
-Default is false.
+Default is `false`.
+
This value supports configuration reloads:
link:cmd-reload-config.html[reload-config]
[[receive.checkReferencedObjectsAreReachable]]receive.checkReferencedObjectsAreReachable::
+
-If set to true, Gerrit will validate that all referenced objects that
+If set to `true`, Gerrit will validate that all referenced objects that
are not included in the received pack are reachable by the user.
+
Carrying out this check on gits with many refs and commits can be a
@@ -4634,11 +4725,11 @@ be overkill.
Only disable this check if you trust the clients not to forge SHA-1
references to access commits intended to be hidden from the user.
+
-Default is true.
+Default is `true`.
[[receive.enableInMemoryRefCache]]receive.enableInMemoryRefCache::
+
-If true, Gerrit will cache all refs advertised during push in memory and
+If `true`, Gerrit will cache all refs advertised during push in memory and
base later receive operations on that cache.
+
Turning this cache off is considered experimental.
@@ -4647,17 +4738,17 @@ This cache provides value when the ref database is slow and/or does not
offer an inverse lookup of object ID to ref name. When RefTable is used,
this cache can be turned off (experimental) to get speed improvements.
+
-Default is true.
+Default is `true`.
[[receive.enableSignedPush]]receive.enableSignedPush::
+
-If true, server-side signed push validation is enabled.
+If `true`, server-side signed push validation is enabled.
+
When a client pushes with `git push --signed`, this ensures that the
push certificate is valid and signed with a valid public key stored in
the `refs/meta/gpg-keys` branch of `All-Users`.
+
-Defaults to false.
+Defaults to `false`.
[[receive.maxBatchChanges]]receive.maxBatchChanges::
+
@@ -4708,7 +4799,7 @@ Controls whether the project-level link:config-project-config.html[`receive.maxO
value is inherited from the parent project. When `true`, the value is
inherited, otherwise it is not inherited.
+
-Default is false, the value is not inherited.
+Default is `false`, the value is not inherited.
[[receive.maxTrustDepth]]receive.maxTrustDepth::
+
@@ -4907,18 +4998,18 @@ and assumes milliseconds if not specified.
Whether Gerrit should automatically retry operations on failure with tracing
enabled. The automatically generated traces can help with debugging.
+
-By default this is set to false.
+By default this is set to `false`.
[[rules]]
=== Section rules
[[rules.enable]]rules.enable::
+
-If true, Gerrit will load and execute 'rules.pl' files in each
-project's refs/meta/config branch, if present. When set to false,
+If `true`, Gerrit will load and execute 'rules.pl' files in each
+project's refs/meta/config branch, if present. When set to `false`,
only the default internal rules will be used.
+
-Default is true, to execute project specific rules.
+Default is `true`, to execute project specific rules.
[[rules.reductionLimit]]rules.reductionLimit::
+
@@ -4951,7 +5042,7 @@ Maximum input size (in bytes) of a Prolog rules.pl file. Larger
source files may need a larger rules.compileReductionLimit. Consider
using link:pgm-rulec.html[rulec] to precompile larger rule files.
+
-A size of 0 bytes disables rules, same as rules.enable = false.
+A size of 0 bytes disables rules, same as rules.enable = `false`.
+
Common unit suffixes of 'k', 'm', or 'g' are supported.
+
@@ -5045,7 +5136,7 @@ keep the connection with the email server alive and receive a push when a new
email is delivered to the inbox. In this case, Gerrit will process the email
immediately and will not have a fetch delay.
+
-Defaults to false.
+Defaults to `false`.
[[receiveemail.filter.mode]]receiveemail.filter.mode::
+
@@ -5079,18 +5170,18 @@ be a list of addresses when regular expression characters are escaped.
[[sendemail.enable]]sendemail.enable::
+
-If false Gerrit will not send email messages, for any reason,
+If `false` Gerrit will not send email messages, for any reason,
and all other properties of section sendemail are ignored.
+
-By default, true, allowing notifications to be sent.
+By default, `true`, allowing notifications to be sent.
[[sendemail.html]]sendemail.html::
+
-If false, Gerrit will only send plain-text emails.
-If true, Gerrit will send multi-part emails with an HTML and
+If `false`, Gerrit will only send plain-text emails.
+If `true`, Gerrit will send multi-part emails with an HTML and
plain text part.
+
-By default, true, allowing HTML in the emails Gerrit sends.
+By default, `true`, allowing HTML in the emails Gerrit sends.
[[sendemail.connectTimeout]]sendemail.connectTimeout::
+
@@ -5182,11 +5273,11 @@ By default, 'none', indicating no encryption is used.
[[sendemail.sslVerify]]sendemail.sslVerify::
+
-If false and sendemail.smtpEncryption is 'ssl' or 'tls', Gerrit
+If `false` and sendemail.smtpEncryption is 'ssl' or 'tls', Gerrit
will not verify the server certificate when it connects to send
an email message.
+
-By default, true, requiring the certificate to be verified.
+By default, `true`, requiring the certificate to be verified.
[[sendemail.smtpUser]]sendemail.smtpUser::
+
@@ -5223,12 +5314,12 @@ By default, unset, permitting delivery to any email address.
[[sendemail.includeDiff]]sendemail.includeDiff::
+
-If true, new change emails and merged change emails from Gerrit
+If `true`, new change emails and merged change emails from Gerrit
will include the complete unified diff of the change.
Variable maxmimumDiffSize places an upper limit on how large the
email can get when this option is enabled.
+
-By default, false.
+By default, `false`.
[[sendemail.maximumDiffSize]]sendemail.maximumDiffSize::
+
@@ -5278,12 +5369,12 @@ Defaults to an empty list, meaning no additional TLDs are allowed.
[[sendemail.addInstanceNameInSubject]]sendemail.addInstanceNameInSubject::
+
-When set to true, Gerrit will add its short name to the email subject, allowing recipients to quickly identify
+When set to `true`, Gerrit will add its short name to the email subject, allowing recipients to quickly identify
what Gerrit instance the email came from.
+
The short name can be customized via the gerrit.instanceName option.
+
-Defaults to false.
+Defaults to `false`.
[[site]]
@@ -5303,9 +5394,9 @@ By default, unset, denying all cross-origin requests.
[[site.refreshHeaderFooter]]site.refreshHeaderFooter::
+
-If true the server checks the site header, footer and CSS files for
-updated versions. If false, a server restart is required to change
-any of these resources. Default is true, allowing automatic reloads.
+If `true` the server checks the site header, footer and CSS files for
+updated versions. If `false`, a server restart is required to change
+any of these resources. Default is `true`, allowing automatic reloads.
[[ssh-alias]]
=== Section ssh-alias
@@ -5391,7 +5482,7 @@ By default uses the value of `sshd.listenAddress`.
[[sshd.tcpKeepAlive]]sshd.tcpKeepAlive::
+
-If true, enables TCP keepalive messages to the other side, so
+If `true`, enables TCP keepalive messages to the other side, so
the daemon can terminate connections if the peer disappears.
+
Only effective when `sshd.backend` is set to `MINA`.
@@ -5728,7 +5819,7 @@ By default 50.
If link:access-control.html#service_users[service users] should be skipped when
suggesting reviewers.
+
-By default true.
+By default `true`.
[[tracing]]
=== Section tracing
@@ -5748,7 +5839,7 @@ In one recorded case the impact was an overall heap increase of 40%
heap increase wasn't nearly as dramatic and the impact is most likely
dependent on which plugin is used.
+
-By default, false.
+By default, `false`.
[[tracing.traceid]]
==== Subsection tracing.<trace-id>
@@ -6040,7 +6131,7 @@ account deactivations.
Note that the task will only be scheduled if the
link:#autoUpdateAccountActiveStatus[auth.autoUpdateAccountActiveStatus]
-is set to true.
+is set to `true`.
link:#schedule-configuration-examples[Schedule examples] can be found
in the link:#schedule-configuration[Schedule Configuration] section.
@@ -6066,7 +6157,7 @@ By default this is `TRUE`.
+
This allows to enable the superproject subscription mechanism.
+
-By default this is true.
+By default this is `true`.
[[submodule.maxCombinedCommitMessageSize]]submodule.maxCombinedCommitMessageSize::
+
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index ce63295527..29d1b850e3 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -24,7 +24,7 @@ If votes get outdated due to pushing a new patch set the uploader is
informed about this by a message in the git output. In addition,
outdated votes are also listed in the email notification that is sent
for the new patch set (unless this is disabled by a custom email
-template). Note, that the uploader only get this email notification if
+template). Note, that the uploader only gets this email notification if
they have configured `Every Comment` for `Email notifications` in their
user preferences. With any other email preference the email sender, the
uploader in this case, is not included in the email recipients.
@@ -251,7 +251,7 @@ Label functions dictate the rules for requiring certain label votes before a
change is allowed for submission. Label functions are **deprecated** and updates
that set `function` to a blocking value {`MaxWithBlock`, `MaxNoBlock`,
`AnyWithBlock`} will be rejected. Existing label function definitions can only
-be updated to {`NoBlock`, `NoOp`, `PatchSetLock`}. New label defintions should
+be updated to {`NoBlock`, `NoOp`, `PatchSetLock`}. New label definitions should
also explicitly set the `function` attribute to a non-blocking value since the
default is `MaxWithBlock`.
@@ -269,6 +269,7 @@ ignored and may be treated as optional.
Valid values are:
+[[MaxWithBlock]]
* `MaxWithBlock` (default)
+
The lowest possible negative value, if present, blocks a submit, while
@@ -276,22 +277,26 @@ the highest possible positive value is required to enable submit. There
must be at least one positive value, or else submit will never be
enabled. To permit blocking submits, ensure a negative value is defined.
+[[AnyWithBlock]]
* `AnyWithBlock`
+
The label is not mandatory but the lowest possible negative value,
if present, blocks a submit. To permit blocking submits, ensure that a
negative value is defined.
+[[MaxNoBlock]]
* `MaxNoBlock`
+
The highest possible positive value is required to enable submit, but
the lowest possible negative value will not block the change.
+[[NoBlock]]
* `NoBlock`/`NoOp`
+
The label is purely informational and values are not considered when
determining whether a change is submittable.
+[[PatchSetLock]]
* `PatchSetLock`
+
The `PatchSetLock` function provides a locking mechanism for patch
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 187cd0fe1d..81f9d9f313 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -1,95 +1,496 @@
-= Gerrit Code Review - Project Configuration File Format
+= Gerrit Code Review - Project Configuration
-This page explains the storage format of Gerrit's project configuration
-and access control models.
-
-The web UI access control panel is a front end for human-readable
-configuration files under the +refs/meta/config+ namespace in the
-affected project. Direct manipulation of these files is mainly
-relevant in an automation scenario of the access controls.
+Every project has a configuration that defines access rights and controls
+project-specific behavior.
+The project configuration is stored inside the git repository inside the
+`refs/meta/config` branch.
[[refs-meta-config]]
-== The +refs/meta/config+ namespace
+== The `refs/meta/config` branch
+
+The `refs/meta/config` branch contains configuration files. It is disconnected
+from the normal branches under `refs/heads/` that contain the source code.
+
+The files inside the `refs/meta/config` branch are versioned just like any
+other file in the repository. This means from the git history of the
+`refs/meta/config` branch it can be seen how the configuration changed
+over time and which configuration was active when.
+
+[[configuration-files]]
+== Configuration files
+
+The project configuration is stored in the following files (these files are
+stored inside the `refs/meta/config` branch, see
+link:#refs-meta-config[above]):
-The namespace contains three different files that play different
-roles in the permission model. With read permission to that reference,
-it is possible to fetch the +refs/meta/config+ reference to a local
-repository. A nice side effect is that you can also upload changes
-to project permissions and review them just like with regular code
-changes. The preview changes option is also provided on the UI. Please note
-that you will have to configure push rights for the +refs/meta/config+ name
-space if you'd like to use the possibility to automate permission updates.
+* link:#file-project_config[project.config]:
+ Contains the link to the parent project, the project description, access
+ rights, label definitions, submit requirements and options to control
+ project-specific behavior.
+* link:#file-groups[groups]:
+ Resolves group names that are mentioned in `project.config` to group UUIDs.
+* [DEPRECATED] link:#file-rules_pl[rules.pl]:
+ Contains Prolog rules that control when a change becomes ready to submit.
+ Note, Prolog rules are deprecated and have been replaced by
+ link:config-submit-requirements.html[submit requirements].
-== Property inheritance
+In addition, there can be configuration files from Gerrit plugins, usually they
+are named `<PLUGIN-NAME>.config`.
-If a property is set to INHERIT, then the value from the parent project is
-used. If the property is not set in any parent project, the default value is
-FALSE.
+[[update]]
+== Updating the project configuration
+
+There are several possibilities to update the project configuration:
+
+[[update-through-web-ui]]
+* Through the Gerrit web UI:
++
+[[update-through-general-screen]]
+--
+* On the project's `General` screen:
++
+The `General` screen can be found under:
+`BROWSE` > `Repositories` > `<project-name>` > `General`
++
+In the `Configurations` section this screen allows to edit the project
+description and change repository options.
++
+Note, this screen only supports changing the most important repository options,
+but it doesn't expose all options that exist.
++
+All changes are directly applied (without code review).
+--
++
+[[update-through-access-screen]]
+--
+* On the project's `Access` screen:
++
+The `Access` screen can be found under:
+`BROWSE` > `Repositories` > `<project-name>` > `Access`
++
+This screen allows to edit the project's access rights and the parent project.
++
+Updating access rights via the `Access` screen updates the
+link:#file-groups[groups] file automatically.
++
+Modifications can either be applied directly (`SAVE` button) or saved for
+review (`SAVE FOR REVIEW` button).
++
+Saving access modification for review (`SAVE FOR REVIEW` button) creates an
+open change for the `refs/meta/config` branch where the modifications can be
+reviewed by a project owner. The modifications become effective only after a
+project owner submits the change.
+--
++
+[[update-via-online-editing]]
+--
+** Via online editing:
++
+The project's `Commands` screen, that can be found under `BROWSE` >
+`Repositories` > `<project-name>` > `Commands`, offers an `EDIT REPO CONFIG`
+command that allows to edit the `project.config` file directly in the Gerrit
+web UI.
++
+The `EDIT REPO CONFIG` command creates a new work-in-progress change with a
+change edit that contains the `project.config` file. Clicking on the command
+opens an online editor for the `project.config` file, allowing the user to make
+modifications to it.
++
+Modifications need to be saved and published ("saving" saves the modifications
+in the change edit, "publishing" publishes the change edit to make it visible
+to other users). While the change edit is not published yet, it's possible to
+add further files to it (e.g. the link:#file-groups[groups] file if it needs to
+be modified, in contrast to making access rights changes through the `Access`
+screen the `groups` file is not automatically updated). For further details how
+to work with change edits see the link:user-inline-edit.html[inline edit user
+guide]. After publishing the change edit, the change should be set to ready by
+clicking on `START REVIEW` > `SEND AND START REVIEW`. At this time you also
+want to add a project owner as a reviewer so that they can review and approve
+the change.
+--
+
+[[update-by-git-push]]
+* By pushing updates via Git:
++
+Since the configuration files are stored in a git branch, it's possible to
+update them via normal git operations:
++
+--
+1. Clone the repository if you don't have it available yet. The clone command
+ can be found in the `Download` section of project's `General` screen
+ (`BROWSE` > `Repositories` > `<project-name>` > `General`).
+2. Fetch and checkout the `refs/meta/config branch`, e.g. by `git fetch origin
+ refs/meta/config && git checkout FETCH_HEAD`.
+3. Edit the `project.config` file or other configuration files and commit the
+ changes, e.g. by `git commit --all`. Note, since the `project.config` file
+ uses the format of a git config file you can also edit it via the
+ `git config` command (e.g. to set a project description do: `git config -f
+ project.config project.description "My project description"`).
+4. Push the newly created commit, either to update the `refs/meta/config`
+ branch directly without code-review (e.g. `git push origin
+ HEAD:refs/meta/config`), or for review (e.g. `git push origin
+ HEAD:refs/for/refs/meta/config`).
+--
++
+[NOTE]
+Updates to access right may require changes in the link:#file-groups[groups]
+file. In contrast to making access rights changes through the `Access` screen
+the `groups` file is not automatically updated.
+
+[[update-through-rest-api]]
+* Through the Gerrit REST API:
++
+Gerrit offers several REST endpoints to modify the project configuration:
++
+** link:rest-api-projects.html#set-config[Set Config] REST endpoint:
+ Allows to edit the project description and change repository options.
+** link:rest-api-projects.html#set-project-description[Set Project Description]
+ REST endpoint:
+ Allows to set the project description.
+** link:rest-api-projects.html#delete-project-description[Delete Project
+ Description] REST endpoint:
+ Allows to unset the project description.
+** link:rest-api-projects.html#set-access[Add, Update and Delete Access Rights
+ for Project] REST endpoint:
+ Allows to edit the access rights of the project.
+** link:rest-api-projects.html#create-access-change[Create Access Rights Change
+ for review] REST endpoint:
+ Allows to create a change with access right modifications that can be
+ reviewed and submitted by a project owner.
+** link:rest-api-projects.html#create-label[Create Label] REST endpoint:
+ Allows to create a new label definition.
+** link:rest-api-projects.html#set-label[Set Label] REST endpoint:
+ Allows to update an existing label definition.
+** link:rest-api-projects.html#delete-label[Delete Label] REST endpoint:
+ Allows to delete an existing label definition.
+** link:rest-api-projects.html#create-submit-requirement[Create Submit
+ Requirement] REST endpoint:
+ Allows to create a new submit requirement.
+** link:rest-api-projects.html#update-submit-requirement[Update Submit
+ Requirement] REST endpoint:
+ Allows to update an existing submit requirement.
+** link:rest-api-projects.html#delete-submit-requirement[Delete Submit
+ Requirement] REST endpoint:
+ Allows to delete an existing submit requirement.
+
+[[required-permissions]]
+== Required permissions
+
+Depending on how the project configuration is changed different access rights
+are required:
+
+* Direct updates through the web UI (link:#update-through-general-screen[
+ updates through the `General` screen] and link:#update-through-access-screen[
+ direct updates of access rights through the `Access` screen via the `SAVE`
+ button]) and direct updates through the REST API (via the
+ link:rest-api-projects.html#set-config[Set Config] REST endpoint or the
+ link:rest-api-projects.html#set-access[Add, Update and Delete Access Rights
+ for Project] REST endpoint) require the user to be a project owner (have the
+ link:access-control.html#category_owner[Owner] access right assigned on
+ `refs/*` or have the link:access-control.html#capability_administrateServer[
+ Administrate Server] global capability assigned on the `All-Projects` root
+ project).
+* link:#update-by-git-push[Direct updates through `git push`] require the
+ user to have the link:access-control.html#category_push[Push] access right
+ assigned on `refs/meta/config` and be a project owner (have the
+ link:access-control.html#category_owner[Owner] access right assigned on
+ `refs/*` or have the link:access-control.html#capability_administrateServer[
+ Administrate Server] global capability assigned on the `All-Projects` root
+ project).
+* Creating changes for updates through the web UI
+ (link:#update-through-access-screen[proposing updates of access rights
+ through the `Access` screen via the `SAVE FOR REVIEW` button] and
+ link:#update-via-online-editing[proposing updates via the `EDIT REPO CONFIG`
+ command]), creating changes for updates through the REST API (via the
+ link:rest-api-projects.html#create-access-change[Create Access Rights Change
+ for review] REST endpoint) and link:#update-by-git-push[pushing changes for
+ review] require the user to be able to see the `refs/meta/config` branch
+ (have the link:access-control.html#category_read[Read] access right assigned
+ on `refs/meta/config`) and be allowed to create changes for it (have the
+ link:access-control.html#category_push[Push] access right assigned on
+ `refs/for/refs/meta/config` or be a project owner or be an administrator).
+
+[[comments]]
+=== Comments in project configuration files
+
+In principle it's possible to have comments in the project configuration files
+(lines starting with '#'), however if any Gerrit API is used that lead to
+modifications in the configuration files the comments may be dropped. This is
+because when Gerrit parses the configuration files and writes them back with
+updates, comments are not preserved.
+
+[TIP]
+When updating the project configuration use the commit message to record the
+reason for the settings so that this information is preserved in the git
+history. For example, this allows using `git blame` to inspect why
+configuration values have been set.
+
+[[inheritance]]
+== Inheritance
+
+Projects in Gerrit are organized hierarchically in a tree with the
+`All-Projects` project as the root project. The parent project is defined in
+the link:#access.inheritFrom[access section] of the `project.config` file.
+
+Projects inherit access rights and options from their parent project, but not
+all options are inheritable. See the description of the options in the
+link:#file-project_config[project.config] file to learn whether they are
+inherited or not.
+
+Options with boolean values support a special `INHERIT` value to make them
+inherit the value that is set in the parent project.
+
+Some settings can be enforced for child projects (or if set on the
+`All-Projects` root project for all projects), e.g. access right restrictions
+via link:access-control#block[BLOCK rules] or
+link:config-submit-requirements.html#submit_requirement_can_override_in_child_projects[
+non-overridable] submit requirements.
+
+[NOTE]
+The parent project for an existing project can be changed via the
+link:update-through-access-screen[Access] screen (by default this is only
+allowed for administrators).
+
+[NOTE]
+Project owners can be allowed to change the parent of projects that they own
+(see link:config-gerrit.html#receive.allowProjectOwnersToChangeParent[
+receive.allowProjectOwnersToChangeParent] setting which is `false` by default).
+In this case project owners may escape the settings that are enforced by their
+parent project by choosing a different parent project.
[[file-project_config]]
-== The file +project.config+
+== The file `project.config`
-The +project.config+ file contains the link between groups and their
-permitted actions on reference patterns in this project and any projects
-that inherit its permissions.
+The `project.config` file contains the link to the parent project, the project
+description, access rights, link:config-labels.html[label definitions],
+link:config-submit-requirements.html[submit requirements] and options to
+control project-specific behavior.
-The format in this file corresponds to the Git config file format, so
-if you want to automate your permissions it is a good idea to use the
-+git config+ command when writing to the file. This way you know you
-don't accidentally break the format of the file.
+The format in this file corresponds to the Git config file format.
-Here follows a +git config+ command example:
+[TIP]
+--
+Since the format of the `project.config` file adheres to the Git config file
+format, utilizing the `git config` command when modifying the file is
+recommended. This way you can avoid breaking the format of the file
+accidentally.
+
+Here is an example of a `git config` command that updates the project
+description:
----
-$ git config -f project.config project.description "Rights inherited by all other projects"
+$ git config -f project.config project.description "Foo Bar"
----
+--
-Below you will find an example of the +project.config+ file format:
+This is an example of a `project.config` file:
----
[project]
- description = Rights inherited by all other projects
+ description = Collection of scripts for setting up foo bar.
[access "refs/*"]
- read = group Administrators
+ read = group Registered Users
[access "refs/heads/*"]
- label-Your-Label-Here = -1..+1 group Administrators
-[capability]
- administrateServer = group Administrators
+ label-Code-Review = -2..+2 group Maintainers
+ label-Code-Review = -1..+1 group Registered Users
+ label-Your-Label = -1..+1 group Maintainers
[receive]
- requireContributorAgreement = false
-[label "Your-Label-Here"]
- function = MaxWithBlock
- value = -1 Your -1 Description
- value = 0 Your No score Description
- value = +1 Your +1 Description
+ requireChangeId = true
+[label "Your-Label"]
+ function = NoOp
+ value = -1 Your -1 Description
+ value = 0 Your No score Description
+ value = +1 Your +1 Description
+[submit-requirement "Your-Label"]
+ description = At least one maximum vote for label 'Your-Label' is required
+ applicableIf = -branch:refs/meta/config
+ submittableIf = label:Your-Label=MAX AND -label:Your-Label=MIN
----
-As you can see, there are several sections.
+The sections that can appear in the `project.config` file are explained below.
-The link:#project-section[+project+ section] appears once per project.
+[[access-section]]
+=== Access section
-The link:#access-subsection[+access+ section] appears once per reference pattern,
-such as `+refs/*+` or `+refs/heads/*+`. Only one access section per pattern is
-allowed.
+[[access.inheritFrom]]access.inheritFrom::
++
+Name of the parent project from which access rights are inherited.
++
+If not set, access rights are inherited from the `All-Projects` root project.
+
+[[access-subsection]]
+==== Access subsection
-The link:#receive-section[+receive+ section] appears once per project.
+`access` subsections define access rules for a ref or a ref namespace. The ref
+or ref namespace is specified as the subsection name and can be a concrete ref
+(e.g. `refs/heads/master`), a ref pattern (last path segment is '\*', e.g.
+`refs/heads/*`) or a regular expression (must start with '^', e.g.
+`^refs/heads/rel-.*`).
+
+[NOTE]
+For ref patterns '\*' can only appear as the last path segment. If a '*' is
+required in any other place the ref namespace must be specified as a regular
+expression (must start with '^', '\*' must follow what's being matched, e.g.
+".*" to match any string).
+
+[NOTE]
+Only one access subsection per ref and per ref namespace is allowed.
+
+The `access` subsections contain access rules that apply to the ref or ref
+namespace of the `access` subsections. The format of the access rules is: +
+`<accessCategoryId> = (block|deny)? <range>? group <group-name>`
+
+* `<accessCategoryId>`: ID of the link:access-control.html#access_categories[
+ access category] for which the access rule should be defined. The ID of the
+ access category is the name of the access category in lowerCamelCase (e.g.
+ `createTag`), except for label permissions where it is `label-<label-Name>`
+ (e.g. `label-Code-Review`).
+* `(block|deny)?`: `block` defines a link:access-control.html#block-rule[BLOCK]
+ rule, `deny` defines a link:access-control.html#deny-rule[DENY] rule, if
+ neither `block` or `deny` is specified an link:access-control.html#allow-rule[
+ ALLOW] rule is defined.
+* `<range>?`: Only set for label permission. The voting range in the format
+ `<min-vote>..<max-vote>` (e.g. `-1..+1`).
+* `group <group-name>`: The (local) name of the group to which the access rule
+ should apply (e.g. `group Foo Bar`). The (local) group name must exist in the
+ link:#file-groups[groups] file, so that Gerrit can resolve it to the group
+ UUID.
+
+To make access rules link:access-control.html#exclusive[exclusive] they need to
+be included into the value of the `exclusiveGroupPermissions` key: +
+`exclusiveGroupPermissions = <space-separated-list-of-access-category-ids>`
+
+.Example access subsections
+----
+ [access "refs/heads/*"]
+ create = group Administrators
+ delete = group Administrators
+ deleteChanges = group Administrators
+ label-Code-Review = -2..+2 group Maintainers
+ label-Code-Review = -1..+1 group Registered Users
+ label-Verified = -1..+1 group Registered Users
+ push = block Registered Users
+ submit = group Maintainers
+ exclusiveGroupPermissions = deleteChanges submit
+ [access "^refs/tags/rel-.*"]
+ createTag = group Maintainers
+ createSignedTag = group Maintainers
+----
-The link:#submit-section[+submit+ section] appears once per project.
+[[branchOrder-section]]
+=== branchOrder section
-The link:#capability-section[+capability+] section only appears once, and only
-in the +All-Projects+ repository. It controls core features that are configured
-on a global level.
+Defines a branch ordering which is used for backporting of changes.
+Backporting will be offered for a change (in the Gerrit UI) for all
+more stable branches where the change can merge cleanly.
-The link:#label-section[+label+] section can appear multiple times. You can
-also redefine the text and behavior of the built in label types `Code-Review`
-and `Verified`.
+[[branchOrder.branch]]branchOrder.branch::
-Optionally a +commentlink+ section can be added to define project-specific
-comment links. The +commentlink+ section has the same format as the
-link:config-gerrit.html#commentlink[+commentlink+ section in gerrit.config]
+A branch name, typically multiple values will be defined. The order of branch
+names in this section defines the branch order. The topmost is considered to be
+the least stable branch (typically the master branch) and the last one the
+most stable (typically the last maintained release branch).
+
+.Example:
+----
+[branchOrder]
+ branch = master
+ branch = stable-2.9
+ branch = stable-2.8
+ branch = stable-2.7
+----
+
+The `branchOrder` section is inheritable. This is useful when multiple or all
+projects follow the same branch rules. A `branchOrder` section in a child
+project completely overrides any `branchOrder` section from a parent i.e. there
+is no merging of `branchOrder` sections. A present but empty `branchOrder`
+section removes all inherited branch order.
+
+Branches not listed in this section will not be included in the mergeability
+check. If the `branchOrder` section is not defined then the mergeability of a
+change into other branches will not be computed.
+
+[[capability-section]]
+=== Capability section
+
+The `capability` section only appears once, and only in the `project.config`
+file of the `All-Projects` root project. It controls Gerrit administration
+capabilities that are configured on a global level.
+
+.Example:
+----
+[capability]
+ administrateServer = group Administrators
+----
+
+[NOTE]
+The (local) group names for which capabilities are assigned must exist in the
+link:#file-groups[groups] file, so that Gerrit can resolve them to their group
+UUID.
+
+Please refer to the
+link:access-control.html#global_capabilities[Global Capabilities]
+documentation for a full list of available capabilities.
+
+[[change-section]]
+=== Change section
+
+The change section includes configuration for project-specific change settings:
+
+[[change.privateByDefault]]change.privateByDefault::
++
+Controls whether all new changes in the project are set as private by default.
++
+Note that a new change will be public if the `is_private` field in
+link:rest-api-changes.html#change-input[ChangeInput] is set to `false` explicitly
+when calling the link:rest-api-changes.html#create-change[CreateChange] REST API
+or the `remove-private` link:user-upload.html#private[PushOption] is used during
+the Git push.
++
+Default is `INHERIT`, which means that this property is inherited from
+the parent project.
+
+[[change.workInProgressByDefault]]change.workInProgressByDefault::
++
+Controls whether all new changes in the project are set as WIP by default.
++
+Note that a new change will be ready if the `workInProgress` field in
+link:rest-api-changes.html#change-input[ChangeInput] is set to `false` explicitly
+when calling the link:rest-api-changes.html#create-change[CreateChange] REST API
+or the `ready` link:user-upload.html#wip[PushOption] is used during
+the Git push.
++
+Default is `INHERIT`, which means that this property is inherited from
+the parent project.
+
+[[commentlink-section]]
+=== Commentlink section
+
+Optionally a `commentlink` section can be added to define project-specific
+comment links. The `commentlink` section has the same format as the
+link:config-gerrit.html#commentlink[commentlink] section in `gerrit.config`
which is used to define global comment links.
+[[label-section]]
+=== Label section
+
+Please refer to link:config-labels.html#label_custom[Custom Labels] documentation.
+
+[[mimetype-section]]
+=== MIME Types section
+
+The +mimetype+ section may be configured to force the web code
+reviewer to return certain MIME types by file path. MIME types
+may be used to activate syntax highlighting.
+
+----
+[mimetype "text/x-c"]
+ path = *.pkt
+[mimetype "text/x-java"]
+ path = api/current.txt
+----
+
[[project-section]]
=== Project section
@@ -281,36 +682,31 @@ branch first and for review to another branch for example. Or in cases
where a commit is already merged into a branch and you want to create
a new open change for that commit on another branch.
-[[change-section]]
-=== Change section
+[[reviewer-section]]
+=== reviewer section
-The change section includes configuration for project-specific change settings:
+Defines config options to adjust a project's reviewer workflow such as enabling
+reviewers and CCs by email.
-[[change.privateByDefault]]change.privateByDefault::
+[[reviewer.enableByEmail]]reviewer.enableByEmail::
+
-Controls whether all new changes in the project are set as private by default.
+A boolean indicating if reviewers and CCs that do not currently have a Gerrit
+account can be added to a change by providing their email address.
+
-Note that a new change will be public if the `is_private` field in
-link:rest-api-changes.html#change-input[ChangeInput] is set to `false` explicitly
-when calling the link:rest-api-changes.html#create-change[CreateChange] REST API
-or the `remove-private` link:user-upload.html#private[PushOption] is used during
-the Git push.
+This setting only takes effect for changes that are readable by anonymous users.
+
Default is `INHERIT`, which means that this property is inherited from
-the parent project.
+the parent project. If the property is not set in any parent project, the
+default value is `FALSE`.
-[[change.workInProgressByDefault]]change.workInProgressByDefault::
-+
-Controls whether all new changes in the project are set as WIP by default.
+[[reviewer.skipAddingAuthorAndCommitterAsReviewers]]reviewer.skipAddingAuthorAndCommitterAsReviewers::
+
-Note that a new change will be ready if the `workInProgress` field in
-link:rest-api-changes.html#change-input[ChangeInput] is set to `false` explicitly
-when calling the link:rest-api-changes.html#create-change[CreateChange] REST API
-or the `ready` link:user-upload.html#wip[PushOption] is used during
-the Git push.
+Whether to skip adding the Git commit author and committer as reviewers for
+a new change.
+
Default is `INHERIT`, which means that this property is inherited from
-the parent project.
+the parent project. If the property is not set in any parent project, the
+default value is `FALSE`.
[[submit-section]]
=== Submit section
@@ -457,9 +853,9 @@ Using this submit action results in a linear history of the target branch,
unless merge commits are being submitted. For some people this is an advantage
since they find the linear history easier to read.
+
-NOTE: Rebasing merge commits is not supported. If a change with a merge commit
-is submitted the link:#merge_if_necessary[merge if necessary] submit action is
-applied.
+NOTE: Rebasing merge commits is done by rebasing their first parent commit,
+i.e. the first parent is updated to the new base while the second parent stays
+intact.
[[rebase_always]]
* 'rebase always':
@@ -476,9 +872,9 @@ Using this submit action results in a linear history of the target branch,
unless merge commits are being submitted. For some people this is an advantage
since they find the linear history easier to read.
+
-NOTE: Rebasing merge commits is not supported. If a change with a merge commit
-is submitted the link:#merge_if_necessary[merge if necessary] submit action is
-applied.
+NOTE: Rebasing merge commits is done by rebasing their first parent commit,
+i.e. the first parent is updated to the new base while the second parent stays
+intact.
+
When rebasing the patchset, Gerrit automatically appends onto the end of the
commit message a short summary of the change's approvals, and a URL link back
@@ -634,132 +1030,32 @@ become empty upon submit, since the rebase|cherry-pick can lead to an empty comm
is set to 'true' the merge would fail in such a case. An empty commit is still allowed as the
initial commit on a branch.
-
-[[access-section]]
-=== Access section
-
-[[access.inheritFrom]]access.inheritFrom::
-+
-Name of the parent project from which access rights are inherited.
-+
-If not set, access rights are inherited from the `All-Projects` root project.
-
-[[access-subsection]]
-==== Access subsection
-
-+access+ subsections for references connect access rights to groups. Each group
-listed must exist in the link:#file-groups[+groups+ file].
-
-Please refer to the
-link:access-control.html#access_categories[Access Categories]
-documentation for a full list of available access rights.
-
-
-[[mimetype-section]]
-=== MIME Types section
-
-The +mimetype+ section may be configured to force the web code
-reviewer to return certain MIME types by file path. MIME types
-may be used to activate syntax highlighting.
-
-----
-[mimetype "text/x-c"]
- path = *.pkt
-[mimetype "text/x-java"]
- path = api/current.txt
-----
-
-
-[[capability-section]]
-=== Capability section
-
-The +capability+ section only appears once, and only in the +All-Projects+
-repository. It controls Gerrit administration capabilities that are configured
-on a global level.
-
-Please refer to the
-link:access-control.html#global_capabilities[Global Capabilities]
-documentation for a full list of available capabilities.
-
-[[label-section]]
-=== Label section
-
-Please refer to link:config-labels.html#label_custom[Custom Labels] documentation.
-
[[submit-requirement-section]]
=== Submit Requirement section
Please refer to link:config-submit-requirements.html[Configuring Submit
Requirements] documentation.
-[[branchOrder-section]]
-=== branchOrder section
-
-Defines a branch ordering which is used for backporting of changes.
-Backporting will be offered for a change (in the Gerrit UI) for all
-more stable branches where the change can merge cleanly.
-
-[[branchOrder.branch]]branchOrder.branch::
-+
-A branch name, typically multiple values will be defined. The order of branch
-names in this section defines the branch order. The topmost is considered to be
-the least stable branch (typically the master branch) and the last one the
-most stable (typically the last maintained release branch).
-+
-Example:
-+
-----
-[branchOrder]
- branch = master
- branch = stable-2.9
- branch = stable-2.8
- branch = stable-2.7
-----
-+
-The `branchOrder` section is inheritable. This is useful when multiple or all
-projects follow the same branch rules. A `branchOrder` section in a child
-project completely overrides any `branchOrder` section from a parent i.e. there
-is no merging of `branchOrder` sections. A present but empty `branchOrder`
-section removes all inherited branch order.
-+
-Branches not listed in this section will not be included in the mergeability
-check. If the `branchOrder` section is not defined then the mergeability of a
-change into other branches will not be done.
-
-[[reviewer-section]]
-=== reviewer section
-
-Defines config options to adjust a project's reviewer workflow such as enabling
-reviewers and CCs by email.
+[[file-groups]]
+== The file `groups`
-[[reviewer.enableByEmail]]reviewer.enableByEmail::
-+
-A boolean indicating if reviewers and CCs that do not currently have a Gerrit
-account can be added to a change by providing their email address.
-+
-This setting only takes effect for changes that are readable by anonymous users.
-+
-Default is `INHERIT`, which means that this property is inherited from
-the parent project. If the property is not set in any parent project, the
-default value is `FALSE`.
+The `groups` file resolves group names that are mentioned in
+link:#access-subsection[access subsections] of the link:#file-project_config[
+project.config] file to group UUIDs.
-[[reviewer.skipAddingAuthorAndCommitterAsReviewers]]reviewer.skipAddingAuthorAndCommitterAsReviewers::
-+
-Whether to skip adding the Git commit author and committer as reviewers for
-a new change.
-+
-Default is `INHERIT`, which means that this property is inherited from
-the parent project. If the property is not set in any parent project, the
-default value is `FALSE`.
+In the access subsections access rights are assigned to group names. These
+group names are local to the project configuration and do not need to match
+with the actual group names. To enable Gerrit to resolve the local group names
+they must be mapped to group UUIDs in the `groups` file.
-[[file-groups]]
-== The file +groups+
+The access sections use local group names, rather than requiring the actual
+group names, to allow renaming groups in Gerrit without having to rewrite every
+`project.config` file using the group.
-Each group in this list is linked with its UUID so that renaming of
-groups is possible without having to rewrite every +groups+ file
-in every repository where it's used.
+The content of the `groups` file is a simple table of group UUID to group name,
+separated by a tab.
-This is what the default groups file for +All-Projects.git+ looks like:
+This is how the default `groups` file for `All-Projects` project looks like:
----
# UUID Group Name
@@ -771,29 +1067,37 @@ global:Project-Owners Project Owners
global:Registered-Users Registered Users
----
-This file can't be written to by the +git config+ command.
+Since the `groups` file has a custom format it can't be edited using the
+`git config` command.
-In order to reference a group in +project.config+, it must be listed in
-the +groups+ file. When editing permissions through the web UI this
-file is maintained automatically, but when pushing updates to
-+refs/meta/config+ this must be dealt with by hand. Gerrit will refuse
-+project.config+ files that refer to groups not listed in +groups+.
+Whenever access rights in the `project.config` file are assigned to new groups
+mapping entries for the new groups must be added to the `groups` file. The
+modifications to the `groups` file must be included in the same commit that
+updates the `project.config` file. Pushing updates to `project.config` files
+that refer to groups not listed in the `groups` file are rejected by Gerrit.
-The UUID of a group can be found on the General tab of the group's page
-in the web UI or via the +-v+ option to
-link:cmd-ls-groups.html[the +ls-groups+ SSH command].
+When link:#update-through-access-screen[editing access rights through the web
+UI] the `groups` file is automatically updated by Gerrit.
+The UUID of a group can be found on the group screen (`BROWSE` > `Groups` >
+`<group-name>` ). Alternatively the group can be looked up via the
+link:rest-api-groups.html#get-group[Get Group] REST endpoint (note that the
+`group-id` in the URL can be the group name). The group UUID is contained as
+`id` field in the return link:rest-api-groups.html#group-info[GroupInfo] JSON.
[[file-rules_pl]]
-== The file +rules.pl+
+== The file `rules.pl`
+
+The `rules.pl` file allows to replace or amend the default Prolog rules that
+control what conditions need to be fulfilled for a change to be submittable.
+This file should be interpretable by the 'Prolog Cafe' interpreter.
-The +rules.pl+ files allows you to replace or amend the default Prolog
-rules that control e.g. what conditions need to be fulfilled for a
-change to be submittable. This file content should be
-interpretable by the 'Prolog Cafe' interpreter.
+You can read more about prolog rules on the link:prolog-cookbook.html[Prolog
+cookbook] page.
-You can read more about the +rules.pl+ file and the prolog rules on
-link:prolog-cookbook.html[the Prolog cookbook page].
+[NOTE]
+Prolog rules are deprecated and have been replaced by
+link:config-submit-requirements.html[submit requirements].
GERRIT
------
diff --git a/Documentation/config-submit-requirements.txt b/Documentation/config-submit-requirements.txt
index 1bcda63285..5ab1addeb6 100644
--- a/Documentation/config-submit-requirements.txt
+++ b/Documentation/config-submit-requirements.txt
@@ -1,106 +1,289 @@
= Gerrit Code Review - Submit Requirements
-As part of the code review process, project owners need to configure rules that
-govern when changes become submittable. For example, an admin might want to
-prevent changes from being submittable until at least a “+2†vote on the
-“Code-Review†label is granted on a change. Admins can define submit
-requirements to enforce submittability rules for changes.
+Submit requirements are rules that define when a change can be submitted. This
+page describes how to configure them.
[[configuring_submit_requirements]]
== Configuring Submit Requirements
-Site administrators and project owners can define submit requirements in the
-link:config-project-config.html[project config]. A submit requirement has the
-following fields:
-
-
-[[submit_requirement_name]]
-=== submit-requirement.Name
-
-A name that uniquely identifies the submit requirement. Submit requirements
-can be overridden in child projects if they are defined with the same name in
-the child project. See the link:#inheritance[inheritance] section for more
-details.
-
-[[submit_requirement_description]]
-=== submit-requirement.Name.description
+Submit requirements are defined as link:#submit-requirement-subsection[
+submit-requirement] subsections in the
+link:config-project-config.html#file-project_config[project.config] file. The
+subsection name defines the name of the submit requirement.
+
+[NOTE]
+There are multiple options how to update `project.config` files, please refer
+to the link:config-project-config.html#update[project config documentation].
+
+[TIP]
+When modifying submit requirements it's recommended to
+link:#test-submit-requirements[test] them before updating them in the project
+configuration.
+
+[WARNING]
+--
+When adding submit requirements think about whether they should apply to the
+link:config-project-config.html#refs-meta-config[refs/meta/config] branch
+(see the link:#submit_requirement_applicable_if[applicableIf] description on
+how to exempt the `refs/meta/config` branch from a submit requirement). Since
+submit requirements are stored as part of the project configuration in the
+`refs/meta/config` branch, changing them through code review requires to pass
+the submit requirements that apply to the `refs/meta/config` branch. Hence by
+misconfiguring submit requirements for the `refs/meta/config` branch you can
+make further updates to submit requirements through code review impossible.
+If this happens the submit requirements can be restored by a direct push to the
+`refs/meta/config` branch.
+
+[[restore-submit-requirements]]
+If direct pushes are disabled or not allowed project owners can directly update
+the submit requirements via the
+link:rest-api-projects.html#update-submit-requirement[Update Submit Requirement]
+REST endpoint.
+
+.Example:
+----
+ curl -X PUT --header "Content-Type: application/json" -d '{"name": "Foo-Review", "description": "At least one maximum vote for the Foo-Review label is required", "submittability_expression": "label:Foo-Review=MAX AND -label:Foo-Review=MIN", "applicability_expression": "-branch:refs/meta/config", "canOverrideInChildProjects": true}' "https://<HOST>/a/projects/My%2FProject/submit_requirements/Foo-Review"
+----
-A detailed description of what the submit requirement is supposed to do. This
-field is optional. The description is visible to the users in the change page
-upon hovering on the submit requirement to help them understand what the
-requirement is about and how it can be fulfilled.
+Tip: Googlers should use `gob-curl` instead of `curl` so that authentication is
+handled automatically.
+--
-[[submit_requirement_applicable_if]]
-=== submit-requirement.Name.applicableIf
+[[test-submit-requirements]]
+=== Testing Submit Requirements
-A link:#query_expression_syntax[query expression] that determines if the submit
-requirement is applicable for a change. For example, administrators can exclude
-submit requirements for certain branch patterns. See the
-link:#exempt-branch-example[exempt branch] example.
+When modifying submit requirements it's recommended to test them before
+updating them in the project configuration.
-Often submit requirements should only apply to branches that contain source
-code. In this case this parameter can be used to exclude the
-link:config-project-config.html#refs-meta-config[refs/meta/config] branch from
-a submit requirement:
+To test a submit requirement on a selected change
+link:rest-api-changes.html#change-id[project\~branch~changeId] use the
+link:rest-api-changes.html#check-submit-requirement[Check Submit Requirement]
+REST endpoint.
+.Request
----
- applicableIf = -branch:refs/meta/config
+ POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/check.submit_requirement HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "name": "Code-Review",
+ "submittability_expression": "label:Code-Review=+2"
+ }
----
-This field is optional, and if not specified, the submit requirement is
-considered applicable for all changes in the project.
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
-[[submit_requirement_submittable_if]]
-=== submit-requirement.Name.submittableIf
+ )]}'
+ {
+ "name": "Code-Review",
+ "status": "SATISFIED",
+ "submittability_expression_result": {
+ "expression": "label:Code-Review=+2",
+ "fulfilled": true,
+ "passingAtoms": [
+ "label:Code-Review=+2"
+ ]
+ },
+ "is_legacy": false
+ }
+----
-A link:#query_expression_syntax[query expression] that determines when the
-change becomes submittable. This field is mandatory.
+Alternatively you can make a change that updates a submit requirement in the
+`project.config` file, upload it for review to the `refs/meta/config` branch
+and then load it from that change which is in review to test it against a
+change.
+Request
+----
+ POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/check.submit_requirement?sr-name=Code-Review&refs-config-change-id=myProject~refs/meta/config~Ibc1409aef8bf0a16a76f9fa9a928bd505228fa1d HTTP/1.0
+----
-[[submit_requirement_override_if]]
-=== submit-requirement.Name.overrideIf
+In the above example `myProject\~master~I8473b95934b5732ac55d26311a706c9c2bde9940`
+is the change against which the submit requirement is tested, `Code Review` is
+the name of the submit requirement that is tested and
+`myProject\~refs/meta/config~Ibc1409aef8bf0a16a76f9fa9a928bd505228fa1d` is the
+change from which the `Code Review` submit requirement is loaded. Change
+`myProject~refs/meta/config~Ibc1409aef8bf0a16a76f9fa9a928bd505228fa1d` must be
+a change that touches the `project.config` file in the `refs/meta/config`
+branch and the `project.config` file must contain a submit requirement with the
+name `Code-Review`.
+
+[[dashboard]]
+=== Show Submit Requirements on Dashboards
+
+Gerrit offers dashboards that provide an overview over a set of changes (e.g.
+user dashboards shows changes that are relevant to the user, change list
+dashboards show changes that match a change query). To understand the state of
+the changes knowing the status of their submit requirements is important, but
+submit requirements are manifold and dashboards have only limited screen space
+available, so showing all submit requirements in dashboards is hardly possible.
+This is why administrators must decide which are the most important submit
+requirements that should be shown on dashboards. They can configure these
+submit requirements in `gerrit.config` by setting the
+link:config-gerrit.html#dashboard[dashboard.submitRequirementColumns] option.
+
+[NOTE]
+In order to save screen space submit requirement names on dashboards are
+abbreviated, e.g. a submit requirement called `Foo-Bar` is shown as `FB`.
-A link:#query_expression_syntax[query expression] that controls when the
-submit requirement is overridden. When this expression is evaluated to true,
-the submit requirement state becomes `OVERRIDDEN` and the submit requirement
-is no longer blocking the change submission.
-This expression can be used to enable bypassing the requirement in some
-circumstances, for example if the change owner is a power user or to allow
-change submission in case of emergencies. +
+[[inheritance]]
+== Inheritance
-This field is optional.
+Submit requirements are inherited from parent projects. Child projects may
+override an inherited submit requirement by defining a submit requirement with
+the same name, but only if overriding the submit requirement is allowed (see
+link:#submit_requirement_can_override_in_child_projects[
+canOverrideInChildProjects] field). Overriding an inherited submit requirement
+always overrides the complete submit requirement definition, overriding single
+fields only is not possible.
+
+[NOTE]
+To remove an inherited submit requirement in a child project, set both the
+link:#submit_requirement_applicable_if[applicableIf] expression and the
+link:#submit_requirement_submittable_if[submittableIf] expression to
+`is:false`.
+
+[NOTE]
+If overriding a submit requirement is disallowed in a parent project, submit
+requirements with the same name in child projects, that would otherwise
+override the inherited submit requirement, are ignored.
+
+[[labels]]
+== Labels and Submit Requirements
+
+link:config-labels.html[Labels] define voting categories for reviewers to score
+changes. Often a label is accompanied by a submit requirement to check the votes
+on the label, e.g. with a link:#submit_requirement_submittable_if[submittableIf]
+expression that checks that:
+
+* the label was approved: `label:My-Label=MAX`
+* the label has no veto: `-label:My-Label=MIN`
+* the label was not self-approved: `label:My-Label=MAX,user=non_uploader`
+* the label was approved by multiple users: `label:My-Label,count>1`
+
+Submit requirements that check votes for a single label often have the same
+name as the label, e.g.:
-[[submit_requirement_can_override_in_child_projects]]
-=== submit-requirement.Name.canOverrideInChildProjects
+----
+[label "Code-Review"]
+ function = NoBlock
+ value = -2 This shall not be submitted
+ value = -1 I would prefer this is not merged as is
+ value = 0 No score
+ value = +1 Looks good to me, but someone else must approve
+ value = +2 Looks good to me, approved
+ defaultValue = 0
+[submit-requirement "Code-Review"]
+ description = At least one maximum vote for label 'Code-Review' is required
+ submittableIf = label:Code-Review=MAX,user=non_uploader AND -label:Code-Review=MIN
+ canOverrideInChildProjects = true
+----
-A boolean (true, false) that determines if child projects can override the
-submit requirement. +
+[[trigger-votes]]
+=== Trigger Votes
-The default value is `false`.
+Trigger votes are votes on labels that are not associated with any submit
+requirement expressions, i.e. the submittability of changes doesn't depend on
+these votes.
-[[evaluation_results]]
-== Evaluation Results
+Voting on labels that have no impact on the submittability of changes usually
+serves the purpose to trigger processes, e.g. a vote on a `Presubmit-Ready`
+label can be a signal to run presubmit integration tests. Hence these votes are
+called `trigger votes`.
-When submit requirements are configured, their results are returned for all
-changes requested by the REST API with the
-link:rest-api-changes.html#submit-requirement-result-info[SubmitRequirementResultInfo]
-entity. +
+Trigger votes are displayed in a separate section in the change page.
-Submit requirement results are produced from the evaluation of the submit
-requirements in the project config (
-See link:#configuring_submit_requirements[Configuring Submit Requirements])
-as well as the conversion of the results of the legacy submit rules to submit
-requirement results. Legacy submit rules are label functions
-(see link:config-labels.html[config labels]), custom and
-link:prolog-cookbook.html[prolog] submit rules.
+[[deprecated]]
+== Deprecated ways to control when changes can be submitted
-The `status` field can be one of:
+Using submit requirements is the recommended way to control when changes can be
+submitted. However, historically there are other ways for this, which are still
+working, although they are deprecated:
+[[label-functions]]
+* Label functions:
++
+link:config-labels.html#label_custom[Label definitions] can contain a
+link:config-labels.html#label_function[function] that impacts the
+submittability of changes (link:config-labels.html#MaxWithBlock[MaxWithBlock],
+link:config-labels.html#AnyWithBlock[AnyWithBlock],
+link:config-labels.html#MaxNoBlock[MaxNoBlock]). These functions are deprecated
+and setting them is no longer allowed, however if they are (already) set for
+existing label definitions they are still respected. For new labels the
+function should be set to link:config-labels.html#NoBlock[NoBlock] and then
+submit requirements should be used to control when changes can be submitted
+(using `submittableIf = label:My-Label=MAX AND -label:My-Label=MIN` is
+equivalent to `MaxWithBlock`, using `submittableIf = -label:My-Label=MIN` is
+equivalent to `AnyWithBlock`, using `submittableIf = label:My-Label=MAX` is
+equivalent to using `MaxNoBlock`).
+
+[[ignoreSelfApproval]]
+* `ignoreSelfApproval` flag on labels:
++
+Labels can be configured to link:config-labels.html#label_ignoreSelfApproval[
+ignore self approvals]. This flag only works in combination with the deprecated
+label functions (see link:#label-functions[above]) and hence it is deprecated
+as well. Instead use a `submittableIf` expression with the
+link:#operator_label[label] operator and the `user=non_uploader` argument. See
+the link:#code-review-example[Code Review] submit requirement example.
+
+[[prolog-rules]]
+* Prolog rules:
++
+Projects can define link:prolog-cookbook.html[prolog submit rules] that control
+when changes can be submitted. It's still possible to have Prolog submit rules,
+but they are deprecated and support for them will be dropped in future Gerrit
+releases. Hence it's recommended to use submit requirements instead.
+
+When checking whether changes can be submitted Gerrit takes results of label
+functions and Prolog submit rules into account, in addition to the submit
+requirements.
+
+[[plugin-submit-rules]]
+== Plugin provided submit rules
+
+Plugins can contribute submit rules by implementing the `SubmitRule` extension
+point (see link:dev-plugins.html#pre-submit-evaluator[Pre-submit Validation
+Plugins]).
+
+When checking whether changes can be submitted Gerrit takes results of
+plugin-provided submit rules into account, in addition to the submit
+requirements.
+
+[[evaluation]]
+== Submit Requirement Evaluation
+
+Submit requirements are evaluated whenever a change is updated. To decide
+whether changes can be submitted, the results of link:#label-functions[label
+functions], link:#prolog-rules[Prolog submit rules] and
+link:#plugin-submit-rules[plugin-provided submit rules] are taken into account,
+in addition to the submit requirements. For this the results of label
+functions, Prolog submit rules and plugin-provided submit rules are converted
+to submit requirement results.
+
+Submit requirement results are returned in the REST API when retrieving changes
+with the link:rest-api-changes.html#submit-requirements[SUBMIT_REQUIREMENTS]
+option (e.g. via the link:rest-api-changes.html#get-change-detail[Get Change
+Detail] REST endpoint or the link:rest-api-changes.html#list-changes[Query
+Changes] REST endpoint). If requested, submit requirements are included as
+link:rest-api-changes.html#submit-requirement-result-info[
+SubmitRequirementResultInfo] entities into
+link:rest-api-changes.html#change-info[ChangeInfo] (field
+`submit_requirements`).
+
+The `status` field of submit requirement results can be one of:
+
+[[status-not-applicable]]
* `NOT_APPLICABLE`
+
The link:#submit_requirement_applicable_if[applicableIf] expression evaluates
to false for the change.
+[[status-unsatisfied]]
* `UNSATISFIED`
+
The submit requirement is applicable
@@ -109,6 +292,7 @@ the evaluation of the link:#submit_requirement_submittable_if[submittableIf] and
link:#submit_requirement_override_if[overrideIf] expressions return false for
the change.
+[[status-satisfied]]
* `SATISFIED`
+
The submit requirement is applicable
@@ -117,29 +301,129 @@ link:#submit_requirement_submittable_if[submittableIf] expression evaluates to
true, and the link:#submit_requirement_override_if[overrideIf] evaluates to
false for the change.
+[[status-overridden]]
* `OVERRIDDEN`
+
The submit requirement is applicable
(link:#submit_requirement_applicable_if[applicableIf] evaluates to true) and the
link:#submit_requirement_override_if[overrideIf] expression evaluates to true.
+
-Note that in this case, the change is overridden whether the
-link:#submit_requirement_submittable_if[submittableIf] expression evaluates to
-true or not.
+Note that in this case, the submit requirement is overridden regardless of
+whether the link:#submit_requirement_submittable_if[submittableIf] expression
+evaluates to true or not.
+[[status-forced]]
* `FORCED`
+
The change was merged directly bypassing code review by supplying the
link:user-upload.html#auto_merge[submit] push option while doing a git push.
+[[status-error]]
* `ERROR`
+
The evaluation of any of the
link:#submit_requirement_applicable_if[applicableIf],
link:#submit_requirement_submittable_if[submittableIf] or
link:#submit_requirement_override_if[overrideIf] expressions resulted in an
-error.
+error, i.e. because the expression is not parseable.
+[NOTE]
+Gerrit can be configured to return a `500 internal server error` response
+instead of setting the status to `ERROR` (see the
+link:config-gerrit.html#change.propagateSubmitRequirementErrors[
+change.propagateSubmitRequirementErrors] option that can be set in
+`gerrit.config`).
+
+[[submit-requirement-subsection]]
+== submit-requirement subsection
+
+Each `submit-requirement` subsection defines a submit requirement.
+
+The name of the `submit-requirement` subsection defines the name that uniquely
+identifies the submit requirement. It is shown to the user in the web UI when
+the submit requirement is applicable.
+
+[NOTE]
+By using the same name as an inherited submit requirement, the inherited submit
+requirement can be overridden, if overriding is allowed (see
+link:#submit_requirement_can_override_in_child_projects[
+canOverrideInChildProjects] field). Details about overriding submit
+requirements are explained in the link:#inheritance[inheritance] section.
+
+Submit requirements must at least define a
+link:#submit_requirement_submittable_if[submittableIf] expression that defines
+when a change can be submitted.
+
+.Example:
+----
+[submit-requirement "Verified"]
+ description = CI result status for build and tests is passing
+ applicableIf = -branch:refs/meta/config
+ submittableIf = label:Verified=MAX AND -label:Verified=MIN
+ canOverrideInChildProjects = true
+----
+
+The fields that can be set for submit requirements are explained below.
+
+[[submit_requirement_description]]
+=== submit-requirement.Name.description
+
+A detailed description of what the submit requirement is supposed to do. This
+field is optional. The description is visible to the user in the change page
+upon hovering on the submit requirement to help them understand what the
+requirement is about and how it can be fulfilled.
+
+[[submit_requirement_applicable_if]]
+=== submit-requirement.Name.applicableIf
+
+A link:#query_expression_syntax[query expression] that determines if the submit
+requirement is applicable for a change. If a submit requirement is not
+applicable it is hidden in the web UI. For example, this allows to
+link:#exempt-branch-example[exempt a branch] from the submit requirement.
+
+[TIP]
+--
+Often submit requirements should only apply to branches that contain source
+code. In this case the `applicableIf` condition can be used to exclude the
+link:config-project-config.html#refs-meta-config[refs/meta/config] branch from
+the submit requirement:
+
+----
+ applicableIf = -branch:refs/meta/config
+----
+--
+
+This field is optional, and if not specified, the submit requirement is
+considered applicable for all changes in the project.
+
+[[submit_requirement_submittable_if]]
+=== submit-requirement.Name.submittableIf
+
+A link:#query_expression_syntax[query expression] that determines when the
+change can be submitted. This field is mandatory.
+
+
+[[submit_requirement_override_if]]
+=== submit-requirement.Name.overrideIf
+
+A link:#query_expression_syntax[query expression] that controls when the
+submit requirement is overridden. When this expression is evaluated to true,
+the submit requirement state becomes `OVERRIDDEN` and the submit requirement
+is no longer blocking the change submission.
+
+This expression can be used to enable bypassing the requirement in some
+circumstances, for example if the uploader is a trusted bot user or to allow
+change submission in case of emergencies.
+
+This field is optional.
+
+[[submit_requirement_can_override_in_child_projects]]
+=== submit-requirement.Name.canOverrideInChildProjects
+
+A boolean (true, false) that determines if child projects can override the
+submit requirement.
+
+The default value is `false`.
[[query_expression_syntax]]
== Query Expression Syntax
@@ -247,13 +531,17 @@ name contains the file pattern, or the edits of the file diff contain the edit
pattern.
[[operator_label]]
-label:labelName=+1,user=non_contributor::
+label:LabelExpression::
++
+The `label` operator allows to match changes that have votes matching the given
+`LabelExpression`. The `LabelExpression` can be anything that's supported for
+the link:user-search.html#labels[label] query operator.
+
-Submit requirements support an additional `user=non_contributor` argument for
-labels that returns true if the change has a label vote matching the specified
-value and the vote is applied from a gerrit account that's not the uploader,
-author or committer of the latest patchset. See the documentation for the labels
-operator in the link:user-search.html[user search] page.
+If used in submit requirement expressions, this operator supports an additional
+`user=non_contributor` argument. This argument works similar to the
+link:user-search.html#non_uploader["user=non_uploader"] argument and returns
+true if the change has a matching label vote that is applied by a user that's
+not the uploader, author or committer of the latest patchset.
[[unsupported_operators]]
=== Unsupported Operators
@@ -265,39 +553,6 @@ is:submittable::
+
Cannot be used since it will result in recursive evaluation of expressions.
-[[inheritance]]
-== Inheritance
-
-Child projects can override a submit requirement defined in any of their parent
-projects. Overriding a submit requirement overrides all of its properties and
-values. The overriding project needs to define all mandatory fields.
-
-Submit requirements are looked up from the current project up the inheritance
-hierarchy to “All-Projectsâ€. The first project in the hierarchy chain that sets
-link:#submit_requirement_can_override_in_child_projects[canOverrideInChildProjects]
-to false prevents all descendant projects from overriding it.
-
-If a project disallows a submit requirement from being overridden in child
-projects, all definitions of this submit requirement in descendant projects are
-ignored.
-
-To remove a submit requirement in a child project, administrators can redefine
-the requirement with the same name in the child project and set the
-link:#submit_requirement_applicable_if[applicableIf] expression to `is:false`.
-Since the link:#submit_requirement_submittable_if[submittableIf] field is
-mandatory, administrators need to provide it in the child project but can set it
-to anything, for example `is:false` but it will have no effect anyway.
-
-
-[[trigger-votes]]
-== Trigger Votes
-
-Trigger votes are label votes that are not associated with any submit
-requirement expressions. Trigger votes are displayed in a separate section in
-the change page. For more about configuring labels, see the
-link:config-labels.html[config labels] documentation.
-
-
[[examples]]
== Examples
@@ -359,46 +614,6 @@ the commit message.
submittableIf = hasfooter:\"Bug\"
----
-[[test-submit-requirements]]
-== Testing Submit Requirements
-
-The link:rest-api-changes.html#check-submit-requirement[Check Submit Requirement]
-change endpoint can be used to test submit requirements on any change. Users
-are encouraged to test submit requirements before adding them to the project
-to ensure they work as intended.
-
-.Request
-----
- POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/check.submit_requirement HTTP/1.0
- Content-Type: application/json; charset=UTF-8
-
- {
- "name": "Code-Review",
- "submittability_expression": "label:Code-Review=+2"
- }
-----
-
-.Response
-----
- HTTP/1.1 200 OK
- Content-Disposition: attachment
- Content-Type: application/json; charset=UTF-8
-
- )]}'
- {
- "name": "Code-Review",
- "status": "SATISFIED",
- "submittability_expression_result": {
- "expression": "label:Code-Review=+2",
- "fulfilled": true,
- "passingAtoms": [
- "label:Code-Review=+2"
- ]
- },
- "is_legacy": false
- }
-----
-
GERRIT
------
Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index 53d9e6b698..4c224b5f24 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -393,6 +393,34 @@ execute Bazel with the appropriate target:
bazelisk test //plugins/replication/...
----
+[[known-issues]]
+=== Known Issues
+
+[[byte-buddy-not-initialized-or-unavailable]]
+==== The Byte Buddy agent is not initialized or unavailable
+
+If running tests that make use of mocks fail with the exception below, set the
+`sandbox_tmpfs_path` flag for running tests in `.bazelrc` as described in this
+link:https://github.com/mockito/mockito/issues/1879#issuecomment-922459131[
+issue], e.g. add this line: `test --sandbox_tmpfs_path=/tmp`
+
+.Exception:
+----
+...
+Caused by: org.mockito.exceptions.base.MockitoInitializationException:
+Could not initialize inline Byte Buddy mock maker.
+
+It appears as if your JDK does not supply a working agent attachment mechanism.
+...
+Caused by: java.lang.IllegalStateException: The Byte Buddy agent is not initialized or unavailable
+at net.bytebuddy.agent.ByteBuddyAgent.getInstrumentation(ByteBuddyAgent.java:230)
+at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:617)
+at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:568)
+at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:545)
+at org.mockito.internal.creation.bytebuddy.InlineDelegateByteBuddyMockMaker.<clinit>(InlineDelegateByteBuddyMockMaker.java:115)
+... 47 more
+----
+
[[debugging-tests]]
== Debugging Unit Tests
In some cases it may be necessary to debug a test while running it in bazel. For example, when we
@@ -664,26 +692,16 @@ gcloud alpha remote-build-execution worker-pools create default \
--disk-size=200
```
-Due to outdated Git version in official RBE docker images, a custom RBE docker
-image must be used. To build custom docker imager, change to the directory
-`tools/platforms` and build and publish custom RBE docker image.
-
-To build the custom RBE docker image, run:
-
-```
-docker build -t gcr.io/api-project-164060093628/rbe-ubuntu18-04 .
-```
+Note, that we are using Ubuntu2204 docker image from bazel project:
-To publish the custom RBE docker image, run:
```
-docker push gcr.io/api-project-164060093628/rbe-ubuntu18-04
-[...]
-latest: digest: sha256:de5186d4313630a6111f9a2449b72563d0bc59ec9fb60956f063b69a38a76834 size: 1584
+docker pull gcr.io/bazel-public/ubuntu2204-java17@sha256:ffe37746a34537d8e73cef5a20ccd3a4e3ec7af3e7410cba87387ba97c0e520f
```
-Re-build rbe_autoconfig project conduct a new release and switch to using it
-in `WORKSPACE` file.
+Re-build rbe_autoconfig project, conduct a new release and switch to using it
+in `WORKSPACE` file. For more details see this
+link:https://github.com/davido/rbe_autoconfig[repository,role=external,window=_blank]
Note, to authenticate to the gcr.io registry, the following command must be
used:
diff --git a/Documentation/dev-community.txt b/Documentation/dev-community.txt
index 1229fafd0b..07e3a11cb8 100644
--- a/Documentation/dev-community.txt
+++ b/Documentation/dev-community.txt
@@ -45,7 +45,7 @@ link:dev-cla.html[contributor's agreement] on file with the project.
* link:dev-readme.html[Developer Setup]
* link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui[TypeScript Frontend Developer Setup]
* link:dev-crafting-changes.html[Crafting Changes]
-* link:dev-starter-projects.html[Starter Projects]
+* link:dev-contribution-opportunities.html[Contribution Opportunities (Help Wanted)]
[[plugin-development]]
== Plugin Development
diff --git a/Documentation/dev-contribution-opportunities.txt b/Documentation/dev-contribution-opportunities.txt
new file mode 100644
index 0000000000..23f916e924
--- /dev/null
+++ b/Documentation/dev-contribution-opportunities.txt
@@ -0,0 +1,45 @@
+:linkattrs:
+= Gerrit Code Review - Contribution Opportunities
+
+If you are eager to contribute to Gerrit, but you don't know where to
+start here are some opportunities to contribute.
+
+[[help-wanted]]
+== Help Wanted
+
+The link:https://issues.gerritcodereview.com/hotlists/5395287[HelpWanted,role=external,window=_blank]
+hotlist in the issue tracker lists issues that need help from the
+community and where any contribution is very welcome.
+
+If you are interested in any of the projects and you want to try
+implementing it, just assign the corresponding issue to yourself and
+reach out on the
+link:https://groups.google.com/forum/#!forum/repo-discuss[mailing list,role=external,window=_blank]
+if you have questions or need help.
+
+[[creating-help-wanted-issue]]
+=== Creating Help Wanted issues
+
+Issues on the link:https://issues.gerritcodereview.com/hotlists/5395287[HelpWanted,role=external,window=_blank]
+hotlist should be phrased as user stories and be well-scoped so that they can
+be easily picked up by new contributors:
+
+* The issue title should name the feature and be prefixed with a t-shirt size
+ in square brackets to indicate the expected effort.
+* The issue description should use Markdown and have the following sections:
+** *User Story*: Explain the use case that is being addressed and what's the
+ value of the feature.
+** *What*: Describe what should be done.
+** *Background*: Any useful background information, including ideas how the
+ feature can be done.
+** *Pointers*: Useful links to documentation and code
+
+Note, only link:dev-roles.html#maintainer[maintainers] and
+link:dev-roles.html#contributor[trusted contributors] can add issues to the `HelpWanted` hotlist.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 42edc1fff9..bcc96b4230 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2248,6 +2248,52 @@ by implementing `com.google.gerrit.extensions.config.ExternalIncludedIn`,
e.g. a plugin can provide a list of servers on which the change was
deployed.
+Plugins can filter the branches and tags that are inlcuded by implementing
+`com.google.gerrit.server.change.FilterIncludedIn`.
+
+[source, java]
+----
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.change.FilterIncludedIn;
+import java.util.function.Predicate;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+public class MyFilter implements FilterIncludedIn {
+ @Override
+ public Predicate<String> getBranchFilter(Project.NameKey project, RevCommit commit) {
+ if (project.get() != "myproject") {
+ return branch -> true;
+ }
+ return branch -> !branch.startsWith("feature/");
+ }
+
+ @Override
+ public Predicate<String> getTagFilter(Project.NameKey project, RevCommit commit) {
+ if (project.get() != "myproject") {
+ return tag -> true;
+ }
+ return tag -> tag.startsWith("v");
+ }
+}
+----
+
+And register your class:
+
+[source, java]
+----
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.change.FilterIncludedIn;
+import com.google.inject.AbstractModule;
+
+public class MyPluginModule extends AbstractModule {
+ @Override
+ protected void configure() {
+ DynamicSet.bind(binder(), FilterIncludedIn.class).to(MyFilter.class);
+ }
+}
+----
+
[[change-report-formatting]]
== Change Report Formatting
diff --git a/Documentation/dev-processes.txt b/Documentation/dev-processes.txt
index 8b7ba4b16f..c2a1f86d5c 100644
--- a/Documentation/dev-processes.txt
+++ b/Documentation/dev-processes.txt
@@ -208,19 +208,14 @@ available.
To report a security vulnerability file a
link:https://issues.gerritcodereview.com/issues/new?component=1371046[
-security issue,role=external,window=_blank] in the Gerrit issue tracker. The visibility of issues that are
-created with the `Security Issue` template is automatically restricted to
-Gerrit maintainers and a few long-term contributors. This means as a reporter
-you may not be able to see the issue once it is created. Security issues are
-created on the `ESC` component so that they will be discussed at the next
-meeting of the link:#steering-committee[Engineering Steering Committee] which
-takes place biweekly.
+security issue,role=external,window=_blank] in the Gerrit issue tracker. Issues
+in the `Gerrit Code Review > Security` component are restricted to Gerrit
+maintainers and a few long-term contributors. The reporter becomes a
+collaborator on the issue and hence can see it as well. Security issues are
+triaged by the link:#steering-committee[Engineering Steering Committee].
-If an existing issue is found to be a security vulnerability it should be
-turned into a security issue by:
-
-. Setting the component to `ESC`
-. Adding the labels `Security` and `NonPublic`
+If an existing issue is found to be a security vulnerability it should be moved
+to `Gerrit Code Review > Security` component (component ID: 1371046).
In case of doubt, or if an issue cannot wait until the next ESC meeting,
contact the link:#steering-committee[Engineering Steering Committee] directly
@@ -370,6 +365,12 @@ Gerrit team at Google uses this
link:https://gerrit-review.googlesource.com/q/label:%2522Library-Compliance%253Dneed%2522+-ownerin:google-gerrit-team+status:open+project:gerrit+-age:4week+-is:wip+-is:private+label:Code-Review%252B2[change query]
to find changes that require a `Library-Compliance` approval.
+To get the attention of a Googler for dependency updates file separate issues
+(use type "Task") for each dependency update on the
+link:https://issues.gerritcodereview.com/issues/new?component=1371020&template=1834212["Hosting > googlesource" component].
+Then it will show up in Google's triage queue and the current person who is on duty
+should look into this.
+
Gerrit's library dependencies should only be upgraded if the new version contains
something we need in Gerrit. This includes new features, API changes as well as bug
or security fixes.
diff --git a/Documentation/dev-starter-projects.txt b/Documentation/dev-starter-projects.txt
deleted file mode 100644
index 92de84d810..0000000000
--- a/Documentation/dev-starter-projects.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-:linkattrs:
-= Gerrit Code Review - Starter Projects
-
-We have created a
-link:https://issues.gerritcodereview.com/hotlists/5052926[StarterProject,role=external,window=_blank]
-hotlist in the issue tracker and try to assign easy hack projects to it. If in
-doubt, do not hesitate to ask on the developer
-link:https://groups.google.com/forum/#!forum/repo-discuss[mailing list,role=external,window=_blank].
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
-
-SEARCHBOX
----------
diff --git a/Documentation/images/generated-suggested-edit-added.png b/Documentation/images/generated-suggested-edit-added.png
new file mode 100644
index 0000000000..37300c3559
--- /dev/null
+++ b/Documentation/images/generated-suggested-edit-added.png
Binary files differ
diff --git a/Documentation/images/generated-suggested-edit-preview.png b/Documentation/images/generated-suggested-edit-preview.png
new file mode 100644
index 0000000000..9ca82f125d
--- /dev/null
+++ b/Documentation/images/generated-suggested-edit-preview.png
Binary files differ
diff --git a/Documentation/intro-project-owner.txt b/Documentation/intro-project-owner.txt
index 97b58afc49..15f27dbdff 100644
--- a/Documentation/intro-project-owner.txt
+++ b/Documentation/intro-project-owner.txt
@@ -44,7 +44,7 @@ Access Control] chapter explains all the details.
To see the access rights of your project
- go to the Gerrit Web UI
-- click on the `Projects` > `List` menu entry
+- click on the `BROWSE` > `Repositories` menu entry
- find your project in the project list and click on it
- click on the `Access` menu entry
@@ -150,7 +150,7 @@ maintains its own groups internally but also supports different external
group backends.
The Gerrit internal groups can be seen in the Gerrit Web UI by clicking
-on the `Groups` > `List` menu entry. By clicking on a group you can
+on the `BROWSE` > `Groups` menu entry. By clicking on a group you can
edit the group members (`Members` tab) and the group options
(`General` tab).
@@ -168,8 +168,9 @@ An important setting on a group is the option
`Make group visible to all registered users.`, which defines whether
non-members can see who is member of the group.
-New internal Gerrit groups can be created under `Groups` >
-`Create New Group`. This menu is only available if you have the global
+New internal Gerrit groups can be created under `BROWSE` > `Groups`
+and then clicking on the `CREATE NEW` button in the upper right corner.
+The `CREATE NEW` button is only available if you have the global
capability link:access-control.html#capability_createGroup[Create Group]
assigned.
@@ -238,7 +239,7 @@ link:project-configuration.html#project_options[Project Options] section.
To see the options of your project:
. Go to the Gerrit Web UI.
-. Click on the `Projects` > `List` menu entry.
+. Click on the `BROWSE` > `Repositories` menu entry.
. Find your project in the project list and click it.
. Click the `General` menu entry.
@@ -431,7 +432,7 @@ messages conform to line length limits.
== Branch Administration
As project owner you can administrate the branches of your project in
-the Gerrit Web UI under `Projects` > `List` > <your project> >
+the Gerrit Web UI under `BROWSE` > `Repositories` > <your project> >
`Branches`. In the Web UI link:project-configuration.html#branch-creation[
branch creation] is allowed if you have
link:access-control.html#category_create[Create Reference] access right and
@@ -450,7 +451,7 @@ this default branch and checks it out.
With Gerrit individual users control their own email subscriptions. By
editing the link:user-notify.html#user[watched projects] in the Web UI
-under `Settings` > `Watched Projects` users can decide which events to
+under `Settings` > `Notifications` users can decide which events to
be informed about by email. The change notifications can be filtered by
link:user-search.html[change search expressions].
@@ -476,8 +477,8 @@ configure such custom dashboards on
link:user-dashboards.html#project-dashboards[project level]. This way
you can define a view of the changes that are relevant for your
project and share this dashboard with all users. The project dashboards
-can be seen in the Web UI under `Projects` > `List` > <your project> >
-`Dashboards`.
+can be seen in the Web UI under `BROWSE` > `Repositories` > <your project>
+> `Dashboards`.
[[issue-tracker-integration]]
== Issue Tracker Integration
@@ -509,7 +510,7 @@ If installed, these plugins can e.g. be used to automatically add links
to Gerrit changes to the issues in the issue tracker system or to
automatically close an issue if the corresponding change is merged.
If installed, project owners may enable/disable the issue tracker
-integration from the Gerrit Web UI under `Projects` > `Lists` >
+integration from the Gerrit Web UI under `BROWSE` > `Repositories` >
<your project> > `General`.
[[comment-links]]
@@ -554,7 +555,7 @@ Gerrit will then notify this person by email about the review request.
With the link:https://gerrit-review.googlesource.com/admin/repos/plugins/reviewers[
reviewers,role=external,window=_blank] plugin it is possible to configure default reviewers who
will be automatically added to each change. The default reviewers can
-be configured in the Gerrit Web UI under `Projects` > `List` >
+be configured in the Gerrit Web UI under `BROWSE` > `Repositories` >
<your project> > `General` in the `reviewers Plugin` section.
The link:https://gerrit-review.googlesource.com/admin/repos/plugins/reviewers-by-blame[
@@ -565,7 +566,7 @@ will add those users as reviewer that authored most of the lines
touched by the change, since these users should be familiar with the
code and can most likely review the change. How many reviewers the
plugin will add to a change at most can be configured in the Gerrit
-Web UI under `Projects` > `List` > <your project> > `General` in the
+Web UI under `BROWSE` > `Repositories` > <your project> > `General` in the
`reviewers-by-blame Plugin` section.
[[download-commands]]
@@ -650,9 +651,10 @@ Plugin Development] section.
[[project-creation]]
=== Project Creation
-New projects can be created in the Gerrit Web UI under `Projects` >
-`Create Project`. The `Create Project` menu entry is only available if
-you have the link:access-control.html#capability_createProject[
+New projects can be created in the Gerrit Web UI under `BROWSE` >
+`Repositories` and then clicking on the `CREATE NEW` button in the
+upper right corner. The `CREATE NEW` button is only available if you
+have the link:access-control.html#capability_createProject[
Create Project] global capability assigned.
Projects can also be created via REST or SSH as described in the
@@ -736,7 +738,7 @@ Gerrit core does not support the deletion of projects.
If the link:https://gerrit-review.googlesource.com/admin/repos/plugins/delete-project[
delete-project,role=external,window=_blank] plugin is installed, projects can be deleted from the
-Gerrit Web UI under `Projects` > `List` > <project> > `General` by
+Gerrit Web UI under `BROWSE` > `Repositories` > <project> > `General` by
clicking on the `Delete` command under `Project Commands`. The `Delete`
command is only available if you have the `Delete Projects` global
capability assigned, or if you own the project and you have the
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 7825e0503b..108022a977 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -28,15 +28,10 @@ link:http://eclipse.org/egit/[EGit,role=external,window=_blank] in Eclipse, is s
Still there are some client-side tools for Gerrit, which can be used
optionally:
-* link:http://eclipse.org/mylyn/[Mylyn Gerrit Connector,role=external,window=_blank]: Gerrit
- integration with Mylyn
* link:https://github.com/uwolfer/gerrit-intellij-plugin[Gerrit
IntelliJ Plugin,role=external,window=_blank]: Gerrit integration with the
link:http://www.jetbrains.com/idea/[IntelliJ Platform,role=external,window=_blank]
-* link:https://play.google.com/store/apps/details?id=com.jbirdvegas.mgerrit[
- mGerrit,role=external,window=_blank]: Android client for Gerrit
-* link:https://github.com/stackforge/gertty[Gertty,role=external,window=_blank]: Console-based
- interface for Gerrit
+* link:https://opendev.org/ttygroup/gertty[gertty]: Console-based interface for Gerrit
[[clone]]
== Clone Gerrit Project
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 6c3c459b39..7e3fa9a027 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -77,7 +77,8 @@ Apache2.0
* jetty:util-ajax
* log:log4j
* lucene:lucene-analyzers-common
-* lucene:lucene-core-and-backward-codecs-merged
+* lucene:lucene-backward-codecs
+* lucene:lucene-core
* lucene:lucene-misc
* lucene:lucene-queryparser
* mime4j:core
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index df0cc42bdc..4302a35779 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -430,6 +430,13 @@ Each queue provides the following metrics:
* `permissions/ref_filter/skip_filter_count`: Rate of ref filter operations
where we skip full evaluation because the user can read all refs
+=== Validation
+
+* `validation/file_count`: Track number of files per change during commit
+ validation, if it exceeds the FILE_COUNT_WARNING_THRESHOLD threshold.
+** `file_count`: number of files in the patchset
+** `host_repo`: host and repository of the change in the format 'host/repo'
+
=== Reviewer Suggestion
* `reviewer_suggestion/query_accounts`: Latency for querying accounts for
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 9ecef3fd35..d0c4553c3b 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -377,7 +377,7 @@ As response the new account status is returned.
"Out Of Office"
----
-If the name was deleted the response is "`204 No Content`".
+If the status was deleted the response is "`204 No Content`".
[[get-username]]
=== Get Username
@@ -449,6 +449,8 @@ a link:#display-name-input[DisplayNameInput] entity.
As response the new display name is returned.
+If the Display Name was deleted the response is "`204 No Content`".
+
[[get-active]]
=== Get Active
--
@@ -1314,6 +1316,7 @@ any account.
"publish_comments_on_push": true,
"work_in_progress_by_default": true,
"allow_browser_notifications": true,
+ "allow_suggest_code_while_commenting": true,
"diff_page_sidebar": "plugin-foo",
"default_base_for_merges": "FIRST_PARENT",
"my": [
@@ -1368,6 +1371,7 @@ link:#preferences-input[PreferencesInput] entity.
"disable_keyboard_shortcuts": true,
"disable_token_highlighting": true,
"allow_browser_notifications": false,
+ "allow_suggest_code_while_commenting": false,
"diff_page_sidebar": "NONE",
"diff_view": "SIDE_BY_SIDE",
"mute_common_path_prefixes": true,
@@ -2716,6 +2720,9 @@ Whether to insert Signed-off-by footer in changes created with the
inline edit feature.
|`allow_browser_notifications` |not set if `false`|
Whether to prompt user to enable browser notification in browser.
+|`allow_suggest_code_while_commenting` |not set if `false`|
+Whether to receive suggested code while writing comments. This feature needs
+a plugin implementation.
|`diff_page_sidebar` |optional|
String indicating which sidebar should be open on the diff page. Set to "NONE"
if no sidebars should be open. Plugin-supplied sidebars will be prefixed with
@@ -2791,6 +2798,9 @@ Whether to insert Signed-off-by footer in changes created with the
inline edit feature.
|`allow_browser_notifications` |not set if `false`|
Whether to prompt user to enable browser notification in browser.
+|`allow_suggest_code_while_commenting` |not set if `false`|
+Whether to receive suggested code while writing comments. This feature needs
+a plugin implementation.
|`diff_page_sidebar` |optional|
String indicating which sidebar should be open on the diff page. Set to "NONE"
if no sidebars should be open. Plugin-supplied sidebars will be prefixed with
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index df5566fe10..ec02cdd3f4 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -63,7 +63,8 @@ the resulting change.
"_number": 4711,
"owner": {
"name": "John Doe"
- }
+ },
+ "current_revision_number": 2
}
----
@@ -126,6 +127,7 @@ Query for open changes of watched projects:
"owner": {
"name": "John Doe"
},
+ "current_revision_number": 2
},
{
"id": "demo~master~I09c8041b5867d5b33170316e2abc34b79bbb8501",
@@ -143,6 +145,7 @@ Query for open changes of watched projects:
"owner": {
"name": "John Doe"
},
+ "current_revision_number": 2,
"_more_changes": true
}
]
@@ -214,7 +217,8 @@ Query that retrieves changes for a user's dashboard:
"labels": {
"Verified": {},
"Code-Review": {}
- }
+ },
+ "current_revision_number": 2
}
],
[],
@@ -437,6 +441,7 @@ link:#approval-info[ApprovalInfo] of the `all` attribute.
"owner": {
"name": "Shawn Pearce"
},
+ "current_revision_number": 1,
"current_revision": "184ebe53805e102605d11f6b143486d15c23a09c",
"revisions": {
"184ebe53805e102605d11f6b143486d15c23a09c": {
@@ -598,7 +603,8 @@ describes the change.
"_number": 3965,
"owner": {
"name": "John Doe"
- }
+ },
+ "current_revision_number": 2
}
----
@@ -680,7 +686,8 @@ returned. Only fields that differ between the change's two states are returned.
"_number": 3965,
"owner": {
"name": "John Doe"
- }
+ },
+ "current_revision_number": 2
},
"new_change_info": {
"id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
@@ -709,7 +716,8 @@ returned. Only fields that differ between the change's two states are returned.
"_number": 3965,
"owner": {
"name": "John Doe"
- }
+ },
+ "current_revision_number": 2
},
}
----
@@ -978,7 +986,8 @@ REJECTED > APPROVED > DISLIKED > RECOMMENDED.
"message": "Patch Set 1:\n\nThis is the second message.\n\nWith a line break.",
"_revision_number": 1
}
- ]
+ ],
+ "current_revision_number": 2
}
----
@@ -1040,10 +1049,44 @@ returned that describes the resulting change.
"owner": {
"_account_id": 1000000
},
+ "current_revision_number": 1,
"current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822"
}
----
+[[get-message]]
+=== Get Commit Message
+--
+'GET /changes/link:#change-id[\{change-id\}]/message'
+--
+
+Returns the commit message of the change (from the current patch set).
+
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/message HTTP/1.0
+----
+
+The commit message is returned as a link:#commit-message-info[
+CommitMessageInfo] entity.
+
+Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "subject": "Add feature X",
+ "full_message": "Add Feature X\n\nFeature X helps with foo.\n\nBug: 123\nChange-Id: I10394472cbd17dd12454f229e4f6de00b143a444\n",
+ "footers": {
+ "Bug": "123",
+ "Change-Id": "I10394472cbd17dd12454f229e4f6de00b143a444"
+ }
+ }
+----
+
[[set-message]]
=== Set Commit Message
--
@@ -1235,7 +1278,8 @@ describes the abandoned change.
"_number": 3965,
"owner": {
"name": "John Doe"
- }
+ },
+ "current_revision_number": 2
}
----
@@ -1306,7 +1350,8 @@ describes the restored change.
"_number": 3965,
"owner": {
"name": "John Doe"
- }
+ },
+ "current_revision_number": 2
}
----
@@ -1331,6 +1376,9 @@ the error message is contained in the response body.
Rebases a change.
+For merge commits always the first parent is rebased. This means the new base becomes the first
+parent of the rebased merge commit while the second parent stays intact.
+
If one of the secondary emails associated with the user performing the operation was used as the
committer email in the current patch set, the same email will be used as the committer email in the
new patch set; otherwise, the user's preferred email will be used.
@@ -1375,6 +1423,7 @@ is included.
"owner": {
"name": "John Doe"
},
+ "current_revision_number": 2,
"current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822",
"revisions": {
"27cc4558b5a3d3387dd11ee2df7a117e7e581822": {
@@ -1525,6 +1574,7 @@ are included.
"owner": {
"_account_id": 1000000
},
+ "current_revision_number": 2,
"current_revision": "c3b2ba222d42a56e05c90f88d4509a124620517d",
"revisions": {
"c3b2ba222d42a56e05c90f88d4509a124620517d": {
@@ -1602,6 +1652,7 @@ are included.
"owner": {
"_account_id": 1000000
},
+ "current_revision_number": 2,
"current_revision": "77eb17a9501a5c21963bc6af56085e60f281acbb",
"revisions": {
"77eb17a9501a5c21963bc6af56085e60f281acbb": {
@@ -1726,7 +1777,8 @@ describes the moved change.
"_number": 3965,
"owner": {
"name": "John Doe"
- }
+ },
+ "current_revision_number": 2
}
----
@@ -1805,7 +1857,8 @@ describes the reverting change.
"_number": 3965,
"owner": {
"name": "John Doe"
- }
+ },
+ "current_revision_number": 2
}
----
@@ -1899,7 +1952,8 @@ is returned. That entity describes the revert changes.
"_number": 3965,
"owner": {
"name": "John Doe"
- }
+ },
+ "current_revision_number": 2
},
{
"id": "anyProject~master~1eee2c9d8f352483781e772f35dc586a69ff5646",
@@ -1917,7 +1971,8 @@ is returned. That entity describes the revert changes.
"_number": 3966,
"owner": {
"name": "Jane Doe"
- }
+ },
+ "current_revision_number": 2
}
]
----
@@ -2002,7 +2057,8 @@ of *that* topic will also be submitted).
"_number": 3965,
"owner": {
"name": "John Doe"
- }
+ },
+ "current_revision_number": 2
}
----
@@ -2155,6 +2211,7 @@ options `NON_VISIBLE_CHANGES` and `TOPIC_CLOSURE`.
}
]
},
+ "current_revision_number": 1,
"current_revision": "9adb9f4c7b40eeee0646e235de818d09164d7379",
"revisions": {
"9adb9f4c7b40eeee0646e235de818d09164d7379": {
@@ -2252,6 +2309,7 @@ options `NON_VISIBLE_CHANGES` and `TOPIC_CLOSURE`.
}
]
},
+ "current_revision_number": 1,
"current_revision": "1bd7c12a38854a2c6de426feec28800623f492c4",
"revisions": {
"1bd7c12a38854a2c6de426feec28800623f492c4": {
@@ -2361,6 +2419,7 @@ describes the destination change after applying the patch.
"owner": {
"name": "John Doe"
},
+ "current_revision_number": 1,
"current_revision": "184ebe53805e102605d11f6b143486d15c23a09c"
}
----
@@ -2477,7 +2536,7 @@ only works if `enable-context` is set to true.
----
[[list-change-robot-comments]]
-=== List Change Robot Comments
+=== List Change Robot Comments (deprecated)
--
'GET /changes/link:#change-id[\{change-id\}]/robotcomments'
--
@@ -2625,6 +2684,7 @@ missing from the result. At least `id`, `project`, `branch`, and
"owner": {
"name": "John Doe"
},
+ "current_revision_number": 2,
"problems": [
{
"message": "Current patch set 1 not found"
@@ -2677,6 +2737,7 @@ Only the change owner, a project owner, or an administrator may fix changes.
"owner": {
"name": "John Doe"
},
+ "current_revision_number": 2,
"problems": [
{
"message": "Current patch set 2 not found"
@@ -3383,6 +3444,36 @@ When change edit doesn't exist for this change yet it is created.
HTTP/1.1 204 No Content
----
+[[put-change-edit-committer-author-identity]]
+=== Change author or committer identity in Change Edit
+--
+'PUT /changes/link:#change-id[\{change-id\}]/edit:identity'
+--
+
+Modify author or committer identity. The request body needs to include a
+link:#change-edit-identity-input[ChangeEditIdentityInput]
+entity. Either `name` or `email` must be provided. `type` must be either `AUTHOR` or `COMMITTER`.
+
+.Request
+----
+ PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit:identity HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "name": "John Doe",
+ "email": "john.doe@example.com",
+ "type": "COMMITTER"
+ }
+----
+
+If a change edit doesn't exist for this change yet, it is created. As
+response "`204 No Content`" is returned.
+
+.Response
+----
+ HTTP/1.1 204 No Content
+----
+
[[get-edit-file]]
=== Retrieve file content from Change Edit
--
@@ -3642,10 +3733,22 @@ limit is not passed, then the default 10 is used.
This REST endpoint only suggests accounts that
* are active
+
* can see the change
-* are visible to the calling user
+
+* are visible to the calling user:
++
+Whether an account is visible to the calling user depends on the
+link:config-gerrit.html#accounts.visibility[accounts.visibility] setting of the
+server. Which account visibility is configured can be checked by opening
+`https://<HOST>/config/server/info?pp=1` in a browser (see field `accounts` >
+link:rest-api-config.html#accounts-config-info[visibility] in the returned
+JSON).
+
* are not already reviewer on the change
+
* don't own the change
+
* are not service users (unless
link:config.html#suggest.skipServiceUsers[skipServiceUsers] is set to `false`)
@@ -4349,6 +4452,7 @@ for the current patch set.
}
]
},
+ "current_revision_number": 2,
"current_revision": "674ac754f91e64a0efb8087e59a176484bd534d1",
"revisions": {
"674ac754f91e64a0efb8087e59a176484bd534d1": {
@@ -4740,6 +4844,7 @@ is included.
"owner": {
"name": "John Doe"
},
+ "current_revision_number": 2,
"current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822",
"revisions": {
"27cc4558b5a3d3387dd11ee2df7a117e7e581822": {
@@ -4840,7 +4945,8 @@ of *that* topic will also be submitted).
"_number": 3965,
"owner": {
"name": "John Doe"
- }
+ },
+ "current_revision_number": 2
}
----
@@ -5392,7 +5498,7 @@ describes the updated comment.
----
[[list-robot-comments]]
-=== List Robot Comments
+=== List Robot Comments (deprecated)
--
'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/robotcomments/'
--
@@ -5449,7 +5555,7 @@ map are sorted by file path.
----
[[get-robot-comment]]
-=== Get Robot Comment
+=== Get Robot Comment (deprecated)
--
'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/robotcomments/link:#comment-id[\{comment-id\}]'
--
@@ -5689,7 +5795,7 @@ is returned.
Content-Disposition: attachment
Content-Type: text/plain; charset=UTF-8
- The existing change edit could not be merged with another tree.
+ Rebasing change edit onto another patchset results in merge conflicts. Download the edit patchset and rebase manually to preserve changes.
----
[[apply-provided-fix]]
@@ -6413,7 +6519,8 @@ describes the resulting cherry-pick change.
"_number": 3965,
"owner": {
"name": "John Doe"
- }
+ },
+ "current_revision_number": 2
}
----
@@ -7021,6 +7128,19 @@ the commit message within a change edit.
|`message` ||New commit message.
|===========================
+[[change-edit-identity-input]]
+=== ChangeEditIdentityInput
+The `ChangeEditIdentityInput` entity contains information for changing
+the author or committer identity within a change edit.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name ||Description
+|`name` |optional|The name of the author/committer. If not specified, the existing name will be used.
+|`email` |optional|The email of the author/committer. If not specified, the existing email will be used.
+|`type` ||Type of the identity being edited. Must be either `AUTHOR` or `COMMITTER`.
+|===========================
+
[[change-info]]
=== ChangeInfo
The `ChangeInfo` entity contains information about a change.
@@ -7144,11 +7264,9 @@ link:#detailed-labels[detailed labels] are requested.
|`reviewers` |optional|
The reviewers as a map that maps a reviewer state to a list of
link:rest-api-accounts.html#account-info[AccountInfo] entities.
-Possible reviewer states are `REVIEWER`, `CC` and `REMOVED`. +
+Possible reviewer states are `REVIEWER`, `CC`. +
`REVIEWER`: Users with at least one non-zero vote on the change. +
`CC`: Users that were added to the change, but have not voted. +
-`REMOVED`: Users that were previously reviewers on the change, but have
-been removed. +
Only set if link:#labels[labels] or
link:#detailed-labels[detailed labels] are requested.
|`pending_reviewers` |optional|
@@ -7158,6 +7276,11 @@ reviewer updates to report. These are reviewers who have not yet been
notified about being added to or removed from the change. +
Only set if link:#labels[labels] or
link:#detailed-labels[detailed labels] are requested.
+Possible states are `REVIEWER`, `CC` and `REMOVED`. +
+`REVIEWER`: Users with at least one non-zero vote on the change. +
+`CC`: Users that were added to the change, but have not voted. +
+`REMOVED`: Users that were previously reviewers on the change, but have
+been removed.
|`reviewer_updates`|optional|
Updates to reviewers set for the change as
link:#review-update-info[ReviewerUpdateInfo] entities.
@@ -7166,6 +7289,8 @@ Only set if link:#reviewer-updates[reviewer updates] are requested.
Messages associated with the change as a list of
link:#change-message-info[ChangeMessageInfo] entities. +
Only set if link:#messages[messages] are requested.
+|`current_revision_number`||The number of the current patch set of this
+change. +
|`current_revision` |optional|
The commit ID of the current patch set of this change. +
Only set if link:#current-revision[the current revision] is requested
@@ -7465,7 +7590,8 @@ parameter (see link:#list-change-comments[List Change Comments]) is set.
Mime type of the file where the comment is written. Available only if the
"enable-context" parameter (see link:#list-change-comments[List Change Comments])
is set.
-
+|`fix_suggestions`|optional|Suggested fixes for this comment as a list of
+<<fix-suggestion-info,FixSuggestionInfo>> entities.
|===========================
[[comment-input]]
@@ -7513,6 +7639,8 @@ link:#review-input[ReviewInput]. Votes/comments that contain `tag` with
Whether or not the comment must be addressed by the user. This value will
default to false if the comment is an orphan, or the value of the `in_reply_to`
comment if it is supplied.
+|`fix_suggestions`|optional|Suggested fixes for this comment as a list of
+<<fix-suggestion-info,FixSuggestionInfo>> entities.
|===========================
[[comment-range]]
@@ -7579,6 +7707,21 @@ Links to the commit in external sites for resolving conflicts as a list of
link:#web-link-info[WebLinkInfo] entities.
|===========================
+[[commit-message-info]]
+=== CommitMessageInfo
+The `CommitMessageInfo` entity contains information about the commit
+message of a change.
+
+[options="header",cols="1,6"]
+|============================
+|Field Name |Description
+|`subject` |The subject of the change (first line of the commit
+message).
+|`full_message` |Full commit message of the change.
+|`footers` |The footers from the commit message as a map of
+key-value pairs.
+|============================
+
[[commit-message-input]]
=== CommitMessageInput
The `CommitMessageInput` entity contains information for changing
@@ -7588,6 +7731,9 @@ the commit message of a change.
|=============================
|Field Name ||Description
|`message` ||New commit message.
+|`committer_email`|optional|
+New message is committed using this email address. Only the
+registered emails of the calling user are considered valid.
|`notify` |optional|
Notify handling that defines to whom email notifications should be sent
after the commit message was updated. +
@@ -8521,8 +8667,10 @@ Timestamp of the update.
The account which modified state of the reviewer in question as
link:rest-api-accounts.html#account-info[AccountInfo] entity.
|`reviewer`|
-The reviewer account added or removed from the change as an
-link:rest-api-accounts.html#account-info[AccountInfo] entity.
+The reviewer added or removed from the change as an
+link:rest-api-accounts.html#account-info[AccountInfo] entity. For
+reviewers by email the `AccountInfo` doesn't contain an account ID but
+only the email and optionally a name.
|`state`|
The reviewer state, one of `REVIEWER`, `CC` or `REMOVED`.
|===========================
@@ -8551,7 +8699,7 @@ label names to the voting values.
|`comments` |optional|
The comments that should be added as a map that maps a file path to a
list of link:#comment-input[CommentInput] entities.
-|`robot_comments` |optional|
+|`robot_comments` |optional, deprecated|
The robot comments that should be added as a map that maps a file path
to a list of link:#robot-comment-input[RobotCommentInput] entities.
|`drafts` |optional|
@@ -8630,9 +8778,8 @@ If true, the change was moved from WIP to ready for review as a result of this
action. Not set if false.
|`error` |optional|
Error message for non-200 responses.
-|`change_info` |optional|
-Post-update change information. Only set if `response_format_options` were
-set.
+|`change_info` ||
+Post-update change information.
|============================
[[reviewer-info]]
@@ -8801,7 +8948,7 @@ selector menu. May be null if no description is set.
|===========================
[[robot-comment-info]]
-=== RobotCommentInfo
+=== RobotCommentInfo (deprecated)
The `RobotCommentInfo` entity contains information about a robot inline
comment.
@@ -8817,12 +8964,10 @@ In addition `RobotCommentInfo` has the following fields:
|`url` |optional|URL to more information.
|`properties` |optional|Robot specific properties as map that maps arbitrary
keys to values.
-|`fix_suggestions`|optional|Suggested fixes for this robot comment as a list of
-<<fix-suggestion-info,FixSuggestionInfo>> entities.
|===========================
[[robot-comment-input]]
-=== RobotCommentInput
+=== RobotCommentInput (deprecated)
The `RobotCommentInput` entity contains information for creating an inline
robot comment.
@@ -8852,8 +8997,6 @@ The comment message.
|`url` |optional|URL to more information.
|`properties` |optional|Robot specific properties as map that maps arbitrary
keys to values.
-|`fix_suggestions`|optional|Suggested fixes for this robot comment as a list of
-<<fix-suggestion-info,FixSuggestionInfo>> entities.
|===========================
[[rule-input]]
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index ec1ac03936..3712135034 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -875,6 +875,49 @@ The entries in the map are sorted by capability ID.
}
----
+[[list-experiments]]
+=== List Experiments
+--
+'GET /config/server/experiments'
+--
+
+Lists the experiments that are available in the system.
+
+Requires the caller to have the link:access-control.html#capability_administrateServer[
+Administrate Server] global capability.
+
+As result a map of experiment names to link:#experiment-info[Experiment] entities is returned.
+
+The entries in the map are sorted by experiment name.
+
+.Request
+----
+ GET /config/server/experiments/ HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "GerritBackendFeature__allow_fix_suggestions_in_comments": {
+ "enabled": false
+ },
+ "GerritBackendFeature__attach_nonce_to_documentation": {
+ "enabled": true
+ }
+ }
+----
+
+It is possible to specify the following options:
+
+[[list-experiments-enabled-only]]
+--
+* `enabled-only`: If specified only enabled experiments are listed.
+--
+
[[list-tasks]]
=== List Tasks
--
@@ -931,7 +974,7 @@ command.
"state": "SLEEPING",
"start_time": "2014-06-11 12:58:51.508000000",
"delay": 3287966,
- "command": "Log File Compressor"
+ "command": "Log File Manager"
}
]
----
@@ -1420,6 +1463,325 @@ Using this endpoint Gerrit admins can also index change(s) which are not visible
When `delete_missing` is set to `true` changes to be reindexed which are missing in NoteDb
will be deleted in the index.
+[[list-indexes]]
+=== List Indexes
+--
+'GET /config/server/indexes'
+--
+
+Lists the indexes used by Gerrit. It provides details about the index versions,
+which index version is used to search and which versions are written to.
+
+This endpoint requires the
+link:access-control.html#capability_maintainServer[Maintain Server]
+capability.
+
+.Request
+----
+ GET /config/server/indexes/ HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ [
+ {
+ "name": "accounts",
+ "versions": {
+ "13": {
+ "write": true,
+ "search": true
+ }
+ }
+ },
+ {
+ "name": "changes",
+ "versions": {
+ "83": {
+ "write": true,
+ "search": true
+ },
+ "84": {
+ "write": true,
+ "search": false
+ }
+ }
+ },
+ {
+ "name": "groups",
+ "versions": {
+ "10": {
+ "write": true,
+ "search": true
+ }
+ }
+ },
+ {
+ "name": "projects",
+ "versions": {
+ "8": {
+ "write": true,
+ "search": true
+ }
+ }
+ }
+ [
+----
+
+=== Get Index
+--
+'GET /config/server/indexes/changes'
+--
+
+Get an index used by Gerrit. It provides details about the index versions, which
+index version is used to search and which versions are written to.
+
+.Request
+----
+ 'GET /config/server/indexes/changes'
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "name": "changes",
+ "versions": {
+ "83": {
+ "write": true,
+ "search": true
+ },
+ "84": {
+ "write": true,
+ "search": false
+ }
+ }
+ }
+----
+
+=== List Index Versions
+--
+'GET /config/server/indexes/changes/versions'
+--
+
+Lists versions of an index used by Gerrit.
+
+.Request
+----
+ 'GET /config/server/indexes/changes/versions'
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "83": {
+ "write": true,
+ "search": true
+ },
+ "84": {
+ "write": true,
+ "search": false
+ }
+ }
+----
+
+=== Get Index Version
+--
+'GET /config/server/indexes/changes/versions/85'
+--
+
+Get info about one version of an index used by Gerrit.
+
+.Request
+----
+ 'GET /config/server/indexes/changes/versions/84'
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "write": true,
+ "search": false
+ }
+----
+
+[[snapshot-index]]
+=== Create Index Snapshot
+
+These endpoints allow Gerrit admins to create index snapshots.
+Created snapshots can be used as a backup of the index.
+
+It is possible to create a snapshot of all indexes, snapshot of one index or
+snapshot of one index version.
+
+The snapshots will be stored on the server at `$SITE/index/snapshots/$ID`.
+The `$ID` can be optionally provided in link:#snapshot-index-input[SnapshotIndex.Input]
+or will default to the current local time in ISO8601 format.
+
+Only snapshots of indexes that Gerrit currently writes to can be created.
+
+Note, that the creation of multiple snapshots, e.g. of different index
+versions, is not atomic. If a consistent state over multiple indexes is
+required, the server has to be put into read-only mode before creating
+the snapshot.
+
+==== Create Snapshot of All Indexes
+--
+'POST /config/server/snapshot.indexes HTTP/1.0'
+--
+
+.Request
+----
+ POST /config/server/snapshot.indexes HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "id": "snapshot-1"
+ }
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "id": "snapshot-1"
+ }
+----
+
+==== Create Snapshot of one Index
+--
+'POST /config/server/indexes/link:#index-name[\{index-name\}]/snapshot'
+--
+
+This creates a snapshot of all write index versions of the specified index.
+
+.Request
+----
+ POST /config/server/indexes/accounts/snapshot HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "id": "snapshot-1"
+ }
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "id": "snapshot-1"
+ }
+----
+
+==== Create Snapshot of one Index Version
+--
+'POST /config/server/indexes/link:#index-name[\{index-name\}]/versions/#index-version[\{index-version\}]/snapshot'
+--
+
+This creates a snapshot of one index version of the specified index.
+
+.Request
+----
+ POST /config/server/indexes/changes/versions/84/snapshot HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "id": "snapshot-1"
+ }
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "id": "snapshot-1"
+ }
+----
+
+=== Reindex an Index Version
+--
+'POST /config/server/indexes/link:#index-name[\{index-name\}]/versions/#index-version[\{index-version\}]/reindex'
+--
+
+This endpoint allows to trigger background reindexing of an index version. It is
+also supported to specify whether to reuse existing up-to-date (non-stale) index
+documents and whether to notifyListeners or not.
+
+.Request
+----
+ POST /config/server/indexes/changes/versions/84/reindex HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "reuse": "true",
+ "notifyListeners": "false"
+ }
+----
+
+.Response
+----
+ HTTP/1.1 202 Accepted
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+----
+
+[[experiment-endpoints]]
+== Experiment Endpoints
+
+[[get-experiment]]
+=== Get Experiment
+--
+'GET /config/server/experiments/link:#experiment-name[\{experiment-name\}]
+--
+
+Retrieves the details of the experiment with the given name.
+
+Requires the caller to have the link:access-control.html#capability_administrateServer[
+Administrate Server] global capability.
+
+.Request
+----
+ GET /config/server/experiments/mGerritBackendFeature__attach_nonce_to_documentation HTTP/1.0
+----
+
+As response an link:#experiment-info[Experiment] entity is returned that
+describes the experiment.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "enabled": true
+ }
+----
[[ids]]
== IDs
@@ -1434,10 +1796,21 @@ plugin name: "<plugin-name>-<cache-name>".
Gerrit core caches can optionally be prefixed with "gerrit":
"gerrit-<cache-name>".
+[[experiment-name]]
+=== \{experiment-name\}
+The name of the experiment.
+
[[task-id]]
=== \{task-id\}
The ID of the task (hex string).
+[[index-name]]
+=== \{index-name\}
+The name of the index. Can be any of: "accounts", "changes", "groups", "projects".
+
+[[index-version]]
+=== \{index-version\}
+The version of the index. This is an integer.
[[json-entities]]
== JSON Entities
@@ -1760,6 +2133,16 @@ Missing if value was not previously configured.
|`new_value` |The new config value, picked up after reload.
|======================
+[[experiment-info]]
+=== ExperimentInfo
+The `ExperimentInfo` entity contains information about an experiment.
+
+[options="header",cols="1,6"]
+|============================
+|Field Name |Description
+|`enabled` |Whether the experiment is enabled.
+|============================
+
[[download-info]]
=== DownloadInfo
The `DownloadInfo` entity contains information about supported download
@@ -1787,6 +2170,9 @@ download scheme and its commands.
|`url` ||
The URL of the download scheme, where '${project}' is used as
placeholder for the project name.
+|`description` |optional|
+An optional description of how the scheme works and maybe comparing
+it to other schemes, explaining the pros and cons of each option.
|`is_auth_required` |not set if `false`|
Whether this download scheme requires authentication.
|`is_auth_supported` |not set if `false`|
@@ -2067,6 +2453,19 @@ columns in the dashboard. If empty, the default is to display all submit
requirements that are applicable for changes appearing in the dashboard.
|=======================================
+[[snapshot-index-input]]
+=== SnapshotIndex.Input
+The `SnapshotIndex.Input` entity contains the parameters used to create an
+index snapshot.
+
+[options="header",cols="1,^1,5"]
+|=======================
+|Field Name ||Description
+|`id` | optional |
+A string ID that will be used as the folder name containing the
+snapshots. Defaults to current timestamp.
+|=======================
+
[[sshd-info]]
=== SshdInfo
The `SshdInfo` entity contains information about Gerrit
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index fff9d0b0cf..b0e1b49f4c 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -2467,6 +2467,100 @@ Skip the given number of tags from the beginning of the list.
]
----
+DescendingOrder(d)::
+Sort the returned tags in descending order.
++
+.Request
+----
+ GET /projects/work%2Fmy-project/tags?d HTTP/1.0
+----
++
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ [
+ {
+ "ref": "refs/tags/v3.0",
+ "revision": "c628685b3c5a3614571ecb5c8fceb85db9112313",
+ "object": "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+ "message": "Signed tag\n-----BEGIN PGP SIGNATURE-----\nVersion: GnuPG v1.4.11 (GNU/Linux)\n\niQEcBAABAgAGBQJUMlqYAAoJEPI2qVPgglptp7MH/j+KFcittFbxfSnZjUl8n5IZ\nveZo7wE+syjD9sUbMH4EGv0WYeVjphNTyViBof+stGTNkB0VQzLWI8+uWmOeiJ4a\nzj0LsbDOxodOEMI5tifo02L7r4Lzj++EbqtKv8IUq2kzYoQ2xjhKfFiGjeYOn008\n9PGnhNblEHZgCHguGR6GsfN8bfA2XNl9B5Ysl5ybX1kAVs/TuLZR4oDMZ/pW2S75\nhuyNnSgcgq7vl2gLGefuPs9lxkg5Fj3GZr7XPZk4pt/x1oiH7yXxV4UzrUwg2CC1\nfHrBlNbQ4uJNY8TFcwof52Z0cagM5Qb/ZSLglHbqEDGA68HPqbwf5z2hQyU2/U4\u003d\n\u003dZtUX\n-----END PGP SIGNATURE-----",
+ "tagger": {
+ "name": "David Pursehouse",
+ "email": "david.pursehouse@sonymobile.com",
+ "date": "2014-10-06 09:02:16.000000000",
+ "tz": 540
+ }
+ },
+ {
+ "ref": "refs/tags/v2.0",
+ "revision": "1624f5af8ae89148d1a3730df8c290413e3dcf30"
+ },
+ {
+ "ref": "refs/tags/v1.0",
+ "revision": "49ce77fdcfd3398dc0dedbe016d1a425fd52d666",
+ "object": "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+ "message": "Annotated tag",
+ "tagger": {
+ "name": "David Pursehouse",
+ "email": "david.pursehouse@sonymobile.com",
+ "date": "2014-10-06 07:35:03.000000000",
+ "tz": 540
+ }
+ }
+ ]
+----
+
+SortBy(sortby)::
+Sort the returned tags by one of the supported sort options: ref (default), creation_time.
++
+.Request
+----
+ GET /projects/work%2Fmy-project/tags?sortby=creation_time HTTP/1.0
+----
++
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ [
+ {
+ "ref": "refs/tags/v1.0",
+ "revision": "49ce77fdcfd3398dc0dedbe016d1a425fd52d666",
+ "object": "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+ "message": "Annotated tag",
+ "tagger": {
+ "name": "David Pursehouse",
+ "email": "david.pursehouse@sonymobile.com",
+ "date": "2014-10-06 07:35:03.000000000",
+ "tz": 540
+ }
+ },
+ {
+ "ref": "refs/tags/v3.0",
+ "revision": "c628685b3c5a3614571ecb5c8fceb85db9112313",
+ "object": "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+ "message": "Signed tag\n-----BEGIN PGP SIGNATURE-----\nVersion: GnuPG v1.4.11 (GNU/Linux)\n\niQEcBAABAgAGBQJUMlqYAAoJEPI2qVPgglptp7MH/j+KFcittFbxfSnZjUl8n5IZ\nveZo7wE+syjD9sUbMH4EGv0WYeVjphNTyViBof+stGTNkB0VQzLWI8+uWmOeiJ4a\nzj0LsbDOxodOEMI5tifo02L7r4Lzj++EbqtKv8IUq2kzYoQ2xjhKfFiGjeYOn008\n9PGnhNblEHZgCHguGR6GsfN8bfA2XNl9B5Ysl5ybX1kAVs/TuLZR4oDMZ/pW2S75\nhuyNnSgcgq7vl2gLGefuPs9lxkg5Fj3GZr7XPZk4pt/x1oiH7yXxV4UzrUwg2CC1\nfHrBlNbQ4uJNY8TFcwof52Z0cagM5Qb/ZSLglHbqEDGA68HPqbwf5z2hQyU2/U4\u003d\n\u003dZtUX\n-----END PGP SIGNATURE-----",
+ "tagger": {
+ "name": "David Pursehouse",
+ "email": "david.pursehouse@sonymobile.com",
+ "date": "2014-10-06 09:02:16.000000000",
+ "tz": 540
+ }
+ },
+ {
+ "ref": "refs/tags/v2.0",
+ "revision": "1624f5af8ae89148d1a3730df8c290413e3dcf30"
+ }
+ ]
+----
+
Substring(m)::
Limit the results to those tags that match the specified substring.
+
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 0744dedf52..109500251c 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -848,6 +848,7 @@ Matches changes with a +2 code review where the reviewer is jsmith.
The special "owner" parameter corresponds to the change owner. Matches
all changes that have a +2 vote from the change owner.
+[[non_uploader]]
`label:Code-Review=+2,user=non_uploader`::
`label:Code-Review=ok,user=non_uploader`::
`label:Code-Review=+2,non_uploader`::
diff --git a/Documentation/user-suggest-edits.txt b/Documentation/user-suggest-edits.txt
index fa49eeb7e9..55181a2dca 100644
--- a/Documentation/user-suggest-edits.txt
+++ b/Documentation/user-suggest-edits.txt
@@ -37,6 +37,21 @@ by using the commands from the link:user-review-ui.html#download[download drop-d
Alternatively, you can use the copy to clipboard button to copy a suggested
edit to your clipboard and then you can paste it into your editor.
+== Generate Suggestion
+
+Following UI needs to be activated by a plugin that implements SuggestionsProvider. Gerrit is providing just UI.
+
+** When a user types a comment, Gerrit queries a plugin for a code snippet. When there is a snippet, the user can see a preview of snippet under comment.
+
+image::images/generated-suggested-edit-preview.png["Generate Suggested Edit", align="center", width=400]
+
+** A user needs to click on "ADD SUGGESTION TO COMMENT" button if they want to use this suggestion. Otherwise the suggestion is never used.
+
+image::images/generated-suggested-edit-added.png["Added Generated Suggested Edit", align="center", width=400]
+
+** By clicking on "ADD SUGGESTION TO COMMENT" button, the suggestion is added to end of comment. The user can then edit the suggestion, if needed.
+
+
GERRIT
------
Part of link:index.html[Gerrit Code Review]
diff --git a/MODULE.bazel b/MODULE.bazel
new file mode 100644
index 0000000000..0b932b8d8c
--- /dev/null
+++ b/MODULE.bazel
@@ -0,0 +1,2 @@
+# TODO(davido): Migrate all dependencies from WORKSPACE to MODULE.bazel
+# https://issues.gerritcodereview.com/issues/303819949
diff --git a/WORKSPACE b/WORKSPACE
index 8ce21d57f5..1c168c6271 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -26,47 +26,22 @@ workspace(
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")
-load("//tools/bzl:maven_jar.bzl", "GERRIT", "MAVEN_LOCAL", "maven_jar")
load("//plugins:external_plugin_deps.bzl", "external_plugin_deps")
load("//tools:nongoogle.bzl", "declare_nongoogle_deps")
load("//tools:deps.bzl", "CAFFEINE_VERS", "java_dependencies")
http_archive(
- name = "platforms",
- sha256 = "3a561c99e7bdbe9173aa653fd579fe849f1d8d67395780ab4770b1f381431d51",
- urls = [
- "https://mirror.bazel.build/github.com/bazelbuild/platforms/releases/download/0.0.7/platforms-0.0.7.tar.gz",
- "https://github.com/bazelbuild/platforms/releases/download/0.0.7/platforms-0.0.7.tar.gz",
- ],
-)
-
-http_archive(
- name = "rbe_jdk11",
- sha256 = "dbcfd6f26589ef506b91fe03a12dc559ca9c84699e4cf6381150522287f0e6f6",
- strip_prefix = "rbe_autoconfig-3.1.0",
- urls = [
- "https://gerrit-bazel.storage.googleapis.com/rbe_autoconfig/v3.1.0.tar.gz",
- "https://github.com/davido/rbe_autoconfig/archive/v3.1.0.tar.gz",
- ],
-)
-
-http_archive(
- name = "com_google_protobuf",
- sha256 = "3bd7828aa5af4b13b99c191e8b1e884ebfa9ad371b0ce264605d347f135d2568",
- strip_prefix = "protobuf-3.19.4",
- urls = [
- "https://github.com/protocolbuffers/protobuf/archive/v3.19.4.tar.gz",
- ],
+ name = "rules_nodejs",
+ patch_args = ["-p1"],
+ patches = ["//tools:rules_nodejs-5.8.4-node_versions.bzl.patch"],
+ sha256 = "8fc8e300cb67b89ceebd5b8ba6896ff273c84f6099fc88d23f24e7102319d8fd",
+ urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.8.4/rules_nodejs-core-5.8.4.tar.gz"],
)
-load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
-
-protobuf_deps()
-
http_archive(
name = "build_bazel_rules_nodejs",
- sha256 = "94070eff79305be05b7699207fbac5d2608054dd53e6109f7d00d923919ff45a",
- urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.8.2/rules_nodejs-5.8.2.tar.gz"],
+ sha256 = "709cc0dcb51cf9028dd57c268066e5bc8f03a119ded410a13b5c3925d6e43c48",
+ urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.8.4/rules_nodejs-5.8.4.tar.gz"],
)
load("@build_bazel_rules_nodejs//:repositories.bzl", "build_bazel_rules_nodejs_dependencies")
@@ -99,9 +74,11 @@ browser_repositories(
firefox = True,
)
-register_toolchains("//tools:error_prone_warnings_toolchain_java11_definition")
+declare_nongoogle_deps()
+
+load("//tools:defs.bzl", "gerrit_init")
-register_toolchains("//tools:error_prone_warnings_toolchain_java17_definition")
+gerrit_init()
# Java-Prettify external repository consumed from git submodule
local_repository(
@@ -137,12 +114,10 @@ http_file(
],
)
-declare_nongoogle_deps()
-
load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install")
node_repositories(
- node_version = "17.9.1",
+ node_version = "20.9.0",
yarn_version = "1.22.19",
)
diff --git a/contrib/bug-icon/1_bug_icon_16x16.ai b/contrib/bug-icon/1_bug_icon_16x16.ai
new file mode 100644
index 0000000000..5872a27d32
--- /dev/null
+++ b/contrib/bug-icon/1_bug_icon_16x16.ai
@@ -0,0 +1,2189 @@
+%PDF-1.6 %âãÏÓ
+1 0 obj <</Metadata 2 0 R/OCProperties<</D<</ON[25 0 R]/Order 26 0 R/RBGroups[]>>/OCGs[25 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <</Length 58405/Subtype/XML/Type/Metadata>>stream
+<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
+<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 9.0-c000 79.f845eb1, 2022/11/03-19:28:45 ">
+ <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <rdf:Description rdf:about=""
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:xmp="http://ns.adobe.com/xap/1.0/"
+ xmlns:xmpGImg="http://ns.adobe.com/xap/1.0/g/img/"
+ xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
+ xmlns:stRef="http://ns.adobe.com/xap/1.0/sType/ResourceRef#"
+ xmlns:stEvt="http://ns.adobe.com/xap/1.0/sType/ResourceEvent#"
+ xmlns:illustrator="http://ns.adobe.com/illustrator/1.0/"
+ xmlns:xmpTPg="http://ns.adobe.com/xap/1.0/t/pg/"
+ xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#"
+ xmlns:xmpG="http://ns.adobe.com/xap/1.0/g/"
+ xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
+ <dc:format>application/pdf</dc:format>
+ <dc:title>
+ <rdf:Alt>
+ <rdf:li xml:lang="x-default">1 icono bug 16x16</rdf:li>
+ </rdf:Alt>
+ </dc:title>
+ <xmp:MetadataDate>2023-12-13T19:08:48-04:00</xmp:MetadataDate>
+ <xmp:ModifyDate>2023-12-13T19:08:48-04:00</xmp:ModifyDate>
+ <xmp:CreateDate>2023-12-13T19:08:48-03:00</xmp:CreateDate>
+ <xmp:CreatorTool>Adobe Illustrator 27.1 (Windows)</xmp:CreatorTool>
+ <xmp:Thumbnails>
+ <rdf:Alt>
+ <rdf:li rdf:parseType="Resource">
+ <xmpGImg:width>256</xmpGImg:width>
+ <xmpGImg:height>256</xmpGImg:height>
+ <xmpGImg:format>JPEG</xmpGImg:format>
+ <xmpGImg:image>/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA&#xA;AQBIAAAAAQAB/+IMWElDQ19QUk9GSUxFAAEBAAAMSExpbm8CEAAAbW50clJHQiBYWVogB84AAgAJ&#xA;AAYAMQAAYWNzcE1TRlQAAAAASUVDIHNSR0IAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1IUCAgAAAA&#xA;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARY3BydAAAAVAAAAAz&#xA;ZGVzYwAAAYQAAABsd3RwdAAAAfAAAAAUYmtwdAAAAgQAAAAUclhZWgAAAhgAAAAUZ1hZWgAAAiwA&#xA;AAAUYlhZWgAAAkAAAAAUZG1uZAAAAlQAAABwZG1kZAAAAsQAAACIdnVlZAAAA0wAAACGdmlldwAA&#xA;A9QAAAAkbHVtaQAAA/gAAAAUbWVhcwAABAwAAAAkdGVjaAAABDAAAAAMclRSQwAABDwAAAgMZ1RS&#xA;QwAABDwAAAgMYlRSQwAABDwAAAgMdGV4dAAAAABDb3B5cmlnaHQgKGMpIDE5OTggSGV3bGV0dC1Q&#xA;YWNrYXJkIENvbXBhbnkAAGRlc2MAAAAAAAAAEnNSR0IgSUVDNjE5NjYtMi4xAAAAAAAAAAAAAAAS&#xA;c1JHQiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&#xA;AAAAAAAAAAAAAFhZWiAAAAAAAADzUQABAAAAARbMWFlaIAAAAAAAAAAAAAAAAAAAAABYWVogAAAA&#xA;AAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9kZXNj&#xA;AAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAABZJRUMgaHR0cDovL3d3dy5p&#xA;ZWMuY2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAA&#xA;AAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAA&#xA;AAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAA&#xA;AAAAAAAAAAAAAAAAAAAAAGRlc2MAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBp&#xA;biBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAACxSZWZlcmVuY2UgVmlld2luZyBDb25kaXRpb24gaW4g&#xA;SUVDNjE5NjYtMi4xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB2aWV3AAAAAAATpP4AFF8uABDP&#xA;FAAD7cwABBMLAANcngAAAAFYWVogAAAAAABMCVYAUAAAAFcf521lYXMAAAAAAAAAAQAAAAAAAAAA&#xA;AAAAAAAAAAAAAAKPAAAAAnNpZyAAAAAAQ1JUIGN1cnYAAAAAAAAEAAAAAAUACgAPABQAGQAeACMA&#xA;KAAtADIANwA7AEAARQBKAE8AVABZAF4AYwBoAG0AcgB3AHwAgQCGAIsAkACVAJoAnwCkAKkArgCy&#xA;ALcAvADBAMYAywDQANUA2wDgAOUA6wDwAPYA+wEBAQcBDQETARkBHwElASsBMgE4AT4BRQFMAVIB&#xA;WQFgAWcBbgF1AXwBgwGLAZIBmgGhAakBsQG5AcEByQHRAdkB4QHpAfIB+gIDAgwCFAIdAiYCLwI4&#xA;AkECSwJUAl0CZwJxAnoChAKOApgCogKsArYCwQLLAtUC4ALrAvUDAAMLAxYDIQMtAzgDQwNPA1oD&#xA;ZgNyA34DigOWA6IDrgO6A8cD0wPgA+wD+QQGBBMEIAQtBDsESARVBGMEcQR+BIwEmgSoBLYExATT&#xA;BOEE8AT+BQ0FHAUrBToFSQVYBWcFdwWGBZYFpgW1BcUF1QXlBfYGBgYWBicGNwZIBlkGagZ7BowG&#xA;nQavBsAG0QbjBvUHBwcZBysHPQdPB2EHdAeGB5kHrAe/B9IH5Qf4CAsIHwgyCEYIWghuCIIIlgiq&#xA;CL4I0gjnCPsJEAklCToJTwlkCXkJjwmkCboJzwnlCfsKEQonCj0KVApqCoEKmAquCsUK3ArzCwsL&#xA;Igs5C1ELaQuAC5gLsAvIC+EL+QwSDCoMQwxcDHUMjgynDMAM2QzzDQ0NJg1ADVoNdA2ODakNww3e&#xA;DfgOEw4uDkkOZA5/DpsOtg7SDu4PCQ8lD0EPXg96D5YPsw/PD+wQCRAmEEMQYRB+EJsQuRDXEPUR&#xA;ExExEU8RbRGMEaoRyRHoEgcSJhJFEmQShBKjEsMS4xMDEyMTQxNjE4MTpBPFE+UUBhQnFEkUahSL&#xA;FK0UzhTwFRIVNBVWFXgVmxW9FeAWAxYmFkkWbBaPFrIW1hb6Fx0XQRdlF4kXrhfSF/cYGxhAGGUY&#xA;ihivGNUY+hkgGUUZaxmRGbcZ3RoEGioaURp3Gp4axRrsGxQbOxtjG4obshvaHAIcKhxSHHscoxzM&#xA;HPUdHh1HHXAdmR3DHeweFh5AHmoelB6+HukfEx8+H2kflB+/H+ogFSBBIGwgmCDEIPAhHCFIIXUh&#xA;oSHOIfsiJyJVIoIiryLdIwojOCNmI5QjwiPwJB8kTSR8JKsk2iUJJTglaCWXJccl9yYnJlcmhya3&#xA;JugnGCdJJ3onqyfcKA0oPyhxKKIo1CkGKTgpaymdKdAqAio1KmgqmyrPKwIrNitpK50r0SwFLDks&#xA;biyiLNctDC1BLXYtqy3hLhYuTC6CLrcu7i8kL1ovkS/HL/4wNTBsMKQw2zESMUoxgjG6MfIyKjJj&#xA;Mpsy1DMNM0YzfzO4M/E0KzRlNJ402DUTNU01hzXCNf02NzZyNq426TckN2A3nDfXOBQ4UDiMOMg5&#xA;BTlCOX85vDn5OjY6dDqyOu87LTtrO6o76DwnPGU8pDzjPSI9YT2hPeA+ID5gPqA+4D8hP2E/oj/i&#xA;QCNAZECmQOdBKUFqQaxB7kIwQnJCtUL3QzpDfUPARANER0SKRM5FEkVVRZpF3kYiRmdGq0bwRzVH&#xA;e0fASAVIS0iRSNdJHUljSalJ8Eo3Sn1KxEsMS1NLmkviTCpMcky6TQJNSk2TTdxOJU5uTrdPAE9J&#xA;T5NP3VAnUHFQu1EGUVBRm1HmUjFSfFLHUxNTX1OqU/ZUQlSPVNtVKFV1VcJWD1ZcVqlW91dEV5JX&#xA;4FgvWH1Yy1kaWWlZuFoHWlZaplr1W0VblVvlXDVchlzWXSddeF3JXhpebF69Xw9fYV+zYAVgV2Cq&#xA;YPxhT2GiYfViSWKcYvBjQ2OXY+tkQGSUZOllPWWSZedmPWaSZuhnPWeTZ+loP2iWaOxpQ2maafFq&#xA;SGqfavdrT2una/9sV2yvbQhtYG25bhJua27Ebx5veG/RcCtwhnDgcTpxlXHwcktypnMBc11zuHQU&#xA;dHB0zHUodYV14XY+dpt2+HdWd7N4EXhueMx5KnmJeed6RnqlewR7Y3vCfCF8gXzhfUF9oX4BfmJ+&#xA;wn8jf4R/5YBHgKiBCoFrgc2CMIKSgvSDV4O6hB2EgITjhUeFq4YOhnKG14c7h5+IBIhpiM6JM4mZ&#xA;if6KZIrKizCLlov8jGOMyo0xjZiN/45mjs6PNo+ekAaQbpDWkT+RqJIRknqS45NNk7aUIJSKlPSV&#xA;X5XJljSWn5cKl3WX4JhMmLiZJJmQmfyaaJrVm0Kbr5wcnImc951kndKeQJ6unx2fi5/6oGmg2KFH&#xA;obaiJqKWowajdqPmpFakx6U4pammGqaLpv2nbqfgqFKoxKk3qamqHKqPqwKrdavprFys0K1Erbiu&#xA;La6hrxavi7AAsHWw6rFgsdayS7LCszizrrQltJy1E7WKtgG2ebbwt2i34LhZuNG5SrnCuju6tbsu&#xA;u6e8IbybvRW9j74KvoS+/796v/XAcMDswWfB48JfwtvDWMPUxFHEzsVLxcjGRsbDx0HHv8g9yLzJ&#xA;Osm5yjjKt8s2y7bMNcy1zTXNtc42zrbPN8+40DnQutE80b7SP9LB00TTxtRJ1MvVTtXR1lXW2Ndc&#xA;1+DYZNjo2WzZ8dp22vvbgNwF3IrdEN2W3hzeot8p36/gNuC94UThzOJT4tvjY+Pr5HPk/OWE5g3m&#xA;lucf56noMui86Ubp0Opb6uXrcOv77IbtEe2c7ijutO9A78zwWPDl8XLx//KM8xnzp/Q09ML1UPXe&#xA;9m32+/eK+Bn4qPk4+cf6V/rn+3f8B/yY/Sn9uv5L/tz/bf///+4ADkFkb2JlAGTAAAAAAf/bAIQA&#xA;BgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoKDBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8f&#xA;Hx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f&#xA;Hx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwERAAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQF&#xA;AwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAAAQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMB&#xA;AgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPBUtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdU&#xA;ZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eX&#xA;p7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZqbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUE&#xA;BQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEyobHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PS&#xA;NeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG&#xA;1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/a&#xA;AAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FVO5ura2haa5lSCFftSSMEUfMtQYQCeSsQ1n85fyy0hZPrPmC1lkjBrFasbliR+yPRDivbc&#xA;5fDS5JcgjiD5L/Mn8wdV88eY5dUvKxWsdY9PswfhhhrsPdm6u3c+1AN5p8AxxoNRNsUy9Ce2nnfz&#xA;TaeW7vy1BqMq6JekGazrVdm5EITugY/aC9e+VHDEy4q3CbSLLUP0SzlG92KuxV2KuxV2KuxV2Kux&#xA;V2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KpLr/nXyl5eUnWtXtbFgK+lLIvqkdfhiFZG+gZZDDOfI&#xA;Wgl53rP/ADlB+XVkWWwS91Rh9l4ohFGfmZjG4/4DMuHZ2Q86COMMN1L/AJy1v2qNM8uxRfyvc3DS&#xA;V+aokf8AxLMiPZg6yY8bE9b/AOclvzM1KEw20lppSnYvZwn1CP8AWnaanzWmXQ7PxjvKOMvONW17&#xA;W9Yn+satf3F/N2kuZXlI+XMmn0ZmRhGPIUxtAZJXYq7FXYq7FX6JZyje7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FUl83+cdB8paLLq+tT+jbp8Mca7ySyH7Mca/tMfw6mgyzFilM0&#xA;EE0+WfPf/OQnnfzHPLDpk7aHpJJEcFqxWdl8ZJxR6+ycR883WHQwhz3LWZF5fJJJI7SSMXdzVnYk&#xA;kk9SSczWK3FXYq7FXYq7FXYq7FXYq4kAVPTASr9Es5VvdirsVdirsVdirsVdirsVdirsVdirsVdi&#xA;rsVdirsVdirsVU7i4gtreW4ncRwQo0ksjbBUQVZj7ADCBaviX81vzFvfPHmeW+ZmTS7YtFpVqdgk&#xA;NftkfzyU5N93QZ0WmwDHGuvVpJtjFvo+oz6dcakkDfo+1KpPdN8MYkf7MYY/aduyjem/QE5cZAGu&#xA;qEFkldirsVdirsVdirsVdirTMFFTgJpVJbiRJklSnKNgyBlV1qpqKqwKsPYimVSNpfoxnNNzsVdi&#xA;rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirBfzx1Caw/KnzFPESGeBLc0/luZkgf8A4WQ5&#xA;k6ON5YolyfL35T/lpe+fPMX1MM0GlWgEup3iipRCfhRK7epJSi19zvSmbnU6gYo316NQFpr+ePmD&#xA;S21yHyh5ejS28ueWQbeKGL7L3Z/v5GPVmBHCrb1DHvkNHA8PHL6pJkXmeZjF2KuxV2KuxV2KuxVa&#xA;8ir7nwyJlSqDOWNTlRNpW4Ffo7nONzsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdiqWa95m8&#xA;v+X7T63rWoQWEG/Fp3CliOyL9pz7KDk4Y5SNRFqS8l8yf85WeS7Bmi0WxudYkXpK1LWE/JnDyffG&#xA;MzIdny/iNfax43kH5if85A+avOmky6LLZWlhpczo7JEHeY+mwZQ0jtxpyFdkGZeHSxxmxzYmVsc8&#xA;p/mb578p2c1roWofVLW4LPNGIYHJdlC8uTozVAG2+X5MEZm5C0Asba59V2eRy0jks7Makk7kknMg&#xA;SDFvJK7FXYq7FXYqtaRF6n6MBkAqm0xOy7DxyszSpZFXYFXKjN0H04QLV+jec23OxV2KuxV2KuxV&#xA;2KuxV2KuxV2KuxV2KuxV2Kqdzc21rbyXNzKkFvCpeWaRgqKq7lmY7ADCBewV88/mZ/zk8Y2l03yS&#xA;ooKo+szLWp6Vt4m2/wBk4/2PfNng0Fbz+TAy7nz3q+uatrF7Jfandy3l3L9ued2kc+1W7DsM2AoC&#xA;hsGCA64qrxxU3brlkYoVMmq1o1bqN/HAYgqpmFhupyBgeiVpeVepI+eRshXerJ4/qx4irvVk8ceI&#xA;qtLMepJwWrWBXYquWN27UHickIkqqrCo67nJiCFTJq/RLOUb3Yq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYqp3FxBbW8tzcSLDbwo0k0rkKqIgqzMT0AAqcIFq+Qfzr/Oq984X0ml6XI0Hlm3ekUYqrXTKd&#xA;pZRseP8AIh6dTv03em0wxCz9TVI28lZixqeuZBKHAEmg64gKrxxhd+p8ctjGkL8krsVdirsVcQD1&#xA;xVYYkPanyyJiFWmAdjkeBNtfVz/NjwLbYgHc4eBbXrGi9BkhEBC7CrsVdir9Es5RvdirEde/Nv8A&#xA;LfQp2t9S162S4Q0khh53LqfBlgWUqfnl8NLklyCDIJNbf85DflLO3E6y0J2p6ltcgGvuIyB9OWHQ&#xA;5R0RxBmGh+b/ACtry10bVrW/IFWjglR3Uf5SA8l+kZRPFKPMUm03ytLsVdirsVdirsVdirsVdir5&#xA;2/5yf/M54AvkjTJeLOqzay6nfifiig+kUdv9j75s9Dh/jPwYSPR82Ekmp65sWDlUsaDEC1RCIFG3&#xA;XucuApC7CrsVdirsVdirsVdirsVdirsVdirsVdirsVfolnKN75x/Pr82NQudUu/KejXL2ul2P7vV&#xA;7uEkSTzEfFbIR0Ra0bxNQdhvstNh4QJVcjy8vNqnLoGNaX+SHmFPLMvmPXLqHy1pkUXqrE8RuLxg&#xA;dkDRkxhWckBQXrU7gZLJqIA0bmfkGPBtZZRp/wDzjXreoaFaXkuuiwv5Y/UexltRKBy3VXk5rRuP&#xA;2qJsdsojqIAm4/akYmJeY/yM/Mby6/12KwXU4oTzS90d2E6EdG9Kiycv9VT88yseoidhKvKXJTAh&#xA;MvJH59+c/L7JBrRk1/SVPB1n+G/hCmjUc/3nHuHr8xleTBCRr6JfYoyd76M8p+cfL3mvSl1PQ7tb&#xA;m3J4yJ9mSJ/5JUO6t+vqNswMuKUDUm0G06ytLsVdirsVdirsVSDz75qh8qeTtV8wSAMbGAtChqQ0&#xA;zkRwqadmkdQfbLMUOOQCCXwXquqX+q6lc6lqExuL28kaa4malWdzUmg2HyGb4AAUGpDohY+3c5MR&#xA;tUQqhRQZaBSG8KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Kv0I1XU7PStNudSvX9O1tI2lmfvx&#xA;UV2HcnsM5QBuJp8ceUll1r8ytNWJoLjUJr57oQXfP0HuatMTMygkjkOgHtm3PGMZlXT7GiNkvdr6&#xA;0/MDzR5q/Qd5faZ6egehqNz6NvM1t9aepghkV5A7/D+8608a5rNgGZsmmVF/zatKM0ejarGvWOM3&#xA;FpM3sC3qx/qyOzL1KbfmhZacrJ5o0y80C4VWZTMomt5GUE8IriLkjMabVpjw9y8fexBfya0vzr5T&#xA;XWtTZ7HzRq0kmpRahESfSFyeUULISA8YTjtsRvQ5kY9QYGuce5AjYeMK3nj8r/ObAUs9YgoXQVa0&#xA;1C3rXb7PJWp7EHwYbZ3plDvx/bFhvEvqnyF540jzn5eh1jTjwJ/d3dqxq8E6j4o2/WD3G+azNhOO&#xA;VFuBtkWVJdirsVdirsVeCf8AOXGuyQeXdD0RNhf3Ul1KQd+NqgUKR4Mbiv8Asc2GgjuSwk+YI4y2&#xA;52XNtGNsEQAAKDpliHYVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir7d8+L+mNa0Dyn9q2vZ&#xA;mvtVXqDa2VHEbf5MkpUfRnLR72yW5AfMfnuy1DyB+cFzc2yUNrfrqenjcK8Mj+siV/l3MbfI5v8A&#xA;DIZcW/dTE7F9O/lRS78uT6+5rc6/eXN9Ka8iq+qYo46/5CRgU7ZoJ86ZQ5WzTIs2FfmGTq13o/k6&#xA;M1GrzifUqH7NjaESSA03HqOFVclHvYT32Q11our+R3fUfLayX3lupfUPLxJd4VJq0tkTvt1MZND2&#xA;9jd80Vw8uSn+ZGg+VvP35dy6klxGEtoJL7S9VA+KF41LMD+0FPHi6/xAy3BlOOXl1DI0Q8G/KLzt&#xA;L5S89w83ZdK1SVdP1eJvhCSE0imI7FGO/wDk8szZYzPHwnmBcfd3NWM0+u81Te7FXYq7FXYq+Wf+&#xA;csfWfzto6Mf9HTTQyLX9tp5Q+3yVc3HZ0bife1zeJ5s2DsVdirsVdirsVdirsVdirsVdirsVdirs&#xA;VdirsVdirsVfZWuJ5pn/ADXkj0KS0tp10SJGubxXk4QvcuWaKNacn5qo3NNs5cVTM3xbMc/Nb8od&#xA;V1LQZteuNYfWNd0yP1P38MMURtkq8qRpGtVYCrL8R8O9cuwZq23AKJQ25vHvy88+xaVrp07WtR1G&#xA;w0SWQoLjTbmSBrdy3940a8kkQ/tjjXvv0OfPR8UAY/VTGL6Ug0fzzaW0d55d8zRa5ZOokht9URHE&#xA;iEVBW7t+LGo6VWmao1yIbKPQpH+X3nCw1jzlqGq6xSw1XUY0stFt3JaFra3YiYW9wVVJeVxUkDww&#xA;yFBjGVl6plba8Z85adBpvnCDynaXiW3l/wA1zw3OqWIB/cMsoB9OmyLdsoX5jwywcraZCjXe8z/5&#xA;yI0W10z8xbz6sqxpqenQX7IgoFkRmh2A6V9CubLSyuET3Sr5rkG76b8n6jNqflHRNSnJM17p9rcS&#xA;k9S0sKu34tmtyxqZHcW0JvlaXYq7FWH+e/zX8meSk46tdmS/ZeUWm2wElww7ErUKg8C7CvauZGHT&#xA;TycuSCafK35s/ma/n/Wra/8A0eunxWcJghTn6sjKWLVd+KDqdgBt75utNp/CFXbVI2wbMlDsVdir&#xA;sVdirsVdirsVdirsVdirsVdirsVdirsVdirsVfbvnOQaH5o0PzWTxsqtpOruTQLBcsGhkYnoscy/&#xA;F885YbimyWxtEat+YX5eyWlxZTa/ZEXETxMUlWQUdSp3TkO+IiVMh3vAdY/Lzyn5n8pabqehapY2&#xA;Pm2K3EV/pE08cK3LQ1QMvMgLMyqD/K3Xbc5sdPqzD0y+lgKIYv5S/MPz95MS/wDL6zSQWTRSpPY3&#xA;Skm3Z1I9SGtCjb1FPhPWhzJ1WKEwJDmeSDIh7d5U8/8A5NeafKNl5XvLhdPNtFHFHb35EEiSqKer&#xA;FPX0+ZapBDVPcb0zX5dJkgbq/czFEUn1l57l8qRXGleaLg33oQ+voWqxfEdRhBCLFtUG4VmCnx+1&#xA;7nGMbXirYoLVdDsbbyLrur+cryCw1zXUNy8srhfqzwjlaW8O/I+kQtQu5O2+ShEyNRFrw7bvnj8x&#xA;fOl/5r1qfWL5BE6WsGnwIDUUReUjDp9p2Z/atM2mngAIxH9YtZlb1D8tP+cltKsdMsNC8y2BtYLK&#xA;GK1g1CzBdBHCgjUywklxRV3KE1/lGQ1HZ5JMonm2Cb3rQvMWh6/YrfaNfQ39q23qQsGof5WHVW9m&#xA;Fc1k8comiKbLTHIK89/Oj8z4/I3lutoytr2ocotNiIDBKfbnYH9mOuw7tTtXMrSafxJb/SGMjT42&#xA;vb27vrua8vJnuLu4cyTzyMWd3Y1LMx3JOdAAAKDUoYVdirsVdirsVdirsVdirsVdirsVdirsVdir&#xA;sVdirsVdirsVfoD5i0W31vQ77SbgD0r2Foqn9liPhb5q1GzlQabiLCRflqLK+8laZcSWUEVyIzBd&#xA;KsaKTLbO0LlgB1Zo6/ThlzYw5Jb+XugaFqXl+/iv9OtbuKPVL+OITQxyUQXDEAFgehO2GR3RACmD&#xA;eZPyh0nzN5m1fTvLqLZWej28aSJI8rwveTEyGJDyJjUJQtx6N2y3HmMKLEws7PM9U/JiWK11GeDU&#xA;4tPutJHPUtJ1XlFNGpNEaGWNWS4VzTiwVa+HSuyh2gP4h8lU7P8AKj8zrO1tZ7XQrt7ecLMZo2Xm&#xA;Aw2KoG9RCOu6g5XPPDJZJojkGPCTuln/ACrDz3f6TL5ku0VdGUMRql3cIefFynFV5PMWL/CBx65k&#xA;DW4wPPuT0t9DflJ+T2l6NpEWp69ard6zdw8TBcorpbxOPscGB/eOv2yen2R35aiec71szhCkH52/&#xA;5xn8n6zzudAc6FfGp9NAZLVyd94yap/sDQfy5kYe0Jx2l6gkweH635C/NL8tNQ/SSJcWqRfZ1fTn&#xA;Z4CtejsoHEH+WRRXwzZQzYswr7Cxohnvkn/nKbUbf07XzfZC8iAAOo2YVJvm8JIjb/Ylfkcxc3Zw&#xA;O8CkTeZfmv53bzl52vtWR2Onofq+mI1RxtoqhTQ7jmauR4tmZpsPhwA6sSbYfmQh2KuxV2KuxV2K&#xA;uxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Kv0H1fU7bStLu9SujS3s4nml8aIpag9z0GcoA3E08u&#xA;02x85+XPy6XXD5hSxjSCTUZNOexilrJcsZVi9QurcneQL7Vyw0S1gEC7VtG1Lzx5C8nxTarpVtf6&#xA;XHG1zcTW85iuoZLhjK3rpKOLn1JOPwHE0SoJiE7/ACr1DR18nzXr3scl88k1/wCYHPwNFcSkvIJU&#xA;ajLwVeNTtRdsEubKB2Y1rOkaz5w5ef7e3RI9J4y+XNNliUveQQP6jvPUFv3oB9Je301JBrZgQTu9&#xA;Cbzx5bi8s2vmK7u0ttPu4lkiLmrksK+mqLVmcHYhRkOE3TZxCrYX5J8mahqd6+o6tHLa+WoL+fUN&#xA;B0OdAknOZyyyzpU0CV/dofn85yLCMb9z1LK212KtMqspVgGVhRlO4IPY4q8c/OL8mfIMnlrWPMlv&#xA;anS9SsraW5DWdEildFJVZIT8HxN1K0PzzYaXV5OIR5gsJRD5RzdtbsVdirsVdirsVdirsVdirsVd&#xA;iqvaWV5eTiCzgkuZ2+zFCjO5+SqCcBIHNWU2X5Q/mbeqGh8t3yhunrR+h/ye4ZQdVjH8QTwlMovy&#xA;A/N2VOS+X2A/yrm0Q/c0wOROtxd/3p4Si/8AoXH82P8Aq2Q/9JVv/wA15H8/i714SiF/5xo/NIqC&#xA;be0Ukbg3K1H3A4P5Qxea8Bcf+caPzRAJ+r2h9hcr/TH+UMXmvAWG+c/IHmbybPbW+vwR2812jSQx&#xA;pLHKeKEAk8C1NztXL8WeOT6UEUxzLkJ5N5G86Q2kV5JoOoC0nRZYbj6tMY2RxVSHC8dxlQzQurCa&#xA;SaSKWJykqMjjqrAg/ccstD7N/N3WtLgXQ9E1S4W20zVLz1dSkcEqbWzpK0ZCgn945QZzEAzyHolu&#xA;u+ePJ3mrzHoWiwarbjRLeU6hqMszehHI8G1vbj1eHKrtyZfAYREgIMgTSe+cnTXvMOheVYiJLSRh&#xA;q+rcTVTaWzD0UNOqyzEfdkRsLZS3NJB+a3ltNY8wWGm6DAqeYr2CafUXV2iSWxgXaO5CFeQll4or&#xA;H78lE7MZizsnv/KyLMeVLO60yyL6zcy/o600L7Lx3kfwvE/TikVKsdvhp0rkeHdlx7N+SPyv03Q1&#xA;hvdS46hrEfNomPI29oJHMjR2kbk8F5Mfi6/KuMpWsYUzjIs3Yq7FXYqwz85YXm/K7zIifaFmzn5I&#xA;wdvwXMjSn97H3olyfEGdG0uxVG6VomsavcPb6VY3F/cRoZXitonlcIvViqAmm+RlMR5mlRnlzUNM&#xA;0XXo7jXNGXVraBiJ9NneSD4ge5Teq06MCPEZHJEyj6TSQ+nfLnm78jfzA0mDQZLGztCtRb6PdRJa&#xA;ujNsfqzxkLVv+K35eIzTZMWbEeKz72wEFKNe/wCcUfLVy7SaJq9zp3LcQzot1GPZSDC4HzY5ZDtK&#xA;Q+oWgwYtdf8AOJvmZVb6prllKw+wJUliB+ZUS0y4dpx6go4EB/0Kn+Yf/Vx0j/kdc/8AZNk/5Sx9&#xA;x/HxXgKIt/8AnFDzq1PrGr6bHv8AF6ZnkoPasUeA9pQ7ivAnmnf84kpUNqXmQkftRW1rTt2keQ/8&#xA;QyqXafdFPAzjQP8AnHH8sdKKvPaTatMu4e+lJWv/ABjiESEezA5jT1+SXWkiIeiaZo+kaVb/AFbS&#xA;7KCxtx/uq2jSJdvZABmLKZluTbJF5FXYq7FXYqgtT1vRdKj9TVL+2sIyKh7maOFafNyuSjAy5C1f&#xA;GH5x+b181fmBqWoQyCWwgYWensDyUwQVUMpH7Lvycf62dDpcXBjA6tMjZSv8vPL8XmHzvoujzf7z&#xA;3d1GtwOlYlPOQD3KKRk88+CBKgbvu9VVVCqAqqKKo2AA7DOZblssMMy8JUWRa14uAwr8jiCrD/NP&#xA;C08/+UdQmH+jzfXNPMh6LNNGrwj5uY2XJDkWEuYQvlfStJ1rUvOGpavbQXiS6o9lxuESRFhsIljW&#xA;nMEL8TNXCTVLEXbFPKmka1pNne+efKsCtp1xPKE8vvUmXTIWIRoZHq6Scg7hehr8gZE9CwiCNwy/&#xA;8t5hr11q/nVwQuqy/VdMR6co7K0JRagE8WeTkzCuQltszhvuhvK2jadf/mb5k8x28Y9CxZbCFhuj&#xA;XhjX63Io6KygKhp13wk7UiI9RL0LINjsVa5Ly41HICtO9MU0hNYn1K30y4n0y3S7vok5wWsj+mJC&#xA;pqU5UNCRUD3yMyQNty3aWGOWQDIeGB5nnXnX3qfl7XLLXtFtNXsuQt7xOaK44spBKsrDxVgRtt4Y&#xA;MeQTiJDqz1uknps0sU/qifx82K+ZP0h5i8zXfk+4ujpelPYNJxQAy6gs6NFIFc/CscJYc1HxH5ZX&#xA;HLIZgOQjv73Yw0+KGhObh8Sc5GHlj7tuspdDyHvfGOp6beaZqV1p17GYruzleC4jPZ42KsPvGdhG&#xA;QIsPOs5/K/8AJjzF54nW6IOn6AjUm1KRa8+JoUgU05tXYn7K9zXY42o1cce3OTIRt9Z+T/JXlzyj&#xA;pS6bodqsEWxmmPxTTOP25X6s2/yHQADNHlzSyG5NgFIHzl+WHkrzfGf0zpyNdU+C/h/dXK9h+8X7&#xA;QHg9R7ZPFqJ4+RUi3gXnb/nGHzPpfO68s3A1qzFT9WfjDdKOtACeEm3gQfBc2eHtGJ2lswMGPeV/&#xA;zk/MvyLd/o28eW4trc8ZdJ1RX5IPBWakse3QV4+2W5NLjyCx8wgSIe8eSP8AnIbyJ5j4W99KdD1J&#xA;qAw3jAQsx/33cbJ/wfE+2a3NoZw5bhmJB6erKyhlIZWFVYbgg9xmEybxV2KuxV2KuxV2KpR5o82a&#xA;B5X0mTVNbu1tLVPhWu7yPSoSNB8TsfAfPplmPFKZqIQS+afP3/OSvmnWXltPLYOiaYSVE4o15Ivi&#xA;X3EVfBNx/Mc2+Hs+Md5bn7GBm8gu7y7vLh7m8nkubiQ1kmmdpHY+JZiSczwAOTBRwqmNlp3mCMx6&#xA;hY212hhIlhu4EkHAr8QdZFG1OtQcgZR5Gleo+Uf+clvPGiFbTXYk1y2j+Emb9zdLTanqqCG/2aE+&#xA;+YeXs+Et47MhMvob8vPzI8vee9KkvtJLxS27CO7sp+ImiYiqkhSwKtQ8W779wRmqz6eWM0WwG0z8&#xA;1+XIPMOiTadJIYJarLaXS/ahuIzyilWlPst+GUg0shYeSLretaTo2teTtYAsvM3mDVGEdyF4WzQX&#xA;3Bbi5jkNF4/C/eoLDbbLK6tVkCjzZl5pvYGtbbyB5emWJnt0i1K7BHGx01VCszt9n1JE+FB7122y&#xA;I7yykegY9peoXWl+YNZ8s/l6keoWV7HBLb3KPztNNnKmKdpJN1YlUDhQTVvuwnvLEGjQemeVvLtr&#xA;5e0S30u3Yy+kC01w/wBuWZzykkb3Zj/DIE22xFBNcCXYqwvzZNrXl/XJ/N8NrHqGkQ6aLe+txKIZ&#xA;4hDK8zTJyXhIOLU48gdtsxcxlCXHVxp6Ds2GHU4RpTIwynJcTVxNgRo1uPfReC+YPzw/MDVp7j0r&#xA;0afZTq8Ys7dI6KjjiR6jKZC1P2q9elM02TX5ZHnQfSdF7JaHCBceOYo8RJ5jyuvh87e4/kv5wg8x&#xA;eToYlt0tJ9J42UsEVRHxRB6brWp+Jeu/UHNvocwnj7q2fO/arsyWl1ZNmUcnqBPPnuPx0RH5kss3&#xA;6JstMBbzablZtDZNjDwIE8sx3pb+mSsgP2qgdeh1W9AfXe347mrsIcPiTyf4tw1k87+kR/p3vHu5&#xA;qmv/AJRfl95h1hdZ1jSY7jUqL60qvLEspUUHqIjqrU9+2xqM2WPVZIRoHZ54gWy22tre1t4ra2iS&#xA;C3hURwwxqEREUUVVVaAADoBlJN7lKpgV2KuxVI/NHknyr5ptfq+u6bDegArHMw4zRg/77lWjp9By&#xA;zHmlA+koIt4R52/5xYv4TJd+T70XUXUabeEJKPZJtkb25Bfmc2eHtEcphgYPP9F88fmn+Wd/+jma&#xA;4s44zV9Jv0Z7dgO6K3QH+aJhXxzKnhxZhf2hFkPb/JH/ADkz5R1gR2vmGNtDvm2MxJktGP8AxkA5&#xA;JX/KWg/mzXZuz5x3juGQm9ftLu0vLaO6tJo7i2mHKKeJg6Op7qykgj5ZgEEbFmq4FdirsVY3598+&#xA;aH5K0GTVtUfkTVLS0Qj1J5aVCJ4f5Tdhl2HDLJKggmnybfXP5g/m55qlnSJrmSMErGG4WllAd93Y&#xA;8UXbcndvc5vAMeCP4ste5SDzJpnl/SZP0fYX/wCl72M0ur6EcLRWH2o4K/HLQ/7sPEHsp+1lmOUp&#xA;bkUPtQWZ/lz+QXmzzasV/eg6Poj0ZbmdCZpVPeGI0JB/mag8K5j59bGGw3KRG30Z5P8Ayc8geVUR&#xA;rLTUur1QK6heATzEjuvIcE/2CjNTl1WSfM7NgiAzbMdKX6v5d0DWYvS1fTba/jpQLcwpLT5cgafR&#xA;k45JR5GlpjOgflD5P8ueZ18waAtxpk3pvFcWUMpa1mSQdHSTm2zUYcWAqOmWz1U5x4ZboEWbZjpQ&#xA;mp6Rpeq2ptdStIby3Jr6U6LItfEBgaH3xBQRbHE/KT8uUk9QaHDyrWhaRlr/AKpcr+GS4yx4Ayex&#xA;0+wsLdbaxtorW3T7MMKLGg+SqAMizAV8VQ+oafZ6jZTWN5H6trOvCWOrLVfCqkEfQcjKIkKLbgzT&#xA;xTE4GpDkxDQNE03y/wDmBc6dpcTW9jeaTHcGDm7qZYbl0Z6yMzV4yqMxseMQykDkY/pd5rdXk1Oh&#xA;jkyHinHKRdAbGINbDyKf+cYNOn8qavDqU/1WwktJluLnqY0KEFwO5HYd8uzgGBvlTrey55I6nGcY&#xA;4picaHfvyfFbUBNDUV2PTOVffwmOjeYtf0Ocy6Tf3FhISC4hdkVqdOaj4W6/tDJwyyh9JpxdVocG&#xA;oFZYRmPMfd3fB7v+Rnni01/U9T/S7GTzZcKrm8kpSS1joBFCoCiMIx5Mo615fLcaDOJk8X1/ofN/&#xA;a7smWmxw8LbTR/h7pHrLvvkD0qvf2TNo8G7FXYq7FXYq7FXYql+t+X9D12yay1ixhv7Vq/up0DgE&#xA;inJSd1b3G+ThOUTYNLTw/wA7f84sWU3qXXlC+NtJuw029JeMnwjmFXX25hvnmxw9onlMMDB5RHdf&#xA;mv8AlXqXD/S9HLNvE4EtnN8vtwSbDqNx7ZnVizDv+9juHsXkT/nKDStQlgsPNVn+jrmQqg1C3q9s&#xA;WO1XRjzjH0t9Ga/N2cRvE2yE3uua1mg9Z1jTtG0q61XUphBY2cbSzyt2VfAdyegA6nbJQiZGhzV8&#xA;k3tx5m/Ofz5Pcs4sdGskLvLKwFvp9ghqXcnYuwFT/MfBRtvAI6eFcyftLVzKzzH5vbUY4/y+/Liz&#xA;lh8vtJ6ZEQLXepzbBpp2G/A8ahdhTrQUVTjxV+8yH1fcpPQPXPyp/wCcd9M0EQ6v5pWPUNZFHist&#xA;ntrduor2lkHifhHauxzA1OuMto7BkIvac17N2KuxV2KuxV2KuxV2KuxV2KuxVJ9c8p6PrU8N1dev&#xA;De26NHBeWlxNbTKjlWZeULJUVQGjVyrJhjI2efyc/SdpZcETGPCYSNmMoxkNv6wPf0eZfnXoOoaN&#xA;5Fnkt9c1O7tZ54obizu5Y5o+BYuDyMfq7Mo/bzA12Mxx7SJHm9d7KayGfWASxYoyESRKIIPdyvh5&#xA;eT59s7qa0u4LuEgTW8iyxkioDIwYbfMZpYmjb6dlxicTE8pCvmzX83Ba32r6f5psU42fmOzjuiB0&#xA;S5i/czx/NGQV9zmXrKMhMcpB5/2aMseKemn9WCZj74n1RPxtI/IGrS6T510W/jbj6d3EslO8cjCO&#xA;QfSjEZTp58OSJ83Y9s6YZtJlgesD8xuPtfZ2dS+BuxV2KuxV2KuxV2KuxV2Koe/06w1G0ks7+2iu&#xA;7SYcZbedFkjYe6sCDhjIg2FeY3//ADjV+W91rUWoxJc2kCuJJtNhkH1d6GvH41d1U9wG6dKZmx7Q&#xA;yAUx4A9WzBZPHvzs0Pz953vrXyj5dszFo8HG41XU7hvRt2lO8UYb7UgQfEwRW3I6UzP0k4YxxyO/&#xA;QMZWUW35GiLyZaeTtO1T9HaXIRP5gvIo+V1fzD9ipIWOIHoPi6AeJYfnPWZkWenkvCynyL+Vnk7y&#xA;Skh0a1Y3cwCzX1w3qTso/Z5UVVHiFUVyjNqZ5OaQKZblCXYq7FXYq7FXYq7FXYq7FXYq7FXYqkXn&#xA;HW77SdLhbTlibUb27trGzE4ZoudxKqFnVGRiFTk2zDplOfIYjbmSA7HsvSQzZCMl+HCEpSrnURe1&#xA;3zNBjvmnyD5t8x+X7zTtT8xrKJo+Udpb2cUEJmQ84wzu08vHmBUqw2yjLp5ziQZfY7Xs7tnS6XPH&#xA;Jjw1R5mZkaOx2HDHl3gvle9s7qxu5rO7iaC6t3aOaFxRldTQgjOelEg0X2XFljkiJxNxkLBRjaxE&#xA;3l5dJezjaZLk3Ed+WYyqhTiYVFeIQt8R23PyyfH6eGuvNoGmIz+KJGjGuHpd/V330TP8uPLd95h8&#xA;46dYWjGIpILia44hxFHCQ5cq3wncACu1SMnpsRnMAOH25roaXSTnLfagOVk7U+l7ryv5+W1ma185&#xA;3D3IRjAkljYBDJQ8Q1Iq0r1zfHFkraf2B8lx9oaIyHFpo8N7+vJy/wBMnnlXWxrnl6x1Tj6ctxGP&#xA;rEXQpOhKTIf9SRWXLsOTjiC67tHSfl888fMROx7wd4n4ik1yxwlscsUhcRurlG4PxIPFhvQ06HfB&#xA;bIxI5jmlWu+Yf0bdafYW9ubzU9TlKW1sG9NRHHQzzSPRuKRI3gSSQB1yvJk4SANyXM0ei8WM5yPD&#xA;jxjc89z9MQO+R+yym+WuC7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FWL6&#xA;zZa1qnmzR4zYGPRNJna9lvnljIml+rvHEqRKTIODy7lgOmY84ylMbekb/Y7jS5cOHS5Dx3lyR4eG&#xA;jsOIE2eW4HRNvMGv2Ghacb685spdYoIIVMk000hokUSDdnY9BlmTIICy4Wi0c9Rk4IV3knYADmSe&#xA;gDzvWvyfl87Pca55hlGkaxdKgtbW0VJFt41+yLlqKZ5CPtEMoHQbZhT0Xi+qXpl+Ofe9Tpfacdng&#xA;YcA8XFG7MrHEf6P8yPdsb5liCf8AOMmuG54ya1arbf79WORpKV/32So6f5eY38lSv6g7w+32Hh2x&#xA;S4veK+f7HrfkL8udA8mWTxaeGmvJ6fWr6WnqPToopsqDso+mubLT6aOIbc3iO2O3M+vmDPaI5RHI&#xA;frPmyrMh0yQaR5b1DStZvp7W/j/Q19cSXb6a1uTIk0qqJCk4lACtIpfj6fUnKYYjGRIPpO9Oz1Ou&#xA;hmxRjKB8WERHj4tiAdrjw8wNr4uig/5g6Ba+XrvW9SlFnBZXNxZzw8hJKJYJ3gVeK78pPT5AeB8N&#xA;8j+ZiImR2o19rYOxc888cOMcRnGMgeQqURL5C6tg/lD8z/JmtfmS81h6+mDULRbb0p1Ea3d56tVZ&#xA;xG8ickjXijGhPIr4ZiYdVjnl22sfMvRdp+z+r0/Z4E+HJwT4tt+CFdLANE7kchV970HzlpmlXOkP&#xA;e31wbCTSw13a6qlPUtXRal1r9oEbMh2YbZm54gxs7V17nmOy8+WGUQgOMZPSYdJA9P1HpzRXli91&#xA;S+8v2F5qtuLXUJ4Ve4gG1CenwkkqSKErX4emSxSJiDLm09oYsePPOGI8UAdj+Ofv680zyxw3Yq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqkHnvSbnUvLN0ln/x0rThe6aw6i5t&#xA;WEsYH+sV4/TlOogZQNcxuPeHZ9j6mOLURM/7uXpl/Vlsflz+CZaHq1trGj2WqWxrBewpOg8A6g0P&#xA;uOhyeOYlEEdXE1emlgyyxy5wkR8kbk3HdiqldzyQWss0cD3LxqWW3i4eo5ArxXmyLU+7DATQZ4oC&#xA;UgCREHqbofKz9iUaJ5kuL/U7rTL7TZdLvbeGK5WGaSGUvDMzoGBhaRfhaIgivhlWPKSSCKLnavQR&#xA;x445ITGSEiY2BIURR/iAPIvOf+chPJltN5cTXtPsoI7m0n56jOiIkrxS/DyZgAXo5HXxzC7SwAw4&#xA;gOXN6r2K7UlHUeBOUjGUfSLJAI+7Z88W881vPHcQsUmhZZI3HVWU1B+g5pAaNvqM4CUTE8i9G078&#xA;7NfvbqxtfNrfpLRYrlLi7jijjilk9OpQNwCo6K/FylBypSuZ0ddIkCe8beVz+ymDHGctL+7ymJAs&#xA;kgXz57gkWL6XyfTljfWl/ZQXtnKs1rcossEq9GRxVSPozfxkCLHJ8hzYZY5mExUomiPNXwtbsVdi&#xA;rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVUL+yivrKezlZ1iuI2ikaJjG4VxQ8&#xA;XWhU+4wSjYpsw5TjmJirib33HyUbeDSNC0mOCP0rDTLJAicmCRxoNhVmP4k5ECMI9wDZOeXUZSTc&#xA;8kz7ySizIgjMlaoBy5Dfala7ZNoo3SV+V9cfXdLGqCAQ2dxIx088+TyW4oEkcUHBnIJ4b0FK71Ar&#xA;xZOMX06Ob2hpBp8nh3coj1eUuoHfXf16bJtljgpDrGjau2u2ms6RLbpPFbTWdzFch+EkcjxyIQU3&#xA;BRkb/gspnCXEJR7qdlptViGGWHKJUZCQMa2IBB599j5PL/z6u/Osfk+3i1RbGOxnvY1drJ5mdmWO&#xA;R1VxIqjj8NfmBmv7QOTg3qr6PYex2PSHVE4+MzED9Qj3gbUef63kv5f6vo2n6+I9ct459I1GJ7G9&#xA;d1DNCk44+tGxBKsh3qN6VzW6ecYy9X0nZ7btrTZcuC8JIywIlH+kY/wnvB+9KNd0qTSNZvdMkdZW&#xA;s5nh9VCCrhTQOtK7MN8ryQ4ZEdznaTUDPijkAriANd3k97/IHzjp0Hkq4stX1CC0TT7plt3uZkiH&#xA;pSqJOI5kdH5n6c3HZ2YDHUjVF819suy8ktYJ4oSkZx34QTuNunlT1XSNf0TWYpJtJvoL6KF/Tle3&#xA;dZFVqVoSpPY5sYZIy+k28ZqdHmwEDLGUCd9xSPybjOxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2K&#xA;uxV2KuxV2KuxV2Kpf5hvr6w0W8vbGD6zdW8Zkjt+LOX47lQqfESVrSnfIZJERJHNytFihkzRhM8M&#xA;ZGr22+ezzbz7+fOg6S36N0yy/TE0sZ+siflDFGGH2HSRObEjqpA+eYGo7QjHYDies7G9js+YeJkl&#xA;4QB2rcnzBBoe/dPvyZ806Xrfku0t7P1FuNKRLW8ilIJD8a8lp/utt+I7Up2y7Q5RPGAOjrfans/J&#xA;p9XKU64chMokd36x17+fVH+XI+Pm/W10piuhRgLeRGhi/SjHnL9X/lpGR6o6Fz4hqzxfXLh+n9Pl&#xA;+lxtcb0uLxf74/Sevh8hxd+/0/0R3UyzMl0iG1S+XT9Mu791LpaQSTsgNCREhcivvTIzlwgnubtP&#xA;hOXJGA/ikB8zTBfMPlXzX578tG31K8stNtLqNLi1tLeJ53ElOcfqXEhSgFQG4R+O+YmTDPNCiQB+&#xA;Or0ei7R03Z2o4scZ5JRJiTIiO3I1EX8Lk+YtZ0XVNF1KbTdTt2tryBuMkbj7mU9GU9iNjmgnAxNH&#xA;m+vaXVY9RjGTGeKJa1HVr7Uja/WmV/qdvHaW4VFSkUVeAPADkd/tHfBKZlV9NlwaaGLi4f4pGR3P&#xA;M8+f3cn0t+S35fxaL5TjudXsIv0rfSG5/fRqZYomCiOMlhyX7PIjsTm+0Om4IXIbl8l9qu2jqNUY&#xA;4pnw4Dh2OxPU/o+DItdtrvR9ftvMWn2st1BOgsdZs7ZOcjRirW86IKVaJyVb/Ib/ACcvyAxkJAX0&#xA;P6HV6Occ+CWDJIRIPFCUthf8USe6Q3H9IebJoJTLBHKUaIyKG9OQUdeQrxYAncd8vBt1E48JIu6X&#xA;4WLsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdiqF1KLUpbRk025itbo04zzwtcIB3/d&#xA;rJDU/wCyyMga22LdgljEryAyj3A8P21L7kt8q6jqNzHqNpqUiS3+mXj2sssaekHQok8L8KtSsUy9&#xA;8rwyJsHmC5faODHAwnjBEMkBIb3W5iRf9aJfPX/OQthZ235hSXEMxknvbaGa6ip/dsq+ioB/ykiB&#xA;zS9pRAy33h9Q9is056ERIoQlIA9/8X3lhPpeZtAis71HudOj1GMXFnPFI0fqIjFQwKNXY1679++Y&#xA;lThR3FvQcWn1JlAiMzA1IEXR+L3H8gfzGOowN5W1EKL2BXuLO5GxnVnLyiTxkDNy5ftCtdxU7fs7&#xA;U8XoPN889suw/CP5nH9BoSH83ahXl0rp08uzZtHgUk86W+q3flnULDS7f6xeX8EtohMixLF60bJ6&#xA;rMeyV6DfKs4kYEDmXYdlTxQ1EJ5DwxhIS5XdEGvijL66Gj6FPdGJ7gafbNJ6MQ+NxCleKjxPHJSP&#xA;DG+4NGHH4+YRvh45VZ6WerF9N8j6P5jsf0z5qgg1a/1RI5kNS0Vtbkc4oLZhxIVQ/wATjdzue2Y8&#xA;cEZjinuT9nudxn7Wy6WfhaYyxwxkjzlLkZS89th/CNkbon5W+QNEu1vNO0aGO5Qho5ZWknZGHRk9&#xA;ZpOJHiMnj0mKBsBx9X7Q67UR4MmQmJ6Co37+EC2VZkOmdirsVdirsVdirsVdirsVdirsVdirsVdi&#xA;rsVdirsVdirsVdirsVdirsVSXUvJ2gajdyXdxFMtzNx9aS3urm2L8BxXl6EkdaDbfKpYIyNn7yHY&#xA;YO1M+KIjEjhHK4xlV/1ol8zfnPpumWHniePTbgXFo8MTA/WXu3RgvBkkkkeVw1VrRj0IzQa6IGTb&#xA;l77fXPZbPkyaMHIOGQJ/hEb62AAB8ktstWj1TydN5fvpIkl0pnv9IuZ5OHFGH+kWqbNyMp4si7fE&#xA;DvkIz4ocJ6bj9IcvLpjh1YzwBIyVCYAv+rM93DuCe5U/KqbUYfzB0aTToDc3ayvwgDiPkvpOJBzb&#xA;YDhWuHSEjKK5sPaKOOWhyDIeGNDer6itve+mpPMfmKxurN9a0u2s9Lu51tTPBdPcSRSS1ETSqYIU&#xA;CO/FNmNCwzfeLIEcQAB8/wBj5FHQ4MkZDDOUskY8VGAiCBzr1SNgWeQ2DJ8yHUO67HFWM+R7S70+&#xA;LVNMMMkel2V9ImkNMpQmBwJCiKdzHHI7JG3daU6Zj4ARY6A7O37WyRynHksHJOA4639XKz5kAGQ6&#xA;Fk2ZDqHYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqxrzta298+g6b&#xA;cx+va3uphLi3J+CSOO0uJyrj9pf3Qqp65RnAPCD1l+gu37KySxjLkialDFse4mcI7dx35pR+Z3lD&#xA;yfd+TzaXcC2XoPTSBZRKJfrUmyRQxLxDmWgBX6dqVFWqwwMKO3dXe5vYHaeqhquKJ47+viO3COZk&#xA;enD3/fdHis//ADj/APmRE6LHa284ZQzMlxGApIHwtzKGo9qj3zVHs7L3PoEPbPs8g3KQ/wA0/ot6&#xA;r+Un5Ot5SnfV9XljuNZdDHCkVTHAjfaoxA5O3StNh882Oj0Xh+qX1PGe0vtP+dAxYgY4gbN85H9T&#xA;0fVNMs9U065069T1LW6jaKZOh4sKbHsR1B7ZnTiJAg8i8pp888OSOSBqUTYa0uwksLKO2kvJ75k/&#xA;4+boo0rf6xjSNT92MI0Ku06jMMkzIRjC+kbr7SUXkmh2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Ku&#xA;xV2KuxV2KuxV2KuxV2KuxV2KuxV2KoaTS9Nlv4dRltYnv7dWSC6ZFMsat9oI5FVrXemRMBd1u3R1&#xA;GQQOMSIhLmL2PvCQ+dYxZS6Z5oI5x6FJI13GRy/0S4URzyqP54hR69eIYftZTnFVP+b9zsuypeIJ&#xA;6brmA4f60d4j3S5e8g9GTKysoZSGVhUEbgg5kOoIpvFDsVdirsVdirsVdirsVdirsVdirsVdirsV&#xA;dirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVUL6xs7+1e1vYUuLaTiZIZByRuLBhUHruBglE&#xA;EUWzDmnjlxQPDIdQr9Nhha3Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FX//2Q==</xmpGImg:image>
+ </rdf:li>
+ </rdf:Alt>
+ </xmp:Thumbnails>
+ <xmpMM:InstanceID>uuid:c99dc4ce-ae2e-40c4-9a4b-aa284c938eb1</xmpMM:InstanceID>
+ <xmpMM:DocumentID>xmp.did:ec52f306-b9e2-3843-b957-fa1c2ed48430</xmpMM:DocumentID>
+ <xmpMM:OriginalDocumentID>uuid:5D20892493BFDB11914A8590D31508C8</xmpMM:OriginalDocumentID>
+ <xmpMM:RenditionClass>proof:pdf</xmpMM:RenditionClass>
+ <xmpMM:DerivedFrom rdf:parseType="Resource">
+ <stRef:instanceID>uuid:20ae2eea-f4ed-4396-8b5c-85f04cdd487e</stRef:instanceID>
+ <stRef:documentID>xmp.did:5f509fca-f7a2-a542-927d-286585280f75</stRef:documentID>
+ <stRef:originalDocumentID>uuid:5D20892493BFDB11914A8590D31508C8</stRef:originalDocumentID>
+ <stRef:renditionClass>default</stRef:renditionClass>
+ </xmpMM:DerivedFrom>
+ <xmpMM:History>
+ <rdf:Seq>
+ <rdf:li rdf:parseType="Resource">
+ <stEvt:action>saved</stEvt:action>
+ <stEvt:instanceID>xmp.iid:5f509fca-f7a2-a542-927d-286585280f75</stEvt:instanceID>
+ <stEvt:when>2023-12-13T19:07:49-04:00</stEvt:when>
+ <stEvt:softwareAgent>Adobe Illustrator 27.1 (Windows)</stEvt:softwareAgent>
+ <stEvt:changed>/</stEvt:changed>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <stEvt:action>saved</stEvt:action>
+ <stEvt:instanceID>xmp.iid:ec52f306-b9e2-3843-b957-fa1c2ed48430</stEvt:instanceID>
+ <stEvt:when>2023-12-13T19:08:47-04:00</stEvt:when>
+ <stEvt:softwareAgent>Adobe Illustrator 27.1 (Windows)</stEvt:softwareAgent>
+ <stEvt:changed>/</stEvt:changed>
+ </rdf:li>
+ </rdf:Seq>
+ </xmpMM:History>
+ <illustrator:Type>Document</illustrator:Type>
+ <illustrator:StartupProfile>Print</illustrator:StartupProfile>
+ <illustrator:CreatorSubTool>AIRobin</illustrator:CreatorSubTool>
+ <xmpTPg:HasVisibleOverprint>False</xmpTPg:HasVisibleOverprint>
+ <xmpTPg:HasVisibleTransparency>False</xmpTPg:HasVisibleTransparency>
+ <xmpTPg:NPages>1</xmpTPg:NPages>
+ <xmpTPg:MaxPageSize rdf:parseType="Resource">
+ <stDim:w>16.000000</stDim:w>
+ <stDim:h>16.000000</stDim:h>
+ <stDim:unit>Pixels</stDim:unit>
+ </xmpTPg:MaxPageSize>
+ <xmpTPg:PlateNames>
+ <rdf:Seq>
+ <rdf:li>Cyan</rdf:li>
+ <rdf:li>Magenta</rdf:li>
+ <rdf:li>Yellow</rdf:li>
+ <rdf:li>Black</rdf:li>
+ </rdf:Seq>
+ </xmpTPg:PlateNames>
+ <xmpTPg:SwatchGroups>
+ <rdf:Seq>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:groupName>Default Swatch Group</xmpG:groupName>
+ <xmpG:groupType>0</xmpG:groupType>
+ <xmpG:Colorants>
+ <rdf:Seq>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>White</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>255</xmpG:red>
+ <xmpG:green>255</xmpG:green>
+ <xmpG:blue>255</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>Black</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>35</xmpG:red>
+ <xmpG:green>31</xmpG:green>
+ <xmpG:blue>32</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>CMYK Red</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>237</xmpG:red>
+ <xmpG:green>28</xmpG:green>
+ <xmpG:blue>36</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>CMYK Yellow</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>255</xmpG:red>
+ <xmpG:green>242</xmpG:green>
+ <xmpG:blue>0</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>CMYK Green</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>0</xmpG:red>
+ <xmpG:green>166</xmpG:green>
+ <xmpG:blue>81</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>CMYK Cyan</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>0</xmpG:red>
+ <xmpG:green>174</xmpG:green>
+ <xmpG:blue>239</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>CMYK Blue</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>46</xmpG:red>
+ <xmpG:green>49</xmpG:green>
+ <xmpG:blue>146</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>CMYK Magenta</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>236</xmpG:red>
+ <xmpG:green>0</xmpG:green>
+ <xmpG:blue>140</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=15 M=100 Y=90 K=10</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>190</xmpG:red>
+ <xmpG:green>30</xmpG:green>
+ <xmpG:blue>45</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=0 M=90 Y=85 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>239</xmpG:red>
+ <xmpG:green>65</xmpG:green>
+ <xmpG:blue>54</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=0 M=80 Y=95 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>241</xmpG:red>
+ <xmpG:green>90</xmpG:green>
+ <xmpG:blue>41</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=0 M=50 Y=100 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>247</xmpG:red>
+ <xmpG:green>148</xmpG:green>
+ <xmpG:blue>29</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=0 M=35 Y=85 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>251</xmpG:red>
+ <xmpG:green>176</xmpG:green>
+ <xmpG:blue>64</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=5 M=0 Y=90 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>249</xmpG:red>
+ <xmpG:green>237</xmpG:green>
+ <xmpG:blue>50</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=20 M=0 Y=100 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>215</xmpG:red>
+ <xmpG:green>223</xmpG:green>
+ <xmpG:blue>35</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=50 M=0 Y=100 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>141</xmpG:red>
+ <xmpG:green>198</xmpG:green>
+ <xmpG:blue>63</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=75 M=0 Y=100 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>57</xmpG:red>
+ <xmpG:green>181</xmpG:green>
+ <xmpG:blue>74</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=85 M=10 Y=100 K=10</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>0</xmpG:red>
+ <xmpG:green>148</xmpG:green>
+ <xmpG:blue>68</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=90 M=30 Y=95 K=30</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>0</xmpG:red>
+ <xmpG:green>104</xmpG:green>
+ <xmpG:blue>56</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=75 M=0 Y=75 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>43</xmpG:red>
+ <xmpG:green>182</xmpG:green>
+ <xmpG:blue>115</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=80 M=10 Y=45 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>0</xmpG:red>
+ <xmpG:green>167</xmpG:green>
+ <xmpG:blue>157</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=70 M=15 Y=0 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>39</xmpG:red>
+ <xmpG:green>170</xmpG:green>
+ <xmpG:blue>225</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=85 M=50 Y=0 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>28</xmpG:red>
+ <xmpG:green>117</xmpG:green>
+ <xmpG:blue>188</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=100 M=95 Y=5 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>43</xmpG:red>
+ <xmpG:green>57</xmpG:green>
+ <xmpG:blue>144</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=100 M=100 Y=25 K=25</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>38</xmpG:red>
+ <xmpG:green>34</xmpG:green>
+ <xmpG:blue>98</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=75 M=100 Y=0 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>102</xmpG:red>
+ <xmpG:green>45</xmpG:green>
+ <xmpG:blue>145</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=50 M=100 Y=0 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>146</xmpG:red>
+ <xmpG:green>39</xmpG:green>
+ <xmpG:blue>143</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=35 M=100 Y=35 K=10</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>158</xmpG:red>
+ <xmpG:green>31</xmpG:green>
+ <xmpG:blue>99</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=10 M=100 Y=50 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>218</xmpG:red>
+ <xmpG:green>28</xmpG:green>
+ <xmpG:blue>92</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=0 M=95 Y=20 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>238</xmpG:red>
+ <xmpG:green>42</xmpG:green>
+ <xmpG:blue>123</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=25 M=25 Y=40 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>194</xmpG:red>
+ <xmpG:green>181</xmpG:green>
+ <xmpG:blue>155</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=40 M=45 Y=50 K=5</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>155</xmpG:red>
+ <xmpG:green>133</xmpG:green>
+ <xmpG:blue>121</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=50 M=50 Y=60 K=25</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>114</xmpG:red>
+ <xmpG:green>102</xmpG:green>
+ <xmpG:blue>88</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=55 M=60 Y=65 K=40</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>89</xmpG:red>
+ <xmpG:green>74</xmpG:green>
+ <xmpG:blue>66</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=25 M=40 Y=65 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>196</xmpG:red>
+ <xmpG:green>154</xmpG:green>
+ <xmpG:blue>108</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=30 M=50 Y=75 K=10</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>169</xmpG:red>
+ <xmpG:green>124</xmpG:green>
+ <xmpG:blue>80</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=35 M=60 Y=80 K=25</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>139</xmpG:red>
+ <xmpG:green>94</xmpG:green>
+ <xmpG:blue>60</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=40 M=65 Y=90 K=35</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>117</xmpG:red>
+ <xmpG:green>76</xmpG:green>
+ <xmpG:blue>41</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=40 M=70 Y=100 K=50</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>96</xmpG:red>
+ <xmpG:green>57</xmpG:green>
+ <xmpG:blue>19</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=50 M=70 Y=80 K=70</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>60</xmpG:red>
+ <xmpG:green>36</xmpG:green>
+ <xmpG:blue>21</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>R=69 G=90 B=100</xmpG:swatchName>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:tint>100.000000</xmpG:tint>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:red>68</xmpG:red>
+ <xmpG:green>89</xmpG:green>
+ <xmpG:blue>100</xmpG:blue>
+ </rdf:li>
+ </rdf:Seq>
+ </xmpG:Colorants>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:groupName>Grays</xmpG:groupName>
+ <xmpG:groupType>1</xmpG:groupType>
+ <xmpG:Colorants>
+ <rdf:Seq>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=0 M=0 Y=0 K=100</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>35</xmpG:red>
+ <xmpG:green>31</xmpG:green>
+ <xmpG:blue>32</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=0 M=0 Y=0 K=90</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>65</xmpG:red>
+ <xmpG:green>64</xmpG:green>
+ <xmpG:blue>66</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=0 M=0 Y=0 K=80</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>88</xmpG:red>
+ <xmpG:green>89</xmpG:green>
+ <xmpG:blue>91</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=0 M=0 Y=0 K=70</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>109</xmpG:red>
+ <xmpG:green>110</xmpG:green>
+ <xmpG:blue>113</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=0 M=0 Y=0 K=60</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>128</xmpG:red>
+ <xmpG:green>130</xmpG:green>
+ <xmpG:blue>133</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=0 M=0 Y=0 K=50</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>147</xmpG:red>
+ <xmpG:green>149</xmpG:green>
+ <xmpG:blue>152</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=0 M=0 Y=0 K=40</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>167</xmpG:red>
+ <xmpG:green>169</xmpG:green>
+ <xmpG:blue>172</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=0 M=0 Y=0 K=30</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>188</xmpG:red>
+ <xmpG:green>190</xmpG:green>
+ <xmpG:blue>192</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=0 M=0 Y=0 K=20</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>209</xmpG:red>
+ <xmpG:green>211</xmpG:green>
+ <xmpG:blue>212</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=0 M=0 Y=0 K=10</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>230</xmpG:red>
+ <xmpG:green>231</xmpG:green>
+ <xmpG:blue>232</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=0 M=0 Y=0 K=5</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>241</xmpG:red>
+ <xmpG:green>242</xmpG:green>
+ <xmpG:blue>242</xmpG:blue>
+ </rdf:li>
+ </rdf:Seq>
+ </xmpG:Colorants>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:groupName>Brights</xmpG:groupName>
+ <xmpG:groupType>1</xmpG:groupType>
+ <xmpG:Colorants>
+ <rdf:Seq>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=0 M=100 Y=100 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>237</xmpG:red>
+ <xmpG:green>28</xmpG:green>
+ <xmpG:blue>36</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=0 M=75 Y=100 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>242</xmpG:red>
+ <xmpG:green>101</xmpG:green>
+ <xmpG:blue>34</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=0 M=10 Y=95 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>255</xmpG:red>
+ <xmpG:green>222</xmpG:green>
+ <xmpG:blue>23</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=85 M=10 Y=100 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>0</xmpG:red>
+ <xmpG:green>161</xmpG:green>
+ <xmpG:blue>75</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=100 M=90 Y=0 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>33</xmpG:red>
+ <xmpG:green>64</xmpG:green>
+ <xmpG:blue>154</xmpG:blue>
+ </rdf:li>
+ <rdf:li rdf:parseType="Resource">
+ <xmpG:swatchName>C=60 M=90 Y=0 K=0</xmpG:swatchName>
+ <xmpG:mode>RGB</xmpG:mode>
+ <xmpG:type>PROCESS</xmpG:type>
+ <xmpG:red>127</xmpG:red>
+ <xmpG:green>63</xmpG:green>
+ <xmpG:blue>152</xmpG:blue>
+ </rdf:li>
+ </rdf:Seq>
+ </xmpG:Colorants>
+ </rdf:li>
+ </rdf:Seq>
+ </xmpTPg:SwatchGroups>
+ <pdf:Producer>Adobe PDF library 17.00</pdf:Producer>
+ </rdf:Description>
+ </rdf:RDF>
+</x:xmpmeta>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<?xpacket end="w"?>
+endstream endobj 3 0 obj <</Count 1/Kids[5 0 R]/Type/Pages>> endobj 5 0 obj <</ArtBox[0.0 0.0 16.0 16.0]/BleedBox[0.0 0.0 16.0 16.0]/Contents 27 0 R/CropBox[0.0 0.0 16.0 16.0]/LastModified(D:20231213190848-03'00')/MediaBox[0.0 0.0 16.0 16.0]/Parent 3 0 R/Resources<</ColorSpace<</CS0 28 0 R>>/ExtGState<</GS0 29 0 R>>/Properties<</MC0 25 0 R>>>>/Thumb 30 0 R/TrimBox[0.0 0.0 16.0 16.0]/Type/Page/PieceInfo<</Illustrator 8 0 R>>>> endobj 27 0 obj <</Filter/FlateDecode/Length 9458>>stream
+H‰”WËŽ» ÜÏWôÌX”(JÚƲ
+‚ ‹|À …o€î߇¬¢út6Î4»õä£XõéOŸOü\ŽßýþóñòëK9Ê!ÿÿý·—¿ÿzùôù/åxÿz”·5§ÿ–‚_p|}÷ÏðÏÿøúòë!æúVj=úÛ˜eï¿`Å_^^cf?ð§â­Fkìoï1vôî»HYǘCÂä”1[UihÊi³Å8óO>MÞÊÒ«)ÒÒ £Æ=ìúÜW¬ršcI«ÝàviðŒ³înØÀ‹F\ÆLònódä¼Rbh1s3QaŦ¥Æ¼¢†g¼¯îÒCô c…ûß_þþòçK0Ú[w—úå†}ˆÃP2 z³îÑeäü<8²ƒæ~
+kr’§,œª¤‹„IS±™ ^Ï×ÒW×°J/Ø
+IZž­˜€à[ƒÏœÜ|bpùhô£Í=â,£à·âsáïÄd·Z yͨÅr\VZ¼ë£ó(UŽíˆ¸òúé8¬áçoo³»·nq½º¼Ý¡5+Ž>êúðeéHåÀ†ƒôÌ KZ&˜hËìN“WEE‘ГlLu®Ò<„žñ^qÕæùïšoæÏ]0H9ÅËÉ­ð|óHúc‹½šïèÏ~^cd5ĺ‚¼–¦i ”6ª½¢Ì±š°*–ñ~¬˜’A(=ŽË,~„îÎöaæ¹ÞAx,Áª:ÎXlª+>ôɃv ë+×1Ï0XWËT9Õ—dIÌѾ’ÅTâ[ÅU4÷­v´„+êÂ]·bNí.]x†K;f{]2Gc~„}»4\OWâ
+›oOÊÒ4»œ…Êl*2™h¬îʪ7?“÷™ûí­U¿‚¸÷›û¥ïÒA–p·ó¹ib,¦=Á‚GBR†AÀ!hN_#YQ¾slÉG˜k€åŒŽÔ¥Uk¦d -Cˆ ™kŒ\òaìÝÓT¬¹ IÒࣅZî-;U›˜5Éžº+”0Yˆ§V/.Žâ}½Àk`Câö—[½5×ïÔƒFœ£úêú¡9Ì[\ÇÕ
+ç%±@é°pÃðÿ+!rnžŽ–žÝ¤¡¡µ¹ö4CÚsd¸]+6j@Ô00­®úøVág%hÒh3cP'¶&b#4Ñz&H²‘–¬–}e’‰=0`m\yê;UŒy‹ ÚpÅ’ù€#HÙDW¢b >(WAßi„=Ü¥õ0FÇ5²©îHcíÞoŒßFǸ!HìÎ|À³íYãšÐ]
+ÄKïéAX©3MvëÍ…„z±¯ï¡¸•§>¨Þ•çðªõ©ß§;[ÔËdb –Ò³}Eæ}…LÄ`‡33hÒ‹XeNÂu¨Ìè4<:n€ÿTÏ÷UOI",ž"p©wæx=5(Ê57)…·O‰ñs±ŽiŒù¨ži¤¿u_á*>u#¿RüqÚt¢E»qÍ;f¬m=óÿŠå†÷‰Š¹IÏzœa­ ïço»‰ÝT÷ä>Y„=VXzê!€ñnh EɈr9XEˆß ÔÓÖAmƒy¨lWsQê]!gÛH¦?‘ùH8©‰’Y@ˆÆy67‹&dY¤ÝvLûÒSÀ•J2[óÛÆÎÄŠ²#WN¤¼Cýó1=W]G‰'æ½Eص7ˆ^ã\Ͼ‘¢]­§H#Ëf‹i°ŽÓè¡(9­‘ðŸä<~SƒR™õ½]‡IÙ titT ‚èßsƒŠÂ 8ù²ÅþJÜ@¾g'Ás#š÷³3šR”`¬ßW'+¨Û¬ó.öÀ'¹hZæ©+£1ª"‘›¦G™v²5% Hu×/°D;ã…Odë‚Ók[»>`²ÏHÙYþz®¿Fö=Ý'z4èfÛÇYüŽL”z²¼Ô¢vˆ±³ü¹5Bí>Œ ]}Èeö€¤6ôRjm«ÆNå„Ót’—¤bÆóžyd|-„23ŠVe{ks'\ܶ&®ÝŸÜ`ú$ߥ9¦_!ÑJ‡c‘$A‹
+”¹»¬ñ<)=S?5Þóãà,KÁ@°üÔ¡èW¿Ot¨4bÒÞÖ°»Æ ¶©UŽ‹w‘~ƒ^ÌîA|, "Ü@kg"`Y8¯ÑžÆ¥(Z!B•ž˜“ñò®W0ªÎ©¦üHG ~Ù‡æäÇ”d)ASîœoäø‰â5üìb—"Í´ÝݬôGƒ¨ˆ|›Ï#?‚™;Ô¯²nåý«²}É%=+“Õ¦mµˆ¯“!}R@°•0YÜ5Å ¼Ûyn”ç¨{=.ÎU‘"Œ¤@íà,?– Ãq`!?lÜzµ,¦¥$5¤PÝò 1 ûQ„?š÷ɺìCu0ËË&©R™I¹É?æÿ!DÕ›ᎀýH‡Î[›k·ÆÖ§%ðã}Íã1KgnØëÜÈ‚4ÚmÒ´O½f8)r‚Ù‚Vl3WYàñÊ£-båv ,I·Î2R—CF+¡
+Q'5îLSc{bæÍÌ¡m1žúìx èÑ(çüÌx÷ýüeE·]/¾¸«Æñ;^¥•×}ñ~¨oþŒg¹ž›r~=*Æ¥5÷U—ïýç"XÈ£™išw3jçEØe—çÿlñÏÚÓÖÊ>R£­øº|¨ž"£„³y!›z¹Zž,Mé½o­:Xà ~LűÖÍ|j÷›{£®#Xç£_†%ùV)"ÉÉ2Q·fãÜОoìXR©I¥Xµ€gû¼¾ñÿq£IÚæÏ ÅöOv´¿ß´o4Ñ—˜¬bÞCî”]le9nÛµ€ø¸\,ý”d?2C‚å2í.çÙ5
+é¦-Ú׎2 Ï•ä°Y×&*«ëb®’XÌC…«Q^¥Bçäë}±´¡G›c2§öÎ?׎ڷÔh6¿µˆgRýÃ@à3ÏßbXî¼úÒ7©òi×ü·1ßN9;‰1W‘=ÓhU=Íþxi³ùœNsWÂIßë ûÒà'pXÃÈ&wà¿ÛQE\Ô«3XÈŽ^gqëB*«KóL5¡Bþë!6i¦¹9²Ì×ò¾ç†Ôáº,qѸ¼«19žñe…HBû©âP^]•$³ê( µö½ÑG(‚ðL:Ò!›ò[®Ns² ð®9Q~Uîk•#8÷s›¯„»†LõÝum+?ß2s]V¯šX™4“¤R}*öi/Š0UÈüÙzV~ÖbÏ+Šõ-žÊ³Þ‹c_é Âg"VžU½† ìŠòž}³A^ÅÔRï®ýu?‹ŒˆÉ•Nl¥•?ØŒöòÏvtæòzi}ÞO£þŒKûõñtÅ[í”ýÁ‹¦H73‹ï•ý
+ ‘Ÿzî¸@„Ty­¨±5êÿ™Úè/›7:Çñ°‘ý»)Ù›¹ŒSè2yŸãV¥XM÷ðÂñÌ Ôê~C£èîN5”ñ]i¤b¯¸¹¶S |MîÇxH­”Áº“’?iEÂÑ›W7GeX‰HJ¥Î+%y÷Y—¨Žž‰û?@§òP9°wᬗŒf
+š?væ`Xæ9Т¨ÍÉú'Ò>qdWHd‡o=Ô¨á¸+Ã44–Vg±±¾½Í™Þ3ý ˆØ„™e–­6ˆ-÷ ĉ3dù¼!}³ò #yÚ¾s“£XÈ&ž;tˆ—ìù%¾ÃíæØÅ“Qýxv–{–Í™688:漘ëƈçדŒ{Ÿ@kÔºls³Œ,pŠØ„÷àøLMkúx|žd$Ã%;òÃ$IO‰bDƒÕуú“c·Þz.“‹t`ÐS0ˆ†%ûôÚâä:ÝÄf/â¦+è\6c8'†PÔ}ä•קaÜF›5µã2ûçÎìÄ&A„™vF4ßî÷NœigÕï;&æÁ ¤õë¦u²:±W-\¶ºX…±¯Ä®uÕV.tg9=b¬2cg)!ÑsWžFgg½!‡rÐø• 8* £&_ì,ùÅÚÌб8WT^;›¹r>Ó1åp×âÎ^‘e–È9U>gég–f£ïþà¨ÈLê4«¨ƒ }XÜ’‘/ {´åÚi
+ªHZa¨'£ÌÒ¬+Œ4ôÕθåؼÌÃØ,],ßÁB
+§dd“®:(ã HWÄZ…=–„Z;zG‹kS`@¼•Ú蚊Íɤ%"jS{lGá:†ÃùÕ¢œÆ¯V­¼T N y´+‡¤P‚%¹ÐÅõ:4Üe¦‚Ž2óŠ”8§ºÿT²IÏ£°ŒÚDŸÅ:1’‚ •É €£;Z2,Ÿt›­ÀX{&«¹y)I×iùbOðÆ!d£$¥Îj<ÓUí't¼¬;[*öXd5Õ]Qaˆ8ìà8û‚|G×æöÍœ)B‘³‚ä ¯ E îl› ÛÏívö"¼¤àäË° \o{€’±ö5A­gð0Î"Ê6âêÙ¨]U°˜Ëè‡z–Ò L8 ¸²Žj1&¨>6{Zz2¾ë˾
+,3 • \{’gQÇQ©Är÷KÚ¼utÕ¢s’ê:žý.­ž§ñ0M2\åbÀÂgN=Kí´U¯6„ÐT58‚5
+*m© ,p=Ú%o{NÎ1x£5½®(³!Ég7ßû'œßµœ5Ài ³+ë.U¦–Ež*Ë10oà\ðõ‘Ñ'¤Z&Må-ûf°›ÁJ„A’°°ÄØ.¦ .ßU²+/_s3Гguq×âÀ¦[;¶ky×ÕÏÇþ\|y‘ç“Ó¹#ÄGìŠÙ{¾Ù¢-šÍjâT*ü©â¥lÎN… Ø =Ø7úRð Ù‚ „k…Û9ÀÔNêß„¤±„Fé„øa±ƒ,‘ŠuŠçü!¿„ªÂ–äA—š¦uÆ_ÿþøóã_ÿyØ£å?ËóH¢Ü|ä×ïüü[ÄA1I®™¹t§~œ]$3jÐâ[þ±ÒŸâ»SœÙE>ÚÂh51ȨÝàòÇF÷Œ MUmËHÉí”Ô<T«Le¨ÀÆom•²åÛ.}ÈG)Œ¤ÿ ¬R¬l?Æû†cãud^uõ­±‡-Ø+½…´lz7kÞÐ2§ˆö*Ñé”÷`S ëæ
+A—§Õ®ít¸ x,à€a¼1HHb(“f§=#‘a.då\ ò´§LE7ÒÔ4]øýl©Œ­îT.ÏŽW–-ú¿d—;z¥½ ƒ·â $DÝד&ÅüiRe÷ðRÇvÒŒ‡Gú$ŠPyüãj¨éöu›¢Z¤ý¨¡Š­xÒÆÏ"¡bFÞ6©!Ž,öÔŒê¨&S:(˜´&íq2$CîRÙ©un gíV÷q
+ê¦Jç( «ðÅP ŠK´\ Y±žâA5Íp!„Q„%iÞœ&—ͪ²›‘†‰ÀÈœE|$ÕÇozg4­ˆ)ãâÍö-YÖ¬£œì\±Ž8¯«m¶CË¥l¼Áë‹Æ&ÖáµáNç'LF_¹BšGów‹(¢8¥»¾D­îö™¥³‡¦ƒL×h®¦]Åx¶¦?+tšhmŒ¢÷£N‹#«R×:¡UoÓ@¸Iqu³¡dZtN¸­œâ ~–€\„Yµ0»vÇЗÍP+% #®®vŸµ§ÿõK¿Kp®ŒÞLë½)²”óÔ î_×´&»æ² nzé34…f°à_ïÝm);tgÈlm€›JZ?6vÕÊ()Þ$€®.Ù”AáÖ5¶Ëh>‘¦ÃÂyfœH>'wœ¬ü³]Д•á°ÔÇ„º¿Aq†ÕJíÙäŒÞ˜·öÝ¿«N÷6ÝqÜÌåIËá<5{>Ž“VéZé'¯õ¾>|k_:£Û»ÞÜ݇y°úff:Õ•<s¢÷¤OS›"ì€VNë„AszÐ^Ã[}" ˆŽH€Ó
+á´Çùúí<ó4V-¶íÎ6nÙìÙPÛGQ Z²½zŠ¿X$ôÔŽ•ôs°·„…mLŸ88¿¤$…-CÖ‰%B„¦r‰÷  ‘¼…xQ&­äêúùãÛºÏP(û<ùçh=Jv ü%*-H™’G˜<&lïÚÃgõÁêð—¦÷Ë}(ÎÁÞ²è‹7š@ÄɪÓÌ—ê¢1®?j2%ÎRÿÓNä/#x@ЇuœGÐØi‹B4×¥r£AØ,¸TÍ~@§»vú߆Ñc·àX¬p”j˵O\²L%+(wª/ùá.çvâb¸Jµâ“ý¤ŽûŒ µ_L¶ßxð©P`;§õIÙ vÐÜ'˜‘©}òØ
+Tβ~£¦ ›È×nf“¿„t„ˆTkza>r¼Va'á#m*l'7n
+ªã¯»îX‚Âm­ŒõHAÑgƒU)|%]&JûHÊT.û¥Ž‘<Cnô–-ê{cgyѬ.“WyÕ»69qÝM®ýŽ`ÒÐáÀl¨®Tact½ V]›<Z°×kn9?oúþ˜]g)ÒœÛå—|Èié
+]ŒNj$£íLLj1º>‚$ ’òxrý‘°rH>üó™ÐÙb˜«+ ¾-Ƙ˜oyÓhºì¡WêfaÏË7ÃÞ)+o̥ьª5uÞݘepXHËœgÂQ£KJšÊä(#U‘`ÿísV*¢öÖ¹[!êþè¦ðÏžô~} ¬’2…©2.Ê8]i8;Ô?&d´œ]©äfw`
+{×Hi Yƒ=Í·x´Í×púä¬ï™7ƒ‘€ÌuPsçë÷+aýuxÙ'¯†¶Ä™¯Go
+æÈIÄÒ^Y–PÔ™¯M‡’ˆ‚+\ºáÙ5¿è‰ýñ83âVËiOL4$‹Ål¼¡X:Pã§Y±<°ë1Þ•2¨‡ã˜üÓ¯Ô¼ƒ¨@6÷íÜ?èõ¡­’8µëé–zq͇ì¤}"Ä@0x¢ÅÚõ‰#Š¬­ÏH•ë#TͤI†kÿÖw$η÷9­h‰‘ oè¸öü–9wp¼dØX/Trß4ýq‹uDO™T}y#«»þ Sýº
+[€é',OZ ÞÌî§Ëîí”VÓi^²·7ÈgŽ¨$¦çë÷Cá+U¨¡ÛÈ¢ÇåAÊŒ#p¬ö§/àõͬb-®ýÃlæ;‚ÇJóܵ¢‘+ÚýÐù#G†½”°ÓÜlœ}x¦ƒ1°½ÿ2^.)vÝ@žßUô¥Ò{2ô k0â@4ôîSõÒ½ !ÜW£R=þÇõ³–‹®HR> ÐŸWc£
+eo©G±šKžÖ»“«^¨Š„öñ„,™•»“Ù3Y¯Æ|ç§sU™YLÐÓ6ž:rllËPßmk×:|‚i„zœ`EeåÖ½"û“få…o)K‰înç³¼(][P>L!¶p•óÙÔá—´«±Vú¬µóìØ6ƒ§s­´Ÿ§ëíCa«4« Y6H´fǬ,¤ÂÁ $Ëæ´N›ÈêduÕ»Á kÔÊqµèŽl­]ª/ Åø{æ´£ [;´b(aJ7r‹a^WßûF¦slàFM°8«ÖF»/«O­iWb9Ð4‹_-¤½½õ
+endstream endobj 30 0 obj <</BitsPerComponent 8/ColorSpace 31 0 R/Filter[/ASCII85Decode/FlateDecode]/Height 2/Length 20/Width 2>>stream
+8;Yd*Y+l@9?iU;C!F5~>
+endstream endobj 8 0 obj <</LastModified(D:20231213190848-03'00')/Private 9 0 R>> endobj 9 0 obj <</AIMetaData 10 0 R/AIPrivateData1 11 0 R/AIPrivateData2 12 0 R/AIPrivateData3 13 0 R/AIPrivateData4 14 0 R/AIPrivateData5 15 0 R/AIPrivateData6 16 0 R/ContainerVersion 12/CreatorVersion 27/NumBlock 6/RoundtripStreamType 2/RoundtripVersion 24>> endobj 10 0 obj <</Length 1498>>stream
+%!PS-Adobe-3.0
+%%Creator: Adobe Illustrator(R) 24.0
+%%AI8_CreatorVersion: 27.1.1
+%%For: (Esther Salazar) ()
+%%Title: (icono bug 16x16.ai)
+%%CreationDate: 12/13/2023 7:08 PM
+%%Canvassize: 16383
+%%BoundingBox: 0 -16 16 1
+%%HiResBoundingBox: 0 -16 16 0.000000000000909
+%%DocumentProcessColors: Cyan Magenta Yellow Black
+%AI5_FileFormat 14.0
+%AI12_BuildNumber: 196
+%AI3_ColorUsage: Color
+%AI7_ImageSettings: 0
+%%RGBProcessColor: 0.270588010549545 0.352941006422043 0.39215698838234 (R=69 G=90 B=100)
+%%+ 0 0 0 ([Registration])
+%AI3_Cropmarks: 0 -16 16 0
+%AI3_TemplateBox: 8.5 -8.5 8.5 -8.5
+%AI3_TileBox: -298 -404 314 388
+%AI3_DocumentPreview: None
+%AI5_ArtSize: 14400 14400
+%AI5_RulerUnits: 6
+%AI24_LargeCanvasScale: 1
+%AI9_ColorModel: 1
+%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0
+%AI5_TargetResolution: 800
+%AI5_NumLayers: 1
+%AI17_Begin_Content_if_version_gt:24 4
+%AI10_OpenToVie: 12.953125 -0.53125 21.3333333333333 0 8224.8984375 8197.9921875 1243 602 18 0 0 78 121 0 0 0 1 1 0 1 1 0 1
+%AI17_Alternate_Content
+%AI9_OpenToView: 12.953125 -0.53125 21.3333333333333 1243 602 18 0 0 78 121 0 0 0 1 1 0 1 1 0 1
+%AI17_End_Versioned_Content
+%AI5_OpenViewLayers: 7
+%AI17_Begin_Content_if_version_gt:24 4
+%AI17_Alternate_Content
+%AI17_End_Versioned_Content
+%%PageOrigin:-298 -404
+%AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142
+%AI9_Flatten: 1
+%AI12_CMSettings: 00.MS
+%%EndComments
+
+endstream endobj 11 0 obj <</Length 65536>>stream
+%AI24_ZStandard_Data(µ/ý
+
+.ÀÒÌ4mŠ0c|F|Á>UG§73­¿c‘½¹rÛËËè
+
+öºhRhZ‰±’ w$6mïr’NK¨•…±( ¢‰Ç‘äÈ;
+#Ù#—<$U2I0Á*Œ;Ÿ1Q{UEµ—EFDÒ:"5ͶŠuŠ‰G&:ŸÚóÉD¤³Ó]4"á94¶Úc»2{”»©ß5ýhhº…›»„töÞ\gËPõÁÊ»Uè'E:SýõCC£Ë¦Cº™AãÂHTI–¥®~Ð>ŸÉ’p.ba8ÙûžVa(–ØqdK)z˜XÆrKR‰Z¶t±bĉ¢§$±$J
+ƒ’Ä‚$ˆ‚ É‘ˆÞÂ0ÄH%B–#q(‡rG^ÉÍ#±’[( BŒFÙaêF‚XˆÔá¡$
+ãÂHŒ›X<êF…™º{I¢(”$Ia˜•­%©$’HV’‚Šbaèòå ûY¿»sBÉšU%’Âpr%I(Y cÝI”…\…qÈíŽÆ‘c…¡ Êr‡²$üÂxƱ ¢$êr„\¦÷Â@E—„í´0l…q$‡’(Å
+ãXîPA¶0T7ñf*Œ­ÅÂ×ñÎ*ᄲ…‘/»0„½Š’
+’G›š™gE‹‚ŠÇ3¬„’¼0’#O¼›•ã¯0è¶da¨eÙHpçC¦ÒhËwµV²RKqMëKf´ú™´×’ÚÎ{’RÞ!“‡·C#ª«h]Z¸f{»I¶uÙeV¤º3]2kfžR£úÂ;• kÓ6-Ñ7îfÞ¡&ì˜xW»d*Œ è&ö³ûXÝL,XPäP<;]$¡$–%‘ÇÈ’,VŽ,ŒKX¢ö0¥
+#)Œ,Œ&¾\y²ø¿0(Y¬ÂX¬å°ÂP¶
+HaTr$‡â•Z’Â8vusñ{µN(Ȫ¦âma$þ¬0œ( äXÍÂøY™ê ûP¼F¢µv(¶ÔÙ÷äÊ%LH #¡´>'…¡É’bŔ˄¾ •¸ÄG§êbÅ×ÚÅ°0ôHX—§H²Ø^öûñŠ<I|9b*n’ÀÉYYyi™šš«äâRÓëUDM袴´^“©ì'ÄÔ-ŒDa¸ì
+ã>x‰°™ýï%Â_D©eË;¯©u;9­™›Çv–’M}¼% Óè?S+´;ѦâÕžBs‡ÌÕ!ƒ>¢¦­§3t)¢é‘âÙŽ®¡*®ÍÐtxc]’Ùf'Ú½ô„$ŽçáýìêL”Xvà¡G‚$‰%ÖQŠËÊ¢F¢
+²ŠæM1'’D²$Jrh¢W¤ É"ŠÈ‘#‡Xr[ˆÜʾt$ö¨E CÑD=tU¯§¶ ™˜¯.¨`¢U§'
+cQäD^(A -K’$r(©ž™\Dä:åA -” »l¾Ó…¡>œx1•Ëê…‰±Ä£ó™ÞáÛïxýñ ,¼]Ò™›HkéŠxm¿˜ì²tÔ±ß/.nHa(‰¥Ã~ {É¡bÄHaœ.9©'x†LaH`#ÁD` °àà ¯3Ϧ1B=6ƒæW>»»“7[I1æ¶ÃÛôqzFÓC…áßÉRí"<<TŠ®\S54Z<^ù^VÊCfl
+C†ˆ 0x°À@0\€FK ×å[ÝWÇSÜ:543£­ÒÑ¢C&ÔM,´B2CÔDÃ{Ý-žo´«E„˜´v=ˆõCFuO õ6íTwzúD­Â]ÊßR^Õó¨n2¥IwÈl•¿^M¥Ãµ³Z>˧‡ŒV¹Ìn­iyÈXá+Ò_5%éÒ/«ÛÕ!£I¸¦½Wš÷AM;RŸÌ¬?ô¢M™× Õþü&í!s3™tÍnW”†B.óù|Õêõúðaĉ #±bÅ •«ªÊê°rëHJ©¥Ef³•™ÝÐÒÔÖC,5) ÝIÈŠM±Â@DAe¡Å‹—ºå .¹Ë± K²(‰…NTÂ…Q›ØMM$^â”Xd9ô‹-•(‡¢JÞ
+ãge‰E)äØÊÒ—F%¯óÊä…R¡ƒ–”ô&Ù“”t¶˜b‰!v¤¡ÝÎÌÊ
+ÃF–¨¥”J
+©£¶´²°°^WVU.ª„+NŒøðZFß÷Ù\¶É ZH¡„
+1ä#ˆ$¢È"‚ ¢ ’@(qR‰%—<Á$»ÂÀÔ=:EQŠoÁ%—\p)Œ\vAeYJP {â Ldb²$”DI$ $$’„–ZI©d’Gnjb^Z:) #
+"xàb"â¡a¡1qåÈëóÿÝ^÷·œrȧ—÷»³óÑ-¦bkji…Qf•(‹ââ&¨à‚ ¤tVZ^>1357DRÉ%Ij-5=½¢¦ª®Šh¢Š.šPZeI,ä²D*©Šʱ‰™J‚ È(’"‡R!‰c9’9ŽC<ð¸Ã:–U6Ydwuq¿·¶^Ú„*Šèj*êéŒŽ
+—/·ØRK-^hI(‹¢(‰‚(«¨’
+*WN)Å
+)¤,‰’$ ’‰&™`òÄ’JœP’@DA¤0 P( $¤Â@JB£;ž
+C‚0HpX€€@‡EAì
+ȃ…aò !²¡4dÊc  ÌÆmýPaÜê0íó¡ÂÈ«<Óíî_;TU¹JTëCÝ<T(Œˆ)C¦0$
+ °€a„†ˆ
+(Th˜ð
+ °€!ƒ†TˆàÀ0‚Gıˆ
+ `‘€á‚… X`"8`ðp…†ˆ ,0 4°ˆ
+&<<D`ÂÃCÌ*`
+.x˜ÀÃBâA JFÃ
+è
+C‚CC„CøD0X áa *D80P ðDpX4( pˆ<(†ˆˆ… ‰[g½1wk©ÖÖ£¦2BDä  À a‚醆 <Ë–!bÁ‚!bÂñ í²ÑT›"4veóT×îɵš§®ž«RCÆŽëó²}OÃ"àÀpÏ\M!áA4Ðð Â!Âá!yàÐ y®
+ D0H€XˆÀƒ{aBbC„ìŽ &,@°€AÄB\™ *D48H
+ x`:
+"0g¥¦ºx×Ú!cGU‡D¾­Û!óûÞɤzÊ:d&@P™ ‚‡†ˆ $,PXÀƒ40HX¨ †k3C(`ð€0@”pØD,P€xH0LhH<øŽ…Ñ ㇠,`"$,8À8,€ 1T€x˜ðT¨À
+@xP4(Œ¥¥C*hP€A´Cf‚< "8`ð€4hP¦†60„ ¡B…… @Qš Ì(› "‡‡ úh#ƒ ˜‡ ˜ &D4D AƒÂ #ÄB&,X` 6Qa‰ÀL
+0LhˆÀ  €°€<x°€#3áá!€ñƒ…1Ä‚ÃCC„%â Àƒ…áЀ˜
+ƒ‚©Àˆ`00dãcÃC† ¨€¡
+‡¹
+†Ã0‡¡ öŽöŒt4Í`þL×Êõ±½ÉÔ£¶¹ÃÜÂ}þÏFÂBćDf‡ ˆ™|(W‹v‹['Tã[êÞ%†©0 AŽ˜r‡ÖA‡²Èm!©×Ì/¦“ClÃ|(Vf,ŒBðÉ^ÉÄHaÑÅ” rTûÂ@Ö
+C‚0D4l€_4Uê%¡wi«RÕ{zóœ¡a‘Ò¡‘óhªß'%•.–-‘V4±Ô%r]ÌrË•Á:ççÌØö14Ç£Kǯ\›ÊjµRmŠV«§k?{’¥>YŠŽà.•Ü.W
+ƒî˜È"‰b5S÷H,H’zÊÝ–ÂX’IJjÉÜBgaØÑmrKÅ[Ær¦Ä¢ Ç&âë–PH
+éU¢;Û—ի–¹«˜V«DH§¹zßK+Û¥3V"ëðÖ…¡V|E–ÂX, ãHî˜Ga$Ê’8^·Q
+ƒr’(…(Ç¥äY9ò¶,»(ÚQ‹¬#ŒlQ%9V•MÖ…±¤’,«Wôžft,UqõvUÂã¢ÑϾ»C&ñêòªdZOgÝ!óûËhÍ…z¾f*þ/Œ;
+#¹cêA®Â°Æ‘`ꈢ,©¹FâHëºcꉂVXÇÞò#¥0ªD)Êq†^%”£0ÖGâ¯0ê0*—ä™—Â82—ÂXœXIäXM]¼­D¢kz­0÷}yð']º²íòT«VÝU»^©æU¦VÐZ©¯ãqéÖtÓCæ.ûEŠ¤WÓÜ#ºê©:SÜU}Ÿ’]ղʖî.÷Œè©f¤dªsaÐfaœ•¢$´Ðñ¥0¬¦· FíJÌMÔ¦Vé¡L¥:ÕeâþhªèBú}ÈèÑ{+ë‘Kaä’7¥0ãê|>ò*[
+ùv!_êF%»-JúcoæDñçJhÒÆq{jUue;V†|³ÎYnR]wO¦»þiIÓïu6êÚ¦ÚêÕLKi¢…–Je¹…êv«ª=ë}7USËðL/3óFµòXåÝ¥–™7èŠÐ¶wúT¥ê‹Ö|œ$Ž'Ë-ª$IaH¢ÆSIœ•“MLŽã8OMÉD}Ü–¼J²¶0%) -ŒZ#«¨X×ëéÑ‚ ¤L R‹Š‰äó‹!vX‚~1Õ #A¶Ù4a+ ,¬$Q‡â»ã¡œR?‡þf’K&¶·‰­ƒÚb$Q–Âxa,d‹la˜-u¹ò)=dŸJhdÄ»³®òîük™ª™’‘ìä=×ô0•Ög´–ÏÖ*²û)ŒË'G¤¸ìŽ…¡{ ‰²¹ì‡ÊrE”ÂX0A.»£*å;ÎÂP,a>KI¥0žÏLMR¹&dk»‡-H¢- üUaxW‚ݳ}ÈüE.½7}›i¬ûåª_ªIë[3n"ê*YÙÚ¹~Ï­0
+à †‰rÿouX²Çšz-ŒRÒ%“<Lð¸Åî'ŽKXb- m ,Œ=«S¶02‘è!›²/Œ¤Ž:
+A$ʼ, ’l¢Ç’DaœÏÇa¤¨fž3fƒ¦0* è¿pïåBÃÊZÍ£M‡ÌmbaÖ&ž~Ã+ûš– Â(»ÂÀÔE
+ÃvÖä
+-õÂÈÂÐ-Œ!ÛQIF³0’¥0p3ÑdW©LîØÂ8ê@…
+£ìL c ÃX…q‰‘Rú‘HŒŠ*GbqÂW
+ãÊÊlDDDÆÃÄÓ¯
+ãW
+)…‘X”c—¼Y9–¤0òÈãÈ"K/ýꦞÂ+2\ü‰niX&ÕS]LEL#]½½¹¥áqÑlRmÑòì*ÕlôLx¸?Ìc»•ó¸—•çæËÓ´Ó-C%ÅÅréa–Ö}Dç̘¸TTW.ÅC²•òy¹YŠ·ž*¡Yg¢Zј]6w´W¶$§âZÙ þ03—²¬…N˜júc5,óI{s÷t{Ðð˜ÝñG“¤îgžÚ;5¥K=µ;º§d©¦k§èuÔr*Ú?K’.ªíÜWõ¦ÚY,FWܺSkĺ[ªÂC¥µw™T÷4·’ˆðÖ !ÏÔöÓEÏ5QÝéímîR™}Ò­’©ÎiZÜûá©2+ÛêÙÄM%–å5¿'•”*K wiщ¶4Kõ`¢Qt-å’Ïmór¢ù–åæž´ âîÙ4)(±LïÖN¡,
+ùû´S‡æ¾S7†›¨¦zýQCc·xêf íê¬êŒŽÑX÷èNIËκçn0óÔ§zG ‘ÖÙ£šÎ¢!í!5fE§¦7ų†ÆiògšyVÍñzæsöÑIcZí¡DÛÙ¢}Z˜æ£ë¶:­¯ÏÝçA4h¶ºS4«ÒcjŠ¬,OÉiçOu®s²©µüOm4»i§ÖüÜh²0ªÑÄWæëÎÒÚ15ø<®ižóœ¡±J:ªæ^ߧø³“¹&Ó{vJi?ë\µÊ%D*4‰gçÖ”Ýé±Bƒ‡yh¹Êòàùô(š¢Z§Ý¡1^SãMÄ¿f_¸ø;š×r±°Ž‘YÕö†ÆÖ¸žt¦WÖƒ{žÆ?Õ›ÏÒÞÙ{)Íôhv½Çmš¥vÑ\mš9­´,M;µ³öEóÌ;>S¾¾Ô‰vVѤ+ÏÏ$Õ)EÄ£YLÌÒ“‰ÆŽN¥Y#ú–ªkÏ%šKë±tî–OÇÖ¬V$3MDcyH–l[‰VX[û3ÒÚoÖÕti±–²ì¬ºöDx¾I{«Íã"ÃZoÕZZOi*ÕyžÅë–ãí¿vŒÆCy¬:Û¯Ú!,<uE<Û:ãÑÛªúÎQm[2ß©´g©mÑlÍåãÙ¾Þ›h»6õuÊf§ÍõI’Ë\¶‘«”¬lHªD'ôÚm§ÌjÕeG:¤Ú›Unº–ÌÃCCÄBÄC0€
+}VçJó­U¦³²­*;[…¤G»µªÄ¢­Û[ÓÁ*éÕ­yOÉÒGVöº“Uö¡Ý-U-9o©ð7]Ú·Ôõ4¦%„R5>é·ÖÔ}èëìN¾÷xõL'©â÷RñPµŽlí­3´]*æÒ-~)ý"¢û–ºôî«öþüo¦«ògÑk7áæ]w{¥¢13}ªW_ŸíH­öLÛoo©Öª›¨Ç¨lë’ÖUW{Éuwêm.Ñ1*¢+<vݺ–Œô*m«èŽ^ùXyêªÖ u]VZUµ´ÖŸ«êêyêúš¹èl•—Ž¿»H“*5ïö
+Ô•U†tßÚêªn•JõFa«jíkêo-"ç!š]ÚÙòšƒk•F7Ò"£/®OóTz®®[=MënÝ}ÕKe[†¯´×šÿRÚ•ÕW{øýUJÛgej{Û\f\»oh{B+Òib­õÎÑZ?Ë¢3.¥æ©{ïTdw׳gtf¢1¿N«¹þ¯)ÒÁ´m]SXSé-V¹êy…{g×÷%û«‰ÎÓXˆøuúkV?–Þܽk:ýÍ5QÙi̤JÐw
+k±òVÓk•ú¼M*ï•mõ.#•Ñ\7ž‹ZHµz·wU‡ùßÞõ:­ÝVÞ+«¬´
+oËZ©rÑPoª^¶›Yé·ÞÊûÞ®uÉ´g*Kßõ¶rñ•mWy«T™zús•}—i™öÙæíNÕôŽô«Û4:+£3f­Õ…æïîË2ëÓÊv®šv`Ôw-Ì¥£{uæ¡©Z[GoÓ1Ú]±V+Q•ÐIw`PÏ÷eî]ªnn«äк‰Kh/3,»|¶„ezÖ³õŽ¾o m_U¾Ï–
+ñ–ÝKËŽ•…‡² ¿>ÛB½CX{E¾-¾ü¤´½úën×i‹ß½ÒׯÔöVŸ¼ò™ºîr·®Î]¥éM·Vé´Õ«]L¯Ñ•uoUiwº½Õ­v¿vYݤV·šJG«0XˆãA†ˆ† *xPaÉê•Ži+÷ é$™Þ­ž¥\+Þž²áé1ïê«„wÊGŸÏ¹Î2‹Ò™”{›r5qéiîÏZ¢]7í0­~úÓ´¯¶ä³zL£&žŸê+‘þ ^ ÷Žj’-‘þt5ÞҩΖÌôt×̺w{s¢L,ÛÃ=-®æý”‘éÚÞª.ê+ëiî–¡ZÏZ’ñ¬TOï›´Ñ¿¶Œø¥}Ù{J©e¸”IÆónÑ);5âi’ïØèº"]õ~óg.§¦í87½?¡Ú–ž³c"ZÅúÞÙç– ÿÓâæ©òïÀ<Í^º¦5J=™›æ¦´zhŸŒ-êþŒ¢íf¦)’–SDöœâïÀØîw 3´JŒ¥‘}‰ô,L3%*í±¯o·Ê¤µJo¼Qt^-¢™¾,Ó6ïà•O^Ûôé˜ÎòzhÊÕgeùî[ˆ×Ä›"ÕZûóWÛB«cWf½–vÅò‹Ð§®Œ!‘úv ×ö—kF¤vVÇ;Ïçyºó|Kå;feÎîh×+™ÙÝ}{¸æ\§¯‰¸u­ë³V·ê­[ú÷Ö\úê3íTžóFxë;»ÍS=Ve’L˜×»â1¼AoÙ UÂU‹JP2";Ýu³HÏêÓÁ 펙%0Ý
+ ¨„1ì"R˜…1…!*ƒ
+B?]ÔTù’ËØw¿Ÿ8R&c°¸Œ6 ” ÓÇÈd:s;+ßǘ‹ìÇeäa¯½ÉÀ2šËj8\'‘r‘Á?Ov›w 23êç•T‡GudÝÌhÉ^ÚmÐeœLø`3#êé¶ïÜ[33b"O^OúTVä›1ÐZª=xÆeÛ“£Z0Âõ´Î6ÜçðÉÝ£ˆ«Gqjzg,H÷ù‘¶3J÷¿MnÿŠ›ÚÎüzËÍè1ÏGÝŒN™rÅ•}€þÒÌè þfF&é
+çÇ7#„aê33£«Xïý*3ãÐ-fƘ`¾‘Ÿ0tßREÞ£¾‹3ÐýسÙp9¡ç-Æ@=¥Xn\›uœ¸¹zõ‚ÝÁfµÈ<Õ¹VÇ;oö=ŸLæ5kÝež@Ö¬B.T™í°ºÔ¬¶Í“úQdéܼv»›µ_ç2i4¶›•Ü¡p³Öo¦}³^íŸgš"þ÷Òfux}—™-«ÈÆ"Y[~©MÁ—ÚU
+±È
+ YìTÕ^ Ym¶!+
+Ê•©ÜƒJ@Ü=B²Â)Œ
+ ÷é„ðã·T´QIÝxDbÛXKAéØX|@‹Zc£ÑiQ#uOs¤½Dá7éO„×F°<¶í6VcŽVIu ym©ÎÎ\8ëÍ’à&”–®·ûÙÄL¸gÊÃKŒ³rÜq±x´TÀ¤‡)á©:LÌQk§)”
+²’-A©f¦ƒtÔ`9|ˆTV?ÓV÷•þH0Zªî㹺‡&Ö iT/ƒù¶^®¢®¨ ŽÉPÿ°D÷S»?Tcꎅ#1·ü„Àè&ý–ýi”œ«·x®I7dSÂGkv©;M°Ø¹A¢–$ôÁfz„`Æ"tL¼‚ß°‘™â¯ãóšÃÞbN°°"+ ¿pâ_lùZÄôç—‡ÄB‘ö ÑB’…ldcŽ'Ó€|‘w$üÞ; Ô¼à.êÿõ=Í_Öþ냅•g¦ 5Jbå¡ j¦eÏênðé’bÜÉz¾HTs?ܬÑ멯 _SC¡Ä—ýU}F†Ôi˺=E[ˆ©dÊtûèKä 6þò~\Ó‰m£æ¤¶/|­â¸/{¤áÇgÍËr7JÂp)b¥b¯¦à†º²n\-|t Õ Djªkþ©¥–”ýM¢Årä=n\PØ ±´î{Lk¦£Èþ=ÐËs€´èxŸ æ(ZNT3½“(8ˆ‘3Ϫ#{Þá«ïcüÇlæâ
+Ù÷x‰Õ8ÍŃ¢»éÈÖ\6p\7F І…‚Ó?«Î-ôÛyœ…+Ná1â¼I!Ľ¡ªnô3—³¸‡ô.øEymÐ~¼>žÉ%•GÔF¾„ Ä×%Є ä22™Ö4ŸÆÀ8 ù `[€:ú ÓPôšßxg…¬Wô¿¹æHI<›ó²#6ÇF¹bfƒçâsF59€‘¿]Gö³6Ýâ}eŽ±b8hžä1†Ù Õƒñ›¨ê»#{ë.£v
+X’N™–ïÈó>šBà+Á–-«ïH8… „]¤A‡b¤V(1Ø4‹ï»eeÓÅòҖΰ½ð}ÌnÂÂäŠ^ZóÈ5Úcü8—8Ê©6‘<³¼‰ï\ßú’Úí:7·‰‘,‘ŽYÜXx ‚_1)4Ÿ º5Ú WÒóør®ïÕ„ çž²8’ÅÏ({ÀvÀTÝoÒiI'}M’Lxþ¢È,'*™ÌhL 9a4÷½øøê|¯îرËr˜™$™qf±îXçßFŠ´qrº!±×š=Šk«ÜjdÜ0®ô<JjKPæ™ÖÅ´HÓ–[GˆÓŸˆb( A¡Ï ±±²ijvüVb‡¿–°PÒ€kBºœ‚ê÷•lx¼óŸ¹î Ú8ÀgØÛÞ.Yp µñ›ýTÓÚ4ëeÚóÍгË*)”ÈŠ¤MЉÍ"iØlpÊnJ 3:¹,I׺µÖªÉ¢–t¿šÞ©Õê‡:ý)lj2©HyTI ÔŸí 'ZY¦­Ýò*Íqç<H?ãÏ‘ ?ÃqŠ>ÿeØb6E4`CA}?ÃOòŽ{°Ä„áñC,²ÎÊØq¨¥BÝÚŒ:! ZšýÀðÙÊ@‚5cžp“—°¸YØ«f‚[”^0‚^Ž&œ`·ì0 µoâ2~B¸dûÀQëâºê+ÜmàSj•ü…Ré ¡)h0ØÍääWÛ¾ ˆö"¸†'‚>8V¸ƒ¶AÈhÚváìÂzî„Øb¥Þw„êB R“ˆ\jé‚Lç{ŒLáÔ_ÿ•—Ïu‰
+²/š;J~}‹Rw·UÈY_¼êçüå¢3N8óNž…çtÓý´PvÏ-_kµ¶Ûí¨Ñ0mÕ³sÿ†Ýåóñ%d…þ41üáäJp'–9½!S2b½É±¬ÍaØÈØꦭ½Hüªªê™@É5…8yÈíÃðW|ÎhÙÌŸ/÷\šûÖQܬþ;ŽHMrâ¼^iÇÁˆå:˜Pí9r[ù·aK˜Uêˆ+iñd|m#YrC-˜ûe*rZÇÞ(²±½¬@FÅÖ"q-¶º4T̽)¯™ËO”›ˆÝz izÈ0dbÃì#b¯vT ¤$!•yÔá`BsÀ[Æ"òjF+70 ’¤þÀÄÂôü¢lì«/žõˇÈ\x¯´ Z2;Œgóâ2™ÄëØ
+Ð 3 Ðv”¡N×%¯ÈéŠ_•ŽÌ\ÔÀ#¹ê/Å‘,€o‚ x‡úFæû~g7oÈÏmÄbw m`¢0Ã&tÎXÍÚÜ“•j˜†-À‚2­;h´DkÄdà(å˶õÌæÚ…cËšc‹¸%KYXûÃ}êŒUŒ¼¢MÄ0Ǫ ÍÀhž4\…V2§°òTûÔBvK\¶X—[EÍìµT²è°Š"À"¯[õZ$Õ4”ŒZŸÀAhKN¡€Î(EQ™t®(ßjå
+õ<‰2ü¤Ë[žNÛ¬
+Ÿ&¾·zË@hCá
+ÿ%npûÙÏØɬ§O˜ZÑL=­É<#0Ò xÂÀƒ|ÙAlfØÒ™E¼å„ÌÖ%†q7Óœ÷eˆrݹ†DèríèP>Ã[oQ¹ ;LyÌ?Y>LÖzÅýfÖºÖ.Ïë$ê-i‰4¸³°¹ðï+Ϧ[‚•×ƒ-"
+}ÅLÃCù»•(èÍ÷7ÿñÁ)k§ÌxñÉp}5‰œ™ãí®Èw·Í©L¶Ç!^ª˜„`Ò#°ûn@>,i8:1aY gu®^‰›NáÐ*£üÁfÁ3»†÷02<ñÇ“á1€zæM+ܾJÞ9A'^ì‡×~YÉ\á¿ ÛD!g™º˜@Íåfd$(é&ÍÏÞé^€s[xhwûoÔ붉mꇬû¹1:/ôuTÈIýòüý 1è –DÔ3åGwr }?Ww”a´Û†ÑÈ<”÷“QÏÑ·v¡Ëûû(l³ 8Ÿ&“l’;jDGÌhý€ˆ õVÊœÄü< Ü¿ÈÕñìý¬ëŠ›æ);#3œ¤†Û4D‡äkdQݹ.D–ŽæW.¥(Ì°°­f¯-ï³Ôò ¨RØ›fÚ88ÓÊŠ¨í°uµ…AÀË;ÆŠ!°À^aCh›zµ:¼`΃îi†ÒÊùƒµhEQÝÅwŠÙ!èÂÔ²¯r (SÏÝ™ÏÌâ*Ù¸ôIŠ#¢ž8 ëœ€D_ÀÀˆ#ÑZ,U%¼Êa꓃(Ý‹—ñìÛ{ÙZt £ƒÇª±1ëLÕ’Ù‚¥Ÿ¤ô»8ãq#2-K½øaß 7¹t°FÈ(`͇!ø˜ÜPH×?u j£¯÷ªúõ{P‹¾ðWxäÀ6<FÕq†  €|e®€AîARh±MÖJè5F%ƒ
+[bHA½M…I[ë œHHA« Šu0Pûž¡µ`m"0¨ŠÎÉ?kñA(ª$Ÿ#{×`Ó{Ì2ý9üc2n6ð$Š²Ÿžò#)ò»ï'Ô1Ðgöd
+Ÿ8…oœ³ÂD§¿x±»²û ‰¦1Ðø–ãÉ/-xÒGìî€#vtfÑ+ÌdFkúº#Œdô¾TÔ"‘F4ô!EF$¦èÓ=ŽÓÁPlK0°¬O"E'}
+ý%?úí³Š¤ÞC#ëˆøÓm¥;Žmj¢%#]9].šhÖYSkˆG{h0 ª5HxÓÀ•°å“h¥æQ¼‰zÑšèP´hÎ*,š…å¤%Â-ëÑ¥Ä+akÇdm— „KQ
+«b–·þ¥¨ÕWTŠ>[–ºG—¢ñ‡Î%á"MO!q)šÀ壘´œãe–›F›'2¬ËDóp¾›µÃMìYÕÇ[ið[Àô+Ùp¢%D Ïf¡e±êi‘+¡±Mâ^…Œò]zCs¢9ÓÝÑ»†µpT$€dž_Švn¼:¥¨"§ÎkÉ—¢äFÖùý‰ŒÚ0`,s(&Ï•¢o6Û<ñ XêÀ½í¥Dõ„Êxα •J#CŒB_".s…B/¹Éaã*FE>Ê(ýzL‹Õy D†B»I k‹réxè™äÝP`y󘸽¡”I º Å‹ÙxC }h©{CéQ~ùJá8XöÉ8¿ ÝLd°¡@‹¶w´2¡Ø↞pöÞpcCO, &é†rἡ›S÷$Ï¡;lËî2Dj΃«­NUäÔ£Öo'»åÿÊí‘’y‡Z‚f´Á.zÉÙ
+edã95´B©K£xÄVvÞ8ͤRx2i¾ 7dŸ2i½ÎC¢“ªÊk Á+³Â[VuRïw0P'Ü”öJ×ôÎý‰àI3ª¥ÀÌçO:†p2´‹R9€â¾pJÛß~Nå0”‹~tEД–êx*îit6¥Q-Ž×À³±¥)õëâtb4¥ïˆd™6¥Xüâ«)•ákF;æ܉M©#Õ7¥rÆÂ!Ï”Š´zŠ0¥¹VÞ3’[ù!gJñÙ‘ìš)5eaœ)µ0£P€y)á`xÚKiQþ£h/¥n"?Z¡”"egÇB 7ôRzÔHÿ…RÍNy¹(”ŽõßHùÊÇœÇz¿°±ëBùk½Àlnü$0ÌZbº(lŽÎ²•]—o_s•àï¬tÍÝSþ†•~ òµåyÒiª2Û¬Œ“öErrÍ‘ô~]C×ÜÚ`<l÷¿x7þäáÜç3G¯=©Žžå37Õð‘
+ç<
+ñ]¬-å£]N0^MEõ·Ùd3x€Â§QQ†ÈAž jªdˆáM–ÈÅiÅž—I`¸UÄú’oÃÙÉàn%BzK #;!60ŠªkrX5rtn+¸cÕ½¥¨’q½-›aÍUãå‘ØÈŽ&Œ°ìd3ÎI²q(T2IŒ`ÑÑêyÏOMö+ªéŸžúxÊ·!ÝÔùøa%Èòqô3ó‹áP’ò€8úpI¿!ËžÓEŒ½êFóܲC„û¿Bäè°(!ÿ1ü§¶½T1 ëMì$Ô
+†-Ç#ÝŸØã½êÝ ²Î-”¨Z³íÈ á–6>#G)·B¾CÇKòud3ˆÐõ{·vo™VñõÜU–ƒûîÿ¸‹/Úù¾±lÛËä9úU²a”îC÷ƺÀ…©¶o&Š%Úz$KÓ
+Žh’É¢¶µÕ€$Ë°¶B•s¬å6­C†ª
+iL2ð%RhÔÌÙÁek1ÜÈáŒe‘V r¯Óã[ÍþÙH,Ü'C–Y[Õ –ÆŠþA("[щ#A˜¨<¬€É28€tpVü
+Z?ÿ«Ï5ü…úZVø¾»†…>ôõRŒjv~sg ö$ßè§8ÉReºÛÓÇYêöh•{6ÜôTÏ“®à¹8\¤õ*¿¹V_Ü‚ðB¦6xXK½Ë3d×Ô_\6ä¦*–Û‰O¤ù³ -­‚÷؛ӌúüJEoæH•ð<MÓ k4×,ÙâÛŽg‰œ³Qíá?˜…B(ôBžV`÷„ÜùŠÿ¿Sámÿ>Š “"|ÊӶÏK4ÓÞÎÃ%cu¯lœæ8‘KŽ›
+´W(‡:O
+ £VÎèyþ×q^¶ÄŸúNü@ŽŸ÷Sÿ¸® N|·eœO»N…fG$¬Ï óþDß!ñq_kÕ;ËÃWÛÄÒžízžššQª]b$m¢Ãã;OˆãÇÜí²ŽädŽ9GñÀ QPe½€¡Æ¯ÚEä…ñkGÙÏòµ5ök>È»å>ºÉ¹Öâ½/eçfzâ°éÉT¶|QÕM×!!úÔ5Ê1 5/£6I¹~Yq(+õZ#ºbdáCKüÒ Ž»à#öBMw¼ÝàRh¾Ð‘1ªÔ&p*a‰öEí³K™rbÞ³"ìÆ‚î0¹½¶&®ÇG˜s»V³e+ðñ§ÅfIêÌpµÆŠ:´>ÛÕÌîuF÷F[“ìñìwEèŠ „W»çÀj*l‹,g úîl¯üÃ4Æ Û…‘ò®jë­mu€{Á£a‘$DXá:ì;‰P>¾à–gÆæÕLu(P@19©ÍÈɉ‚ÊèX°:ø¢Ý÷a෸œ]ûêÛñ„Œ$“ƒ¥LAÎjo&rt²®–Q›I„UìˆUøøˆ@Ѓ¨À¡'"FhPèò:)-ºʇ‘HPZ(aþ(ŠÌ9e}eQšBï
+a;X¢¸?~ª:–ËBÜ€v£ƒ‚Q¢TdB!Bú²rIܸPdVJ>»/ o»‰ë8WOÁIá—¹é V)µ‘O“*0-p8r¹(åñóäð¡.-ë¿äŒšw¾+éí‹tpœuGžÉÀËÐÀ¬+¿ M2 ™ˆ,j™ 8«‘• ô‡e’À,u†–”–yêð¨ñ¨æ¡9çrXÇ´Y˜ŽŸÙ}(YÔqAahŒe»Iv_äó FtBÎ
+ü”NJ¢ª?™pYÞ•²–äµ>øáòA_sc꘢ŪyÞĈvª½^Š6ÒP‚ÜX{z¶7?A%öü•ýZ/‹¼C‘ДÑPd6-ǯIÒO4×I×ÂÅ£~°ý~ÆxIJ›ˆ‘Fç°ÚSOVe¿—÷œºP§Ž³)—q⎠…[Û›e¦gC 9†<ˆÝ ÄèæÈ¿ƒý.ÀÑø¬ÍM”ý5ÛXM~“ñãj]×™øj²´ÈÿÍjî-Ügiýz$bµ¾“@è ÷fÈÜ ÁÔÄ@Ð"lªúUúá¡m åt/׳ü!I' ¡‘dbºi2º"ì“D#9ÅÜ@hÐô“*Ý×÷¼iDZó 4óGƒCrtk«1ÿ4aàb\e¬ìVê/‚ž<§"©Ð7“ é…¸£Ë‘i´÷«hÒ•þº(D²•ó?QùHDùáGLEÏÍ‚|íàƒ=[½f%øÎì—D8оŸt}ï‹»ûè „ä ÁÄ”>à)ݾ°D©é""._qf¶¸ã|”l_ûUVÛL $w„Üî’lN¡í­~³ÊìMG[¢ç'—–< ×˜¨FK$æY0M¾"Ül·DÑ$YjYÙÆÀìÅú"ĤÄØæÖË?´,²fŠî™SàµàÃýº òãå7<ç‹ é¹Àyìx¢–˜cö1ºF‡úØç˜x5,ø„NßãþƒsÁ§çÌ*)nÚÈÉ$Yo¢œ¯¨èìV/Û fXöf¡Ì:JU’—³±-UJíÁMAá›àæ!½ÈÌÃØe…ŒJ+ôDZ„ðPw ˶1Ù–é™BD'\!9´Àjû²p— Èn®¾³N>=XðÈMvN¨Œ`…ç~nTFG˜ºß½á£bu­w…ƒùi·²“}"ÙoÉVb„¶Œs;lX˜ê2.ê5ÌÑ/=Õ dÓÒ€d_#á‚´¤)œfHÓq˜„Ô„ñÈô3L¦—^¯Õ.M´R1¢0úì±µ!žLç/˜/H:‹ñœ¯Å0öćÀXiÂ[­‚ô\<b©2 
+U™åâ”»OF{)ÙV‰éuVQ~Rþë\¾Õ»#)`½T.z«vxﻎ´|i]5õ+Ë¢Û9Ÿ„þ©Jr…ôÒèÞ¾—'ÆðfèЈ »’O·”.AèSV‰4*_ñfè~7~ÂÛ¸¨}?²|hÜŠÐàkw·CH¹í’è i6‘ntÓOÉÅK9P†ìîm”Ýø•6E H\n² ¦Ìçs=‹O½z™]s]þ·*g˜ãŽ»{¹!<¢G©v+‹þ¡tîæhM¯]˜ãPdYÿóC[w þ¸EȲÑí‘ÚÐ_Õ*|u7'|nð«…d·…½oVÈ=ËJk¿²­|`»Ò?bÎjt«]ŸA œ¸ eŸ^"…¡u~·Ž&E¹LaÈ`7–«¼´^7nôÆ©Jõ=y(îŸ1½ä?‹5£^ÉÓlÝîjË—ƒlP÷ÈÅø @$N.¦N¶~–?{&×·[tÙåÚó£Ëîöþ É€$
+¦mIÞóbasxÝdfÁ©‘[Ú>ß¿_ÉEÁ€-s؈)B·éRÊe˜Ž|jLT¾¢…~ÒPûƒ™£ƒ6ä$EËé…äÆ  eI¥ ®`U<‚ dPDnd¶§g¥d0ˆ"?·þ¼ŠNCÙÆ'飉pøn[ÞÇqAen1u§–é‡ß̈X¦0¦®]ïjª*e6²lî"0ÇŸü%ž%þèÄfßÍö{w
+• ¬ if<vÊW^jŽÆ „Ç(X g¸ðS*@×@Ô]P±XB³2Q=‚˜«ŠÊ!«b˜(Œ0N’Äq *KÞgÙ×Ù<GÊøF0%š~xŸí«¡ŒyT±…'­‹þÙƒ^¸„_“OiÐWÑÖ¨é¤ø:(ÝZ¦Šâï}ØŒ8°–PÿOÙ<JIU ½j­š‘TÀ|Z‡ÓÇåyVüƒómË÷6KXÔxتXH»îÌ?m/‹œ‡,½¡¼®—#kÖ‹q¡ááøîÇ5ËÞ˜vûÛ„¤•ÿö5Ä
+àØÀ¸ÔqÏ=Žmˆ÷‚ðMb¡q÷ ŸY!¾ÿÕǼ¶xÂë!í»- ¡l3 'ð.öžF¼– þFb2e$ÀY½(­3 'í/^¹â÷_eÛÙ:ÓÇmá ÈoõxÈ™Â8nr£#^bØy‘ñá%×.&0å¦[}òHñáíŸÐŽÙ*¶Í¿¿^/ˤ•dY[.ArV05©ßb¨l9QÇç»x¹uâ>ýÿ§‹3RÕÛÓ'ÍæRŠ‹\¦*
+®jErµC_Þ% C¥²«jËyQâÌCì*ЖÀÌ*Oé]#÷j¬S|
+ÈŬb·%ªˆb’µÕXµà´þÖA›X‚Sµ=‰Ýé*À=i+­¥ÝA»˜{ˆ´‚صñ ÔÁÛƒ±.hsf¿´
+ ‹h£ù‘Õ(ÿqÎm;í ¨
+y}©ë¾§(ÅÛeò%!ç_+õÆDzÖï»:1 !{”æ :ÎOâAZÂ@<]Rc¥Yc
+†_• ƒœ² -kÃê-Wz8UcHÙÒAÉ&¼AõÓ_°o ¸y²è]³OwĹ™æBQÊ*ƒ«w»øÞ|òIô =ý¶Àþ[€º•• ?‚O½F÷ m2·ιˆó“ý}IÑä!÷á{#åOØzÿ”uا  Xzèª°Ø u <°§#PXξò­wMÔ®ÚP-ihý¤Ñ¡þ(ó0ÀÕyÑC2ö]ï—9x2K½•µÎ‹ÖýøT°Úµ¿åÛ£€˜‰@zähÀ:,Ûû[÷T¿."ú óã?ô·ûÚÁõ‚äЬ8Ã'Å/¢óoÆl!9Nú-ŗ迈„C™»Øxoÿæ $ªÐà’fSWî^(
+œ—“ÄaCºêèuZ’Š”èÓp»…ÇΨ¸-²½R`\ÐÂT‰F,ægd‡dºÌ~â]“e"”æ­e† í~ÿo¸¨‘á8ÆiK]~È„bp|+‹ñFõ äQà)«G=ÇQò‹æ8ù)6±è
+·É/fÓÏlb(××4|ÄÆMXØŒ å\¡"=7z‘I8,#Ô…ˆ~Eó`ÙjüiNò¾Ýò;ˆXôÅC
+†?|þøi ƒÍÆéÂP)°~Æj¹çŽMNáæAà‘åëJ²ååJòÖ(¶}Â^ü³|€;ߣˆ…ªgåÑþu¡Ø‹ÑRQv†ÏloWnm[ðý“^UônØâ›–ÚM¡–Ož¤„¤^?L0½ó,R…ʹ d›¾sãê
+’ˆ–s!˜  [ã?z©•³–¢É¡­è(ü¦íÙ_ªéXÓn£/BÚ›µÀ€‰ Cls_¸èš;`zÇDKS00ÕALälÇ\^crãw¹±¨†Ï]»îò¥ÎÍa Ý “èùçfVVH)³¬]pžrðiª„2G¾ÏJ
+¸Õvq`Ã'ÎÊñV´vf¤„ÿ`XA
+ÜöA¢]nÔŽ¥R´?®Ä<fìÅi²˜¶ÕYÌx³ì’˜YÃaæF:Ô°#ªeº¥ê5¬5Û2+‰ˆÖ»•µÒØÜ›'×Idëßö±KòpDÅÿS^ÕïMÔxÖcËH.ì³ê³ý§8Y)‘NÍ ‰rÚ5!’ÛTn_Á —&PÚKÁgBae8JZ·h<(Ó榳(˜½ØyÜŒ(ýê9Üm”tF©œ€;oˆ’0"œ†~×¹  éؤ;±:nÇž=Uq~q«mDaÈ]‘j„^`vâÌc2ãK5¼"‡JUç+ž½@Õs+ú(*¨Ž`“5äk Ü¥è@‡…y¥áƒ°ÏúTÍék×7ø›*ئ`søÃikR@zÂ
+¹é?©ìCêå‚à)EÕ7͇øâ|¯BU‡EqaÔ.1 ©Ee¾ˆP§¯Ruºý¯ªq¾¼ª¯¤³!zȧÙå5îËÖÙâÍn• !Z- èòšðžQç)à|Ñ’7V¬Ç䪽îøs’q‰xH#%¯ß_½ûR—WŽZíTðqhÌ•'æœdîÞåb?¡0a¤øp(µŠÓ÷¥Ä`XYT½ÁDªš» ù'g0žgn1½;ùHÓ¤ˆ—Ôn…
+¿ zò÷d]MÓŽÝI¥ÄË,»Ð|ó7ÝÏl…í%Ô˜qúòIÆØä/ö^‹V‹ô—N‘fRKêúYÛËþÑC?/)€­ù܉Z˜ö•­^2îäa _‚Ì(l΄Ï%64,j‚K Ð*ˆYâ©©$bÙà”zÇ”ºÝ£¯æ'Ý XF깨}׌kD _ŠýKñ o÷Tvež…³èŠb9·f”[ ü«ü‚)‘ûç8Š×'¼ÆØÄ ËÆ? bÎaçŸ
+M^ô;Þ‡
+ïmš…‰¢ZߺZÅCÓ®;å½ñZûé ÌiU¥Ëú;_§sÖ_kKþRÒ?ØâIý뺰­ýG@ž^ÿàåëVî[ÕŒ¢îµ¿ºIó7IÝÆÂfÅuÏÙ=dgçö¸Û?®· 7’¤ªnÈui8rŸ¹É½¾å ªŽé£ÒÏôÈ OÙ= ý $Ð/åLñ‰éry$yß\€oà%ÉfÝ#÷ŒÚ(á½V˜®i+kîc¯G¹½|?÷Žá——:ˆ1·Tå‹ø {‡|]íðõš¬KèrøSÔ?q%¥ûÛiBŽlo”åÛ•aùñ|'æø€ö£3»)H¬7j;°ò>[}î"¸Z‚õÐBÒËkŒÀ—£8ßd—{Ó.éòeëàjÛöÁC½’,ýÂq}.–9âaŽ¾qî[7b~/<S¼ÌÆó²’Lr…Ó¡mˆÿ¨¨›¦#~Ñ‘µLÜZSÕΠT À5†â¾(^s¯N“õbðRÜÒM±ý=ÅîÉE8°öTÌu¼”$üãÈhØP¹Úî¿Ò•ž™„~Y&ÝØT‚‰×}§Ïd¥¼(
+›ž<`!õ’èÓ4n@+ÝKjZ}ã`rÄò1>­ú§×ú‹Ø‘¬…:YM[?=œYžÿCÆTc†ˆÖÜC0ŽT §ñ±òƒè
+áîl¡°ÞXâ˜ë¸{”­tåªàÕÓ"m¡§ýÒN ƒú½£ÊèV5wª·n*1/`\À¥áõUvÈD
+ŒšŸѯ
+ûiÇú¹³˜'$à¹`”&Èv&NõÏi]w ; uLCãó¢!1vâ–œÎÅKˆ¿V)|ÝÔ¼•—VÛ}9bRS¶Ó*¿#½ý}•Œ‰d©UFD»ú"C Ú‰öߺÀXb2ga¢AÉ?wVÃŽ;›JÂÈ/<•UžÿÉm1ïò+LïÈ%6mU¦µ:áâC*±Ö¨¬û‰Ýìøûø
+uøŸ$5ªÃšã µÁ–«Rò
+».w)sZQÖ-?ùÇ,R·øÆñ¦Š5ˆ¡~ËŽì#áÝ(<2înd6ÈÎDZûcì,Å~ ±Û=¨z.ÐoêÔÒ”†U~¶²¯ŽB:ÖÇ]o^˜§`|LZ¾Íu¦²Uƒ”[ø§\G­)‚HxÁò,g«+Q>,¯Òs–ë„uHP€¨¬Zÿ-Åņ^FÖÑ"TÈ«½Ak³’Ý7Æ ,wN&Ù
+™ºâ‡©qTì$@6½y
+þû¢¶B¼ëïC9(·¡p›ãœî wï´xs¯ÿÓ»².?E G!±4ȇxì‚”ÆÚ“ÕN½5€&i{ I©v§ÿÚÔå*B\šKþîYŒ~ÐWê#
+d‰Uo¸.Ü#&>4ÿF°¸ï™+? o#Б˺f£§jì#{Þsö›õx“I˜vHÙÌ.§©Cæ*ù@)
+ «n>À9óœ2±™‡fj ßó½–,¥(³‘Æû¼K+6Þã…ñÔ»ffÎ=Ýò§ž ãäWd„jÏÖ2ñiÂ/X>8 ÚÓ½pݤæØ·Tî6üÀ0õþkÖ
+– éݧ ’áÆ+ŽrSé3˜K³~Âqý® ­/ìüc›ÎM‘0bٞ˛¦¢ÿV»ÔTó+Š_Ý!8Â÷[‹’X§n)@HiQ?ý˜Å‡ †˜†©~V—\m><Ò _½'3˜h šwzJsÛö–î[ƒLôî6\>ìuRK+/¦òˆž¼¶¬…Œä['(ÎB1
+gMÒÇ·kS?~ÑZ¼W°)¹*ß¹³f¹{ _@9åFj®Ê¾v9n•³$=çG—+݈ßÂùBd+@êkas-áìÁ:V…ÞðAü܉‚]Ä?<üo- ›%3…-õÙ-qBËGgßõ\Ï0$¬áàÀ×Ùw–¤°°F€£¥ßP‹‹ÈCËv–PJ“|B"ÒÆNqlD+ý¹tƒÏ§«niž"J1Žü(s”3@Ö¸ô_Ò´/y*+Imym(#›%ܵn°ÏHª« }>=`åܼœÍøˆ->Ü7mFÁ¿q±è5U¦¥Fv£:@‡G­îVB’¿(c!ôª—ÒÛa„e}¾µÏçeÞO”cÜo‰{ò~WÓƒ°»Éxè_>ñ³¦ÅÆÍo ‡ôñÏã2†Æ™ë8f³Üv\«f¯}:Æ]L;RþY‹=¬TTÛíœáÞ>S2¨ôèª!\‚šÜ÷Ù â%|i&ðÀ€àßaxw7æ°@²]<©±š”0'
+xZ¡ÁY§kR Jºª‚Ü–Ñçe¡nÇö?ÙíçH,ˆFvpÜÍ‹V[p¹ÙØUPóÍ%6Æv¯íø–Õ¢`P?ö<öÂ=¾—oЀ],¯˜g·ÏyÚ÷‘:-Üù}"¨j—
+&¡*ã/«àðê¥èÑ%­ìèoŽIˆ¬¶ƒÚ’Š—÷dS;2¦\ÎEVð
+ïüð:Á†u)¬+(hÛbx#´:“­uG¨¥nAžôW¡íRÈönÆdÑØf8gT1€àŒ²æd×ßWQp¸cíu‰‹ožä  Âü†68#UÏb¡ÞŠ®¡6 {ìQƒjÎÑ>zLòQ(ñ#,Ýl4€-à´Õçž-¥ážšfBN“h¥%jdôi¥Þ´Ó Ùû»ø_¡Vg*1 r0 
+Q{ÙŠHaÖ¯¶»`Äm⺗BB³ÒÞX›!G$½–$’úðÝÏúøØ}Y®ß%dV‹¿ú²ð
+Iƒ!ЙªH;éjVàWä)…ÑÈÚSR+¡¨ÊO4ñ½ö7ÈDû“h–H$2<€©‚怨¢×£ÈM)vûPèÅL³Ûbà?ùR-ñi±>xôÔ¤i¤(cð%Úà³×õˆÔv´pWŠßl&·ÈÅì!U(ìð»|f¤xßBåb|Gƒó5,¸Áq<Ø’ y7à^‰šPSÓ µ­ŠQ•èZŠ"©úãÂ÷øÂo¶€3
+0ç|?È·+†œIÃä:6_í
+ ô
+Û!†…¾VÀ$ ¬ ãõlðûqù‡ÔHòEKľ«N† ©pXbŠ:ÌîÁ¹Ð£‡Å\Çj%#‰]u„Vñc½@'©uòšÞÏÂVá§/¹Ébrä†?ö}éŽÐ.¼¿ïqøN…3ofº™€w{?}ÿU"±¬]éuÓs‘BÍÿÀ©T¯AšPçx’ë
+-çjŒú‹Z¨?…+²¢=¶… ¬
+ œÓfÀ? h!@|
+Ú·Ö¿õ¬4OXëê+‘†Û´È›úÉ¥‘“nĵ£ºu¯s¢q@3«d²KP­
+ðƒ!”!Ìtgo=N럠,X‰“_ü‚E£y(f%ÏSŒï<ã})‰2ÎY5€ïì„s’ÛÏl©3ç Á¦Aá¹Ë´`8X
+å³¾lÌæÅ›±¡ï
+Є‰ÔdòÇ*¶6ªâ„SX·ƒu+°&iÝìçÖ˜–áV¿ÛêV°3„\¢Ô°Ha é¨ëÚXÇô+JYÅý % Š9ë45úG‚iñ½‰EJ¤W·1*ǃ…k¶ÂpPvçÀ“°š%
+‹„B¤ñK¼;Å@¶¨#Y¨•‹Cþ'’’ôñtìOw&¾í„x}ß’üæ”…³ƒ÷uƒ×@þ½›ÚwcÅÎ6&´iyÕ°zwhÁ–zÛ?XF5Ü…hɤ?ûÁ@óV™G­Eq$㎌.ëLGÉ9òò„p{K4
+à[«ˆZYn‰³rcõL
+aÁü‹Üúœ™J‹?cKH\)Ÿ³­ MƒÆ£d•.‹cªšIµé
+Œ$ñ¯Þ ÂÙB:<p(ûb6ŒekbÍÐ=Ïà˶¯çÔµ!}¥”ö
+iš®‚Es¹N¸0¼®#ªþg~—!²¡_V†¤Œ# ºB2ÇÕ“Ï<!¸¦ì«¬©ó_ôáGìU‹j/ÃæbƒþuÄä}½ÇI$Ø?‰—-wÄ:¦¾lÄãÈ÷YcAO§@žŒ| öîèÖ_•k]¹$—R òLªì¬¢‰ý9€ì$íÖ"íw%øѽ´g/\êeD/òq8&/+k¸Ï…¢‡èì¥ôxWÇÆæHÀmf×ëèFqKü‚þ«Fኸ¨ùEÕQl jïÕ:¡&J)_XðÚ‘Põ †
+šŽƒÞ8V <WÌô¾¹=Dƒ3 P€™¦¦DÈŸôÿ¸õ*¬a{“4ø%+¸9&jôù„áß¹F
+áÑF<ÕdÄq'®&‘I"°EDuAÀ1!w:dãÃÈ6‚†ÒO#Ti•,4¨ù<ct¾!YvG#=àà%÷àµ~¼låÅne}éТˆÅŸY¾þnUB¦dü|:ávçŸÄ(bÛ×Eb»_<AÀ¤+ŃÁJj
+Gº
+ˆº@P°iO áí§Ú¥I¬BžâMT·Ù`N乊”lù‡”¸X`øÞsÀqº$€?óDzVøþä¶60o,ÏÐÉÚf« C:9ÀT7i‰XqéßÔžMjóó4.úBú³36aD¿a\·#õ¥ÆÇî/±ÉŽŸBê91ùÂù£Y¨„.ãÃ÷5¥펽ddSú^ZŸÛúžg:øŒ¨ß¬Ko“ôæ7d’ŒB-ø¿J[íltg¢\µ€­ÔØÿ¨Y¶6æOÁ6+Äö[wÿ‘7”v´…DC°–i÷U5¥
+Ž¤î?ü¡áèm,Ê>+¦Ûx¤zo6NópÒtÆax–Ñ
+U,ê³Q´Çì’[.ºK¿H§\Góü«‹Ø«ÅÇt‚TãXµü ²_cq¤sßà¡ê}ZÓoS$üi!ŸlÂð(tÜ”œ4ËN»i/•¢JNµö9HG™©G—21?M·±îu#ý77W×mÛõ‰îÆXĆT]èÇku‰䘭ÖÒÉf ´Ð½Õ2p­ÿ@/óÖè‰Söz7æ¥â¿˜/1iÙ®¤´Î¼Ø&¿2J¹?›‰å@¦…tÀBiµÐšä¨€üÄaÉ y9Í—‹M-úx‚é¥ÇáéOŠ^-g¢þŠÝÃôE}雯n^
+gkçXZM5W "e‡ÓA>(¡ÏœûÓzLjWÂIV ŒŽ!6æžËw†Ú`tð9–åB@gI¾’ªºM°_K|å÷ÖåG˜tŠzrŽwžA$ÜÀ-Zº° u
+gEz«Ó VµveÑ ÏÒ}-`ýp>./'y¡œ ÔùïRZ¹z¡¥ZÖžrI…¹W죧ŽK} ”yö{nÌOOQñeyºÔÈJ“°ÿ¦>7õý´¶zQzD˜ÎÇDÈ WRé&þ©Ý ½áÅæuiBHIÊ”’T¢Ã
+E#ŸðXø[¸¸Ç#Ÿ§U•{|(䜪°®54T’6ˆíœ"jÅ­˜#¦ZKÿH¢X“ '–Ù§å!eµI…”¬_)ßfU«WÍ?ãHŒ¿ê¸b.?ë¥8Ç$C7u¤GÈØv‘ÑŠW(¬‘6c«¸H¸ã’8ÁŸ6$vPÓý Ùx¥¸zÉi<QQ´ƒÍ…¦ä
+ŠùV3š$óa%„LæÛDQ‘ñ‚1¦™ÅÔâ)Ë+‘לÐc¤iPÆ¢ŽÌ¯˜Ovð¶"JÔ?æã•g*V5ÿäBab”É_§é„Š*¯f¼Ì͈ê×¾Ó¼*ŒÕ¡É:"ÎMy$ªã¨@~#¡Úë`‰Ë¨8Uœë QN®©…TL„üŸˆHpf̈ée*FÔÖÏH™% §¨*Š¿b¯ìa5µŸí8VÍUWUž9ŸpÊŒÍjæyU*Ìs0´LËOI¸ªfÔ”M.›"­!ÏAé¥8ÇSQ¹Bbfˆ\L¦N]£2§˜
+!´‘$ÏÁDCÔŒM"¥ÊvQ›á—P…ÃÈƼ%¬PM¸*%&{¦„—áÕx„6UQMy†v)<l¸hSÙ¢ÅÞÏA‘^[Ê_¥ŠcÕþ¯†ö\›‰0ä6Ðå[à ¿!Ñ;¤‚$¢ÏLÑ~¯ºàûï©"â&úˆa^EŸ8Ý8ãTaù§ª$ÜX|öÔªñ©ÚS¯“ˆï\0ð!¿é¥ížaƒ}èÃãnìj‹M H…).V02ÊI$Œ§3®>ÍóжàAœLˆär†_$!V$ùžRDEŠNU2¯ÊM†”5&]ƒ” ñÞTféÃDóWR‰¢‡œ&¤YQÆ„ÜG^ñ¶"¹ˆ•Ï”Bg
+’ñÂ"¤ö•„\¨JŽ’À )È ¿–„h£ùØç¨Ô‡n⭆ʲU\h¼7>î4΢Ü1y©ÎѼ”U˜$(R©‘„­ËK6¡fÁA#*’aYö+ajÒP„BŠtScU-K¬Áš£j z‡{D]ˆ÷‰Ï«xy‚ÞP}«;5š'¯ÜãµE‘( ×Á7¢Ùä–!Ç×?÷a–ìÓ):ø_!#Y(Vp@D!Šw¦b&¡Âu0ÜÂäO …̧µkÕúáO4ÚÓ$¢BÖ‘6à ÍC^¿a:s³Gˆ«o2œT(W¬bøÖðMHNA}/–…Êv™5†ä1;崑ĔԌs’™PY%d1™‹LC2’&ùŒäm–Žp"ÝI ×ûFà6Š<:o*d])h\kO­‚;!ÝDËq‹LšÝ/ÅçÕÕ¢xý§ÑiŠ¯R£ÕÕ¸¸®½¤Õxˆ#¬ªFfZwIô=ÁJœ¾ÐOê)ߌxLøMdb r¡‰ÓHC=‰—7™ðèQbŸ'«‰W¿½£¯Ä÷\L1DÏ;–05²tNŸ0ŸZ0 âår+^þ_n±Ó°|˜b@õì—6/;¦LäÀüÆië„]ˆ·a<^¾€ª6µ0ãsqƒT€Á Ð  ‘è¤qTåšhžäaK‘c"[ÒH´©¬Pâ¥t™E b
+9êeOI b­*CĪÔrþ¶?RÍÌÜ4¤EÈÖfLÃÊr¸t'PÙ±eã@bi×ì@åˆTQ›Ä)âΛø&V"œöŠ!vùű¥x`SM¹¡í’¶t\–¸eb˜åAIÉ'Ij„“£ ‘ƒÉƒ™ÊŠ–µ£”ÍÉ.r¦e—œWðœ¸73p<Ó@rŪ²e¬ØwGÛA}òôx»VßrÛòP#÷'äK8Þ5\ÈäÛéšÉMæ†Â
+òW îƧ½'”"è¯üŸŠ‰réã»æ ¬»X´ZîáØB•0¡¥¾'« A›Øiñ^„:¤=Ä°F»ÚÚ+«Ò®“Zz$È®’ RJhL*¡ÂÕ‹­ÚÃÊÃƾe“”»"Ðññš&”Œø^0Pð w«FhªdÈ:ý‘,Ü-MÜT‹?qzU4å‡úŽâ}K0bŸ‘™ŽD|‘üÞWêÄ7§PKÄw[b¢¼ÍÿÅëöT›8´ÖP0ªâŽÅâ1DÍ‹¶VŸ,N‹Ó>ü8^’£UD„رkЄ|Û„vµ,à”{çº-¾J±]Æžœx×ØÛz`ÍçUþ˜îÊ;›Å,ÿå©@²"ùÜ ŸÉtâM±6 µ›‰½ŸkEå™ÎMsøç"¤Wƒœæ.‡lNôùPýµ©±Ì‹¡\SÇÞ}jæõk¾¢5c«ÎW?ì-ÎgÈ¡>óSqî.Ô‡8‡ÈCïÍ5Ab%Rœ9ë«ØLÅf–áü³ QæÑ?„lzŠ84Òàe^b‡Í—‡l¡„e
+2«˜GæIï¯!—ÓWdYɃt”W¬å*åý$x£E̹N© ’Ï!!6E¡fìK%³ËIa\Å÷/&Ré&4r1ʹœ\Z õ/ê„âLãu”S˜v
+¹ÌÖQz‰v“φ¦MN±‡áŠ!¯|]\L†¡­ëtVyi ÝêÅ¢`íÃÍÉ°G‚?¬rþ!'F_}æRñÀ˜´BÌ8#97l= ÂMdäûŠ«¼dºÉ„™É'sGI™ÌÅsÈÅä01œ×Ìd\©n›û‡s\ѲQ<ì
+Qk!€€D X
+0Xuœ`Á+Àà@eŠDVÁLHÌV ¥ø¦tJxre‚Ö/|#†õ>ÖÞIZÃFÿZ¹@ê —«5iHCèdq|Ô˜]$òl#VE‰øÕ)L´šh<äx(h$ºU£•)¯GÇVÕgX­ñˆ…PwvÌxÐÛmîMƒ¶ù´±ñ‰¢j¯™„s±˜%îÅС,£""ÎÛT&AŠm™†ÅØ©L–®ÂH¬à Ù\j:W…‹^Á*ÊÁ4xÌPÈå}‰#‘ò^US¨–’X~)ªðD'(¡âô€äAcÉpéx7¼Œ#ˆÃšúˆ"l<"/2%â'¼”H !¾(=1&¶-AY-%ÕOyb«âI¾°÷‡B-{±°¦/‘_¤øå|p‚ ©†Ê½Þh¬83F„8~N«U(óGdø‰±ó°)”uB-Jøbî
+ÂzãþÚcL¿8Ac¹:Èâ`~å?¨!ÛÈÙÚÎ㥉<NëÐ]îA*}DS/Å+X¢8­"4óéÇ(YM£<áâ÷¬TFÕ¾êÎ"–¬Ï^Ú4„ÇÛ…º–EçZ1/ÂeþD‹ÚF&ÊÂvœb ]Nóˆ$ƒ7‚q™?¼Þ]¤ ë¬J7Ø€(܉BSBwå ÿEè«0Ø|ê£h8¯’¹Œ W,Øe?Í~ÆZÃÛS—¨’@žÍ ç#Š@Ÿs‰Ò¦"¯ÖHi»I¾X4¯$±ª|$.Æf„8°à øM˜Y§}Ø4<¨}†Æ&»¡™ÃÖ5‹GN•­ç@¤òòØ` Çíñ
+·k#“âÍ™Œ³"Ñyµ²a6
+žÑ¿m­Íöe.éìàrÙ&v†Ë.+¡ÙS™CH&S™e«Ê_!c(;¢lN±ÅChîiĹPÍYc Sjn‡ýyÕO,š“ÔÃ]"{†Þñ‡V
+7DìàB5Ï\µ½„­5Xáñ|î¼(ÄtW%/ý!RçY:²)¹Ìcu@s"äªCÅú±lv@«E‰g¡fÍ;#ØyI,ô¬×Ÿ×Œ‚KdRŒM·ÅT6¬hVs…UÄ>c:=s|Â|JŒêà{÷um±_ÉâAꟌlê€N¥š n-þMè@öS÷^Ìú&ĽÜÛƒ2­h/Z™Zº›©Úˆ(êû‚$¡4‹‘P›Ï«†ÁÎ4÷¸ŸCp>mÙуïhö¢@4;—YÈçŒlȾŒï„øÃÍè3Ò¤šÍ“
+•¤±aŒ&daz¨Á({ħ˜ql”V}<Œ@ ül.JW4¯e¦˜{jê™"Æ'<V33Íç5åúÜÅØ9ã˜Éf¾Êgää^È\V§e1$ŒQ^š _‰ƒ+K»‘gôÌËci4F-RXÚ¨Ü[I­ŽÞþ§¥¼L
+ãfù†òÚ.U¢PÔP¾ÙœÑŠ\s¹týX‚ÌNÐz¦ä5å¶À“'A8×´*ÉgžWÒˆš[èÁW…z5öŒhŸrçÓ`ا<¿ç­ñÐ#¯­ê šùtæ©@1»|ê,JrÊIåݪè/9g­[ºy/¯jKLÊ9Œ†(^T•ù9¡É!ÓÆ<þ†En„“á8e"(Hw)nÚÐ}NR«pPKU·Ä©œ¢¤.
+‰ñfe‘FõQrPS3*ÉA Q™éñÍêSÓ—9;àI·75w”nÄRºj £ñ–%n¯'™„ù>5?'D¹T”W‚ʦÏi”ê‰í¾Cr¨*+Þ²=ꮼKZŽ’0Ë -·+”½$ˆzÚ-Yœ‰ÒÕ¢ûÎWª=eÔÐpÊÄhõQs“r„C‘‚ÜW?—"Y_õTE{ÙÄ“ß)¨$Dô¢Õ)ÈÊ?¿CvðÍìÈÝà·»ÿ±§³Ñ»chJob´ô˜`£¾òÐR;‚SbÄ·•\9Ø¥±Pðågˆó$+:濉&¾’½Ô;`T‘Cß­—ú¢'"EìE¯Ó˜‚œËúx©yÄ|亙"ßì…(±
+ºI”ˆ¸×‰+ú¼Ò<?»ŒÅ,vo¤ÊѬ ÝÃ
+…mŽùøú‹aN•÷EÊ(Ì—¼¡1sÓÐAåH\¢hdäð#+Q&Ï–bQKDĘEªDvpYÆy yKŒòȤÔ$i¦‹Œ‘`ËNJ§Íò#ìi©q½$éåAvs»“âcrËxP¦ÒR³«ôrddoœ˜ùŽä"ÆŒHÏÕ¬꧗ÖeCŠÐCÜÐ
+Y¤}(Ü“ˆéòú*ÅŒB)¤w²(¢ÓÈÐØ¢˜\.
+3—2•%1 eXª©úG*`±4Ø‹…fOB\B.OÉn)o si][_®ì_ÂÏË
+?ßHbä1ʇ^÷X«•P‘Èc”¡š5ÔHÒ¨›Æ … j} %Cv”C¦¢tE¹+Aáé4xé‡o ÷MŽ ïÁ”áøÄh§–,FïDyÒ–´+ l}ЄÁñ†Ä69¨#a¶ÒÊ›<Vó`DDf¦æ ` ^áN!“uø}§ÆUC>l:¿ôAQO 29²Qƒ"bUÄy>±šÄñ'Dþ„ÅÈdÚ<ß)y뜎ˆW6HöJ¾Ä —![’š)•GuC­ì­#:d³®~…RØz[Ü̮ݹH z<eɃ¢%‘T.§òô|…<±¿R)+ÌŠ+Yé©žbR¥r¡ÍºÒ iVç<=ºv©+Ô×Åž— ›QW{À! iR[,’ÚÜ_òyÓlÈd©$Ny(ˆm_¦
+q*¨êbf]ѺjAÒ2p[¢–8ˆDGŽw¦Js¸ü#èE"K÷Ö9TJW\”ù™0)颊y¢DâYÏE<ª
+#]͸ŠÊBQ㢪F˜¢ŠòˆPJÊT£ÊTp‚,ž(ó­)ijEÀ@x@lW¼Ìõ‚úšíA«%µÕ)GåAl{0U™crxý|æt™j”£f©·¨e—r°º§©”CÌG˜´%¿¬BÛ¢(U«°eáMQN*h\c±Oô–×¹lí\¥Ñ^.ùuoãºé€Öª­:ƒ‚¢+-Á™‘‹1§=d>#C¦*ÚZ*" ªºLQ¥r…†HP«BÁ*÷Õˆ{F(„DðÀ!ñ÷“Õç?M%gÎVIÈ'âü8#ÕEl¡ .¨
+½Ÿ]¢Zy Eå  ¹Y3’*ámáF¤ææ˜$&‘^ÞÈÎƪ S'Ô*§ª­x¯©(„¦ª*¹p[®ªË"BÝ×Ö2d™xòBö¤RdYx»FzrËè’ÚªºÜ.Våãa0!¤²I®÷vËââ”ĸ1Òì—‚T…:Q¦¤ëcQ£ŠJë…„,u©ö4&F¨ö”ÜÚS’2o…”¦î’©Q÷Ao×ç5{µXYf ?QÙR‡Ô0^B!OøZ3äFî¸üš<ƒZ]\šCj5iZ1ˆF\œF¥æ:Ù1ƒjâðO‰î<c]NOUH±*#Ú®AÛª/«@âÔYšùL8‘F†óÁ{²Ã|h Œ¨W6ŽÜþ‚„Áù ®!©f"#‡(I«,"Ÿ3²Æ"§UŸ)ÕÃÿl#Úw»‚µêÎú ý1È
+–¤Ë$n4¥CÆý‘Î÷/,úH|3Ý!»=
+YåÒµä䤦N“yù`TÃSé0>àÄ\âÔÝ2 áƒ"þ¼ë®‰b¦©Ë>¤fsÅà—ðíIÑʃ
+ŠV·SìÞ0¹äS:¨P”Ü–£—«Ìi•O¡t@QS¹?“EU4BòO ˜< : ÇJ,š(;3s(ìQÕ¢ƒ‰ÎÛACÌbÅ°Ö`T”¸LAÆ<¨‹!|À‘DZ§Eõ­¡-,UQŒˆ*Îa× s äUä (öO‘Üɇr¡ÒqD\÷ÁEAþ7ƒ©ÏHÅìD¹(Èͬ¢@6æU#?ˆ¬4‘“M™6L‰ɱ9 «kî`fõ
+ùÜÁëB#å׈wö;°ijT’W’æ‘AõãÎúŠhpÚ†¨Ú„PÅ)Rv„GȯHˆ¢õ€ä W YõEž)‘Ä™NШ‚AX{À2RªéãJ&TOmjì±ɗêÁ¨:>Iv’dVnM¢S&êÁ|V&_ÞKÉe…¸>Äbp4¸h寨 f>uÇ“ {XC”=ä±2fP5¹„ÜÈ’xÅe¥<eÊX.ÑH •bHRv«h⦢ȱpO¨’i$ÂÚèP±äÜø¡(¤¤ŒáY,+W û GCí ÈAvIC.¶ám!/.ál4T ¹£„".ãŽh™à9¨Ålþã ¹"\.!³Á-!OmB±ÁsÀù†v0iÚ¹7„¨Ø±Iæ[ÔE¤çD…¢)‹=I‰]Tµ»¯AJ{ÄóÀ#q6•ä°:ëÞÌhTÛ"õ¦D• Ÿñ@>s-ÄpÆ)U"˜ãÉ"殩wÏÔÝS0ÔM¤i¦0ÔÐ= ŒëUU¡ yn ˃q1Ó„Ž'By0ÅP´-[ahQã;X<˜3,ž AŠ…Šh¤ê%÷¤4õÖDˆúöM½CäÂsNÁ ²ú–qM¸!Zø72ïwðàô÷ö†´f\%¿ékÃþ ‰ÆŸ›R8UØÁ”) 2~„-óÞJ†$8ê[õ„½")xVô„W-ÞÃ*êÁðW›äây’©|qV <KhD³p9á'î¡—#já'ZѸ,ñ u¿VžSÃωW~±ìO†X¦óS—#Vº ¹ŒóaµŽn!/K‹‘TYªTUfr*û
+2ÕĦÊu;¶³iª\K§Ê¬»†N$TqFK–Í
+‡L 2 et™I„
+–ˆïßêVQ-äQ—q…¬!´¢Í±Vt–Ì\N+â/©)®þmª³¢á»™Ñ‰áÄ•ihRn„è^tÈ$ ùPªh¥—<BU]gH…áâ³û!9¨NÂp"R©>Úºç(-¯TÌ-µØtäÕ,Îâø‚ A#‘¢Í‚üo¦¦b&nZUh:ñ*VèS!ù‚^Ò† ¯—Nj ½¼š 9k¯¨1F(²aŠƒ>™ˆ4ZÝ+5jÿ~¨¤xZu'DC2ÕxFªþIQ¿Ž—¡ ;.
+¢$Ã$\ÔTÉ"HŽ(ËPÝŒã{/¦ÍCRîUI-Ä…ç ãLÈÌ1„íIÄ9î™}ûâAVb*ÉÙþ¥)é¾K!…ôC $££¼ôº+ø­"'q›p³›AÉwPBNõjå€.
+M•.É«9CÅþõùô7©I2RýõÍÁ"_ÿˆøQD¦¶%t­ý8Ñh,ò<ÊÄYO›§·+ú§&ÖQé1F ñå“Ð|¿k•F?3¢‡BÓ|DÜ=œ ayô¢6±ƒ} ò£äKÈ£þ§ÈK…!‘ÜT,änÞ4Gl8$5“ÒhûÌ}×·¬Å+¦n©Eœ_É!½9"C&,ùIr”HÁ©1[½Ìý¯2/³FHOœÞЇrÏÔé×
+}Úˆ}:§†;}o‰)Ä>¦Ü˜‘Qñ}ÔÊLL)HdñyÕ‰j'¼*?ŽË.c¿*wH‹¢ÉWEnA§^“°ì”ˆï5““X$äÐTÃ8k2§™ñbpð²ˆýF…¦¸Â%$R'vIRg&®ÊL§YåìˆÜDœJ3´q*Hˆèsʈ dˆhCRv´A%žßÚ¤Ò* ŠEnšŸ(\ÚMa…¦Aò¨Q!ôJ¬tMrPHÉMò\3“s2î$ª×Ê{¢g"Me§*5ÅP;!k´ˆNa)M«Þ‰}´Ò¡‰+Óñ§}£ä¥]†E
+u­Ù~52ÇF{ÚI bíùÕÐKZñ³/’Lk_ry¿(¹ÜŠ…÷¦ ¨TSkâðH,S„r§–„L}Z’ ¦(Úân{Œ…’ÖFC{pÓû$N†Omò@æ«2I‹ÙC†wÊ&ò • KXæ{/–òµH…OŒÙœšÉ2ø¦â¬OHPLH¹Äó´Q²ª†¢ƒ©éN(RS–£Ž;êžÑ2Ü™£>uŒð2cqZUSÏ+Q׸°S ÚLTçý¤¢Üƒ™ïíbœͲJÖ°T,UCUÙ¢©¢ ë* Åž¬[ÁVM§¦%PÄ»èˆ…é •×£ª TTC¬Á(OKA©:„+#^û=½m'´Õ ’Š­š«¥Y uŠH†:SöGžim†ÊÂM¹‚ P:˜MõB£st‚R
+}„¦B_Eùc$y«ÿӃr`q)Ðã> 2¥(ô‡è„Í"ó®9=(¾b ¯ŠLñrPŒ>Ëç‰é`ÓêÍÜõ“Qºæ&/ˆh4CŠa•ê\QUˆÂ‘¬EuP]š"ßíFŸ"/Bdö‹´H®•bâ’Mš®’÷¡  ImÈQVø:Ohä%kÈ´ÐEZC«¥"”
+Í¥¦© q†\ÎÅŸ[v‡*=`_.K¬Å~ƒÞÊ™%£xQÔüf‘Y1$Vê-ÍJŽ´U©\÷€%T¢ÆOKÕmrm.ËSéÇÒ¬R¢Ñ¬‚ïò«)Uöb”4V_xõ‘Õ‹ô
+ ù!!ûJÉR¨19d%£°äõ~T•¸H]yú쇥Ìé2®P±Öë¶TU¿KböšÚ`ÕLÐÚñÁ}Àà‡ÈcÖNŽW„òÄ2Å¢zK#T1Tµ±âèb4/VÒa‘Tâ±Úlž‡G‹¶ý©Kùئ²DºDÂd¿uP9ΉËáEÉË!2ÙÚ£Èôò”žzµŠ%fѤ&QbC¢Gªå—|üjq<5á!ºª­ðËBWƒcTxDF¢,¬—©BED5®¨rÍKÝRTUT¹â dÒ´UsDîz•Ãr
+‹ÁÍxB›8tK¢ çMÂ^íU.-D2NË2ΰ«Š‹Ä¬j9M‡]QV–¹xHd5bW•¥.âXMôàu1¸s’‘aÔŒ¨“ôŸXú‰B•ˆ¢HoŸN•¹?ÕÄý@éAiPÅ”CJ.oä`%çdt*=Ÿ*[Ê( ›Õpj vÙl®Æ¤™»ŠÓ˜>¦šdL!Ÿ_3Š>¿R¦Ù– ¾"š©*4QrÌ¡S<sr‘Šª­HCE³Ö¼ð”_†Ø4i9H »XùBëÈ ËuÓˆf›8GFõÔp,noXd©Ð„£f0ü$ŽaCDä3#EÇ(b¦¢HŒWM6šîÀE>Å]üXTA’ŠK¬Ómût­ÉÙÚÁ„$ÔЄ¢0>?úOÍò†ùNyõÆÔA†Ê(‚ŠÎ€ª,%379 Mt|ª$'Õ&jåƒ æwŠIëôª+Õââ=^ßUüb(Gtæ¢Ó=8©Æé¬ó«xPÜtéÓX­¢ƒh5©TÍѦÚ{B3Š¾2r–Ò@tPOH¼é‡ÑAo!ËkÕüÝgCnØ¡Š0b&ìM™ õ”:‘·+”PÝeNTǬèÀzzP¿ÌæûÀ5U}ΗΈï%áÂMFv@}£ê`ÐD/º$½ƒÖ“hF¨¤C•Þ\^;xXçÎïoâyÃŒÅ~Ÿa¸ßÞ΃ßÕˆ·&Èí\[x¤šÔO|‚ÁƒGŒ×„¤7¤ÓT¯œÁcœ´ï Ò€uYÜc`¡[„7}lXBR#›Ü±Í™ËAÒGd$öm•áÈcT¹.ú†tÓgä€/‰)úG: Ò=”Nhþ˜˜T,2Ñš|½!f1‘0"ºBßQ›iU+f§ˆžög˜Ž‚öˆMÔ
+;i±'Y”ì±0;Žãàj—,î™f¸Ù™*,Y”ß´bë[”ânûc;uÔO©ºsRW4bYÉVÿ¾J$¶ƒ$RI¬VB
+y_•Ê¡Óúù”qô¼Ð]Ò)¡(M }K+£N–ORÍñ¸G!bóÑ"¹Dô Šèý˜¨tB"â’Ìf¤LZc*žWG¨§shê:êR*©ã¿ˆ%¾Oî?þ‹%_E=Š‹øÿ- Pt0ŃÆZᦩò(jÊûM=`”ýªs¨4Їæ¤Ç*š
+ý_tíÙÄ ôJ9A5.aŽñ*­´7óU
+“£êç¸Äì~l™kÒÑK´Êü,÷JJMàµ!“Î ¡™ú£ý`œÌ–&Î r'£VTU7C¥ÇÎÍYÓ³çÑt÷ªÅÈ$¯ð ¿åKpRP [¯bø5Á=ÄjÊôL)·Àž"ò#.†}3I±á÷O‘g¬yÙøf’$% µòñáe™f†ûõáž+kdÇfçMó³R9(IŒÜy3S¼wöYJ3Lëݦ]41Û
+9ÇÇ×­ÀÎY³ª:ñ¦XÄk ´ssI¦èâµ@æÁ
+…!ˆÑžõë2®Âg…gª3ñ.¬Ö]%j5”kµ…ýÓÈ75—»½~Ç™ªg¦šÖÛ«CïUfYhOÍžhÐ<€|Á’B•z妲õOOšý˜_ÍšÝÑ;M4wâ¹ÒÇôV9l:ÃâüÇ^W?d1@ñC½û`ÊPK»á>c(è² ·d»ŸáÁíÿ€,xz<‘ÇyÉbÈ#˜HN§
+c*/½k™ªF<€Z$Ž\MðZ 
+·‚·‘—¾—¢%—ª0Ñ0ÊÊsÁÎêæ$•¹¶9/Ë÷UíÛx z«doKÔ3–»>§{CàŽÜßb ç…~:XY¼Gé‘ê壚+D–w®ÀŠ -`”RÕ8è÷SšO'ƒÁ}ˆ} JÏ>l,j±uye`ö^y©A¢f2çµcò½Êa¢8DõÎñÌÐó¥(aQ œ›v¡¬w»
+‰Á8›É×ÍÉðI;xG©åômåðòVêž1Ídâ®[Èzó'`ÀÜÇûth÷Ñ›„‡£‚¥%æ¤rQÖ‚Ðï
+)Ž´Ëäí·Ó™Çȵ!#'4,ûÎ{éõÁf­ÞI1×i 2ý^üá¢[¨0&ÃÔ$ÓŽ\ÂÚÒ
+Z!
+QÓ«ËPV·ë=S@ŸÃSjÇUu…º˜Zém š°J•%“A[•Ç‘ÑÂj ^œ€­ü¹ùµ
+lZÒdá­¿‰J²«É¶Q²„ØzÌàŸÚAIÚÚµB$ìÂë ;²iÄð!%¨ª@Êóê%žùÑŽST(äX*ÝÏ /›ž¾¾ký›!L¡šaa@Ž‰É¨¼¦«7ÔŠ»($ÛOÅHѺÐÈ€ ™†ø»úS
+–‘ KÍ*ÓÙ>m ê,ž¶KfT6ž4蟮m"…~Ýýøx!#``­·;õϤÙ(x
+âo;jË“Uƒ*Ý›nTA‚QBÓÒ¦¾".9 ›ZCé
+€YhúÊÛæúLåX[Ü.Ÿò&v0O+—QKƒ*çí,>ܶ“ö =
+¯x±DiK”Q1¢Ñ.je‰# -ÝDa^䥿%2ˆs™ÛsT_lïÀ u ‹5§bªS«Ø_ü¤!U{ª/r”Õ
+µ‡á¦nã÷ÑË
+Q\º¸þü_M( «Ø(¥ld]µ©¡ 4{ÄO°#:D O*ß„-Ü89’z¬•}¢Â—[&Äâ=Ž„ì¿æ®j«ú£éô‡c¥OÚ»Ÿ#~V¨nö@¦“LIðW UÆ¡Ë7·ÔGàÚ D /’BsnÁ0ÕH?Rò"ÙYè~ Ô“0DÀeÕW U~Nû`ºÍŠîNP^½Åo¼†ã*£×ü„yb½4`ÒÖœ=\ïXÈq¹‡<=7([
+n6ÙÍ!$O´2£NIK#¶@/úEÂûM°QÙbGäO¦~œ";.â†;)ÍG¬+†•jX>¹å ³$1^¡¯uu8U¹®/nåN ÎpÇdåÀ.¡ÅhK>uÁ‰ïo I+$bŸ“yÿÂ"Ѳ{ÑëaAÖ#¿&ï•»–…ÄMí0xE–0§<ÝŒ¸òo¥AA …üϹ“UÜð$ò7<¶Î½7¤‹uæ9ŠoŠÄu¤42ë¸D •ŒÂ´£v)£@ïÁb +Š=+#>=Bª:Ÿ`ˆD)6pà)¬×BX¸PÀ¶,†8èOKg‘ƒ)ßtÂùJèô3^éURÎfTJ-á©š,켧×8õï£î÷¡€Œ?º/þ)áÐ;cð0þ®K›¤ä«aKƒhô·Ë˵,)Hš-”ÔÀ„<iü ¤šç>»†$ŠÏp±Ý.nÙ †¶±^…D7 Fô¸±ãÂV5*#{ˆøïÿRÉÄ•VÐË2Biv‰5ˆåsœh¹Ñßî¿È§0(X2t†ÍGf@Tõ¤IÌX©šÚ²ÿÆÓ=¹‡§ G'bw&€„t°5,e‘kW?XoS«.K‡@NµRi²$ÈhåÔñÖ0äPMoÓ'"{I³,/l[°¶!¨EaœÌ5¶@G8+Ô0G OuÜyJ97D—㘡ª¼
+-@*9!5^wç!w Å-¿AⲺåHZ8^¿–žøj ü‡4Äw§”ø¿}óu ¯Îg³¨°÷sô 8l}N7‘¨£$íM ›Åk웣rfn
+’8ª¦X…
+ƒ |á²òè/«G âCúï|#ü–¦E˜@&=rNki¢Ú?yí²6ø8ð<¾Mþmc¼zìeSµ&ê´ JœÇvž²©-ñï™"@çÞ],]Ÿ’ê¯i`­&ðE«¤£‰!§úŠQXÃTŽ¶90¿ïŠE„v€õ½P¹®ödŲÚ>Ù’4Ø3á#¢iõ FéçÃ!@¤ÌLâ%†>çj%î
+ej¼HMˆ¨ ̨¡0{ÛH9{6Á¶‚ÿ‘†CqÑŠbAC¨Íx>FZ°Ž95ë­~_ÒÕR¿²X b†ª@¡øHR•?ÄUã5BAó!èbËPEŒDhχ®àu.D5‚$½ih›š(lÌ(„
+ÚRÇÒs¤™Ì‹ffºiÿ÷IÛÕäï‡zý"Š@<¡m%]ù̶Âóþ7 YcæÞ ´Š’$ÑAå%ë G#,³E˜Š€±V?f¨)àµ' ê›n…vàñ56ìmòìÉÐ'EÙÿÕÿÕ¼åçtÊzÒh§ˆ¾íèFI*¬ÅcU7]µB!véôW혆¦ à"¿í½º-˸üzѺoH5@ª©Z¿yP'aX_t$ë®>ÊžîYÙu£ãºbW_H¹nå#Ú.–óyc ‹8÷Êt‰ñ$äWj“Õ!$¼!¨ñƒÖ¤$ŸxNKAùADB/ªGR×3‘èx3÷4%&MÕÜ+ÏùV);‰Ÿ‘I(Œ8ÑûVÈÓPštQ«]/0z9`Ur§Àè<¶KËÝ·ñC DUT Lç-ü¡a#¦Eú–7#Ð}í‡Xd»VRý›ŸþáÌàý:…Šñva@MLdhÉÒ2cèK^0 œíL©Í Ø‚‡"7•‘bj9ý"ÜÅÌ„T?ÚA^eoXs ·¤?P·™¯É¦RŒè]Oï}¸p´ï¨s.ýÓm &ðA-ØÌ"-~³ÕY·'Š)ÙÝ6‰vÑiÁ8¨š*‰ô-ŽH¼·^{HÍ°H’˜Ý62°@®wFâ9^!©’¾‘y0ˆ0TÂTw´ FA\6¹ucRZÔ E*W(øK‡fÚxZ<Ù ¿)±Fɦ½!1‡©ú72Ö‰xÒ¾ÉÖ\4[ ·§Ú“@'‘îB#hÝ%»‹5ºM¨Ñ¦¤¹4¹·H ËŒ>†éÞz¼)ÖƒÄÑB¥ –?“¥v¬™ zT«ˆÇE¨Å0©‰ß§¹OUÎm÷…Â&§
+åвK(J¡âx Òir¡•³_®?ÊÚÅŠâðù
+Øç+B}ÔBuçÈ¿K¦dÖçuêel=ņœ¼>ú6o·"˜À[ù@@iÐé$ŒèCè…ĹSlØ€– xU4c"¨& –m·øJŒ'. kübEÉàÿˆ
+éÿ˜ªëS8o¨@YX€ ÷g_ìןpãIø—vdóî“Û*6r{™C|ùBžX€&ñà¡Åέ;kÌùøª[/o @s¶×è>Ψé«âI–ošöeìÈ“Z)B&,4—¡,@œfÎÐù¤›kêî-ëXú'H~ýÒ ý1ÿ¨gÕkK
+Ú^‹S䎚¥ö2(RctUJÝ#ÜEÒŒ‡ Ï&è“éÛÕ ïÿKÔö¸þ[ÿ?´«  "q.¢óÇ¿WkEVVCž§'gäÏ;$ßÐ2¢û.
+2Œ¾".Ü)þYëpÖâÝâÈÑ£ÜÚ<·Fæ¿Ê¾Z®Õ B¡ÆÖo>ñ¢€‹vî4£ÿ/—òÔ³QèÅe0ò“Z>¢©ÅçLƆ’{ °:}(&,Oèù½ Æ·ð”k¹[F!$§"§Œ­aûÎ;`àWvwmí…€*’o²td~½öwŽª}ô YGîÄ-„£Š“ÒnYWÀÈñÙR=M’]pš #ÔbêÄ
+DêÙÝÈÑÄ’%øjšÌÎåûYôÏÞÿÎ>!‘±,g¨ó7FÜ ÛÀ¹šÿž­tv£ká·;üYM,=2wv3ç¬7‹*p3{pý^GÉqÃÂxM¨Ÿ†ÆLÖ@Äõ? ÖW3ÐQÂGfÙÖ³x¸²„éw¦~ÊqáPÁ |H§835TG
+¯ƒïÂ2âÙ¬5«dƒºÿF{ë…”ð;ç‡}k8¨Ñm—Ý
+Ç£MAí÷ÁPZäªÀék*ÃlJ†š«\£š]㵫°qŸ´ÜÆkÞC€‹ò÷mžÏîmû`>&‘âß ™`q¹iëR^Ä°KR·&RÀØc쮞¬B§e/×Lð'#Ê!‰QìFü›Jæäh‰è-"  ˜¤ˆ‘Wðžä~fíB’q>%ó jÇõMÝeYõC 4±"Þq½ÒèÚ>©ÃâÚÜ‹ŸäŽ#$:ê  ¤“p5H¨Åé;»`ÀÁy“ʵ]kNq§/¶wšfTjcG¸Óø#vò®¦ª×æpŽ,)înSf¢ÊÌLEç‚/†;</è1”>… õçä>×QIå݃@Ú?… BxI7†¡ r>J
+ýOƒ†õš”žÙæ ‰‹ߊXCÃ+‚õNî}3Iøù¬$G­2}¬Á†SQ~ôdo¿¸ZÆÇCXÕ‹‡HÄ?¨8ó:`×^s,©V±hbþ7¹yÁõ¡ýÔ•Ù
+œ9â>ÉUûü >Óô4w=1W)(+ÇôÅŽÀ¿
+{È@å´gÏ=bÇüÔ?°¤ò$:‰:fÏØq»¾Ó+ÿÔí"K+vÈÿp0ŸÕá
+/²¤>úB_Pœ&.ìEë˜[d+Í•ªÔ•øêцh5v Œ UÃÁ.Ž!o
+o{pŽÒ.6so˜¬ö§²eh¶Ú‹s±õ ÇÓˆ ùßû¦ám6>(hòŸÎ¯$p¤B¦§îÇÄöfЩpü4Ÿ‡‰›I !Èæiçœà†p}‘5LtI‰ "TP+T~0ï๷ Oaâ|èþF†P%®xšÏ}ªÖµÑpȺád»“fNTMê¯Ø‹t˜¤åf\;ÀD}>F}ì_‡Ëâî¤@2h)uJÞ<‘p+dÚÆÅn’ø”u 5k°êzß´”SƒfÆXòXÂrQDÆFëÍ/7Z wwLØô(„Is “Ù>Tß<„üPegº~ç)ù£}¢Ž.ãëí¬]ÐÙÇ5Ú…¥訓[
+®Ô®(¨s¾)ÛI£¤;© ëŠÆše…+z Gqzé ïžvv¢
+Üã¨X½A'TX¸ö€TÄ{ .›eêÙEgNWŒ¦nÀFÓõa
+g?ËCæ[S¦Žˆ&ÜA8ÙŽ¿ãEU´âE–朜íCïì«gÕpADÉaî…x™ãVà<ŽPÂÞ⯌}Lž2ƒÎψ)–;ÅÌp;ñ6 KBú1è—• Óûkÿ <r5"ñÁ>ŸŠà~>«òKà ÒÇHnL¤Ft±E`Jéi9üîí(ø¡$}óårÌÍåDùµÔB2ž™ YIL>lþŠµ Þ¼Þ˜*o³Î> …68hœêª:X·ž&ü·aK‡TÆvó’Ë Àž ‡VÎAÂò,^°î]Êf,øÏÌ‘J–q/­¨bs1.Öë.K…6?UÉ
+<gŸ{ihJ MÊúaî˜ OJKe”¶+4ðþf÷ÍÔð J"?gãV˜
+Mâ¼~À,¥‘vŽª÷¡ùÀ‹t´Sk5¤; »åS–•(Jʉ¢.0_ð„ì˜ìJ>}3Ͳv¤è¦C&ÿp
+™M8I¾þS¸©AXûaöý§Ý†9û›e‰999ƒää÷ðY˜õŒ«iÞvK mŸb™¨¥W÷ d:3$’1çÁù·Óh|âW>žF0R•žãú±—È…‘šDöù†•DÈŽ O—l%+Í]MÌš¬w8[öSã0P£e¡…ÌB`ùÝÃÕr„0±ãÙ7ôý§U>qŒ<Ìѳf’U…ûÜ‚¾—>ß0þŠÔÁ-r¿j@·Ñƒ¤¡FÑ6ŒÞÐ;²BNÆxdWï®&Ÿ²’›w1š´D-ÜæÆ;¡¹(XI‘7¡°ZXÍ›»ó,‰üÅå#°w4Ö£ öî¥>ÐÉ»B„×úòÄ…d†„ôçŽEªѶ6F"àаët•vè0›!¦2²AÚ,EŒÏ+Äó„Qé,m&­:ò:á-0ø:Çe=!wýºð%¬”ŽÌÇ«^’*ÌMûM°hdy@·Q¥F®˜†ÔŽÁz&Ä–H-ž|eu9ù œ_ß'º½å§ 06/R쨱©¯w+sX
+LYDz­í>mÂ!«z¤éëÐ=‰ѰJ$âŒXƒGE†8?QØt­ ­Æ­{XwÜ×÷ŽsWÄ3ü‚ÊÓ¡›¬k†;a··/…u;-²§˜T
+í´Õa:‚iÈ”˜ãfš=œ¤ïáÆ®Úo,•ønÓ»äáz'hÀ‰É¾CH¤×‰{³«Éh£À]i©|ß¹+zˆëfŸÔ'‘%v
+Y›€®>68ë¹_(ÂŽ,ö—<æ7ï« RFÚ]Ž„èE8°šüƒ¥Ì&ÅBÙí}(§IóaOc~«²%"`X2Ö¤>¡ù\&]•Ñ6ØüPSYY.4Ò·‘@Ƴû߃1Ÿ½©‹5ü ]#C~$K­&RÃIÃÐÁE=zªL
+cªå†'¤¡U}¥¬Q0Û 4*Ô(Mìg{®IiJñZ1òYy«TÈ6©Â`..Ø1~¬Ì•] ª•ÔÕöá¢CaëM ç_¾³¾Ã¨
+à´‚v»·p»2
+ç¿Kº}-ÍŒe#ç;…°prçmpoâ©öÝìàQÚw IÂÚt®ëŸÌˆZ=­JXc¥9Ü•.®ÈŠŒl„ÐA1~_u½kÇ_4ÔTB)#îTIßG·ï°âLZûçÃ/Ä‹âÖ¨Ÿ"_’2Ö– $JaÌØa‡üyî.žDJQ:]L 7 D§Öù¼äñrƒ!÷GÞkûBV…,†³äñ‚x †·vÉÐÈèÇá‰À` Ò%AZ¶G"ûC KÀ0ïÙåþn³P†–4¢…}v(üq³ÌN7Ñ8TCÉc<Ü7©^玩òØͤaëèJ8è߈;õØØ¡­±ëçühª£AûítI–‹î—L#úÛr¨éG¯¶¸{¬gœ‘ÔÎÀ>O@’Î×|£‹Zše<Ý…€pîˆ-”Ïpžr¿iyiÈÏÊ®g M5­‘DÕ£b³#Ô Us…¡(eHH&LS­ ά
+ÅQÅgÊuïµûÓÄo:ª´ÔšJ©²
+ñDŸàÓ+@ŒÌÐR¯7°AÆ.V&V®Æ!Vi™¥ˆe4
+N²—¾”äÙúeÂùkSÞi’
+·dÏ”>!ðEy?e MæÊxåðbH°ž§ï›…T2zÅr‘
+_¾xÔ‡[™¥× \1úÑ8L@wqh‹LØû}¸‘"nX”îà`è.p+ËÿqSöøØ«o*àïdìMÍ67k¼)ÏÇÊàÑÝЪöèVÓ~r+`Q¸|ð
+&‰‘Ñ>Ž‚Ü.¬MŠø÷ƒñõ· rµ8±X6l=ÕÓ^?؇… ²ˆè°#J($ýÿüÂÆë?
+0m:”rjqX€kê-À•°©š,À<´V[€KZy€ ·³ˆ<\€Í@›!@©ò^å²6wóÓÊ„ÛZ½ŒkÁÔú‰KÎU±åkõˆúµºÁgi Øê€ÂgY7„§òd+L˜aŒt¨Ï~¥­¶á³=5(Úúl”+Bó³°éœÎgš¯äõI ÚJ™æe¼ŸÓ°
+Œ?c²·: ÄBzÎUpÁ•rÈÀßG×¹dmˆ}$LŠüÂöŒ7P¯êî0Yº ù5'ŽB{e$È.Y`&Õª~±KVE×0‹™~7”ÉL³Œi-tÈ‚ ´D ÒPñ€z.p®POy.&Ù{ùH†Ê ©K
+`B:)
+º½Ôýry¨n×ûà$¾°‡žÆKt_‹ä>yL$žƒpXîááS 4‚Nã­ו;-$ltKùæBÕ*Be ýßmœ¸!¹qÙè.
+# )PÒ)Oêº3ÆŸpÖÔž®Ää°œÕYvŒÝàŽRÓÖ´œ$‘Ðü²/E_Ž’Ó¼ÉÓ僊m›,8B” *­dHŸ¤L!a[ÁŽÌ^€‚ô›ëýðL^<¹ëÁ@©ÉÑ,=8Œ¨cÎ"{D–p)`Û•‘Èi…ø6®9H?d&(F+“UP¨6ƘðÁ\ƒµ¥ ÖKqhþØ2F$€ÉnWßï Y=Å«0_ö´B±Í™ 9GVbFþõHqj»”ŒHÆ5Ï@'1’ƒæ…Ý È
+˜Iîè7d'¿ðô:‰ëëk¸²nmXÔ ø¤™24ŸO ‚_þ LÈYõ2Fj¢Ð•ÖE¬”¡ïœÜ!0°òI®ÒB¿"Ä]’3Ax|˜¨G”0æÑ!’ŽH0ö1°·D¡gÞ`ymGÄjý2$?οÚúvmZ~
+4AVk™$ë
+cšßÔ8ÕPµ ì%ºH­©±Je. z¤ÑN ÀìH"ÿÐqÀ'8ð‰ñ"¢€™…Œ D
+DuÇáݹk=×Ñ 'k'ö 'е"¿tšpѱèÓ†êò5jÐ~¯paÌþ«´q,Ü|ñÿƪ@_ÐN†%ùàÅb*Åî®TJß´+Öšdðºb¤$iÔƒŒ0ºIHTœ+O~ô„¹òîŽt+dnD†¸ò8£-Á¹Àȸ·Ò¢\º•†Eµ­”@Ý­€èŠk…é ™?µâSÒ®´’XBÜA+„(g%æDèb|j*+ˆ"<dù/ÜÆX&+~Þ‡nt •)äo8¢•z…XçÉ·«rða®ÈŒs+FV”Ÿa+À’Ž„ÕÉ|Î,E1Á0gB«Äì1«ªä AU
+úƒZÀµ¨Ê—;×7¬>½´aµ¹V
+Ou5"f;Uò6˜L§j%ƒrªò øÿ¨Šñ
+ÀŽ‰tS€à'¨Ó±ïK€§QÖÐSŽøe+0îéD+è
+Ð!`„ˆ0À˜úÄÁ6€·#âe[
+€Ub«â_
+‹Ð½küȃ`¥ýþZƒÆ8Éa/Ž¦R‚þÜ·áºÂëS,¿/!*ô/}êкÎg>ÿ/ÊÏFþÌ´øfÒÏÏ‚oü°ñCó.öy/8øììkš¯Yy½µòuqÕ«"_Lu¦¡õ€ zHÏŽ;Oåæù‚2ɯÊ7¾¬4ò sÐÆwö5ñÉ;Ô^Àíµó <'e籄ë0>ÓY/PZL{\Ï “ÕËHÙz¥ÙiK¯K€ý’n½ßè%¿Õ„^€•:·çõ‹8}“ó‚dÒ–7+:O×ôÄ0¯åñ\Žåu¯œ+š6L3Í÷Œ¼p†¹TÃf‚,'_×¼(ßk •ä¦§šõAÎd¨ÑÁ8^1Ó,ÿâ!ÍQ\•¢¡Ÿ ¾¢@CñÂåÏ,óààÌótà7Ð þþÛ;_fâ¾Éed⽋‡2Û—·$™Êd\ÀuÏÅ1€xnOaÌ@›‘€øÚ·'1
+´ÔH £ÿkSa"µç$À„¶g
+Ï×õM¥ÿ¶’S«ŒúÓŧ×Õ\T‹É'—o½dJ98Iî ñt‡i Á×…<à½.¬|¹.Œi]ÈÔ…Ö…1dówÅë²y€ZÁx|a ¬Y×Qa4Hêê»ÍP½Ä
+& ðsèWbðøB 5Pÿ8 #Ûç#¿2¾Œ%š!µXuÎÅ *œ:¿ZëW˜%f³æ&†ùæ®=\ÄM
+K¼úãÎEÂY>%vnG¦`ćE‹Ñ3¼È(T¢ê;AÌ"¿ä„Dq*Ù[zNWˆ„"¨©9nË£×BS=¹¨ÌT pôGKxk&–D%FÉž°Ïò¿Hv"ü¥Ò8>'4û&!©UéKWs—
+ôOüí&®+’0©.àî÷Þ z Ýä2rû0xþCnÁ^abó«‚×¹—åðžæ½
+¨yDEãø­æõóN~|ý:Ìt¿wÅtÖQr„¿yl!ljO§>é*x?ïfjpöq¤¡ó[X§/z"¨
+"˜"@J².ípËBñOUiÍNz‰27Û¿Lx•).HúÕ%#²4•Í‡ÿ€›I¿£¹CrìKvlÌ`!£ZyQ*Q‘XTiDGLl4”K•HHw3'ØKã€z€º C×ó£X
+¯Æׄ@@‰žh.(ÙwŒ”Ø…t`@—ÆþŠ$6öÓg&í¾Ñ¢›¯Eßö §QRÒiïŒ5ÉÎSÀÀ{TWúV†â[×Íöâ)(èš :m®ÚO.Õ³N+Oв¨â–·.¤¨—AQÁiªéÄå_°“i³c·
+­”¶¸É> ›ÑÌ1"Œ}òÿSk?( -»ˆ¦”¤^pX÷N$'v7‡Úr!ïÞ°rÀñj±[3òx<$b(=Ï.í-Ìð,¨Æ¬™WIbΣ¬IRB7!¨€Ä•ln]&ó°ï"°£ò“…¦‚£å.<Ÿ
+â÷)M@®K- ”„eþ!E¨…q¯zÈ&*±Ó%Žz0ýŠ©äù¼”"{àz‹ë°]ñXxÀžI/þ!Ÿˆñýàã ‚Ö9Êa÷د¤â}D‘Uk‰5À=#$Ÿó£7™ÇµußP)Ôa¡Š>R‚ù±µ||—-цƆàZi°c§
+1ç-q3‹ÆO»Ü‹!Té’³ÖCGÆØ ¿‘&
+ËL7¬¡ÈÅ\Ðwˆò‘l†¼š=§Û:<&T½ž
+’7U£I„È–qß­q¯åêûF‰3
+…BÜ-bgn½¹ÐÙÇ°Âß¼œ7½Ë,¶àCåÀ¸¦áćdHÀÈZ†ræ¡|E‰¾p×=+œË'ÓQlS,¨J-ãÏ|P[äò0¦±ïÉlª±+þgQªqeĆå ¯èÕ0l–o§¼“8Ë'J£YN<†¹€ïÝú6@µ˜–2­ÉI•iÞB?#µB’LÔÓÍ—Q”šß§uÆlוF
+ZB
+ AH’XÐD Tp!îCYØ<ÂV¦gU&°À0ï¤7I%‡óøïûÒË|Å+ƒšv:¯,k:žÊF²G—¶ ij‡š}ÞQ3M3p5)€,´M²ç’Ùø£jTA²Šse8ø[ÂW„þo"åÏã+ÔøÌL
+þö¹hB©©ãj3hŒy.¸@\ Ã)¢P—^\{i¥·wƒ“ /?Kï˜h
+§ˆ¿÷·K 2ÌDE7{t-mà(å
+5p_öKŠŠ††H¤Ç¥Ôj»åþ¿Ä ¹ÅŠ“]àöŒöä v=‰ƒLJÒte_èKÈ0aÑ0]þøC¢ÜZH±W„ðÚ¤Âtt%3%¤Ó© ‘)ry$$!ñLKJ¢ûm‡`ŽE@ªV ?²ûGL¼÷: ª°Ï6x67ŸVìèch²@ÂmቄŽøI
+. tt°ƒ¤’„Ë dBîeTG²ÐÁ«%Ì £‰±ÂàptŸÐEˆÙ¾®þðA”)Ä„ÿtγx¦:ÌIÛ†à¾}² 1SàÆ®¦ûûí £f÷8*ZÝ¢ºt·kq~˜’ >Ô¤áâû<d¶È2CWKæÝb,žE¹âºkÁ°c!B"ùÀâåH­fß
+?Ÿ›à>Dé3ϱä¤ÒºQæ’bŸ§Æ™!Ed½¦ R]_C.^¤ƒvíƒ$÷xw퀚¿üwŸuàÇnðOƒÄë«zmáT? Ht&^ŠÇz#
+ß$à¾åH¶–=vw&ê6C -ÓÓJ%˜`8ëNqÈÁ¾×“Ë|[`3Îea"kRØôJ(ªrœ•‡¦Ü ‡W ,д¡Ã]ür•áD{6qÅ9sîaw£`(Ékµ-Ëg Eúð"į<ér¶l€
+24³¯Ç¤©üÁ¦»‚ª4w²Ž?ìaâæ‘Fˆ}«ö ¡ÍŒatÒz>Zc¿0öÈÄL‡|óˆ@˜ÙŽÙvRLVd}yÃqnÙØ‚Ë"§m{£óÍJI!¢¼˜ÊÐ)ÀIQEµlQ-l9ôÖÒ·§¾£ºãÚøTpøï Ãú¬c¶#:RÉíœÌxu²ßÍN>˜wò²]Ä=÷-?á'6UV;“Vî@gîh{#‘jV¢gˆ¡Œ5àN;o#¼í§3elÌÆ’¾"ù§‹äön0†zUZ'NM?¸‚æzŠÅ“6´3¶Tg'D÷Sþf
+—‘TÇ”G½w™Dp} =ÛxË
+a3`
+ÅÁ|†ŸPo}mׇÇÖëÁç¼Ç—ÁÀ ŸïDB…\cªˆ57jÛj-ybq8©{?z¾îEj`á„™H5v¶nç¿%#•À|žì t¶2g%ú|à‚AØŠ†_¥íxº ´1„8‹/šRm¼¦òηƒÅÙNøåí *Å3ä¾"ÂMõ°à0^ÕYa!uHbzÂUÜUú-!Å1;Fà6ÍiP
+kyeÿß/Þ…ûI&×>±C¯¾£õý&W'óY[bçÓµ¢ÁÛMPSTpΤ©” ¿1s]”¥AXsF%$ÂéQ”0X<åé+¡xGƒHP‚%¬ñ¼s.¡ºUác똣Âh&”Ò:ÉÚdBé|'€ X©Ch3ƒ„ þt†°lJT°à¸ðÑñ*žþAà\x„Ááù†U^¹Æ›ãþhá)#er
+*¼†PtJsˆÂŽJ–tCµ¼©ÆRM¸9h¡­¿sy ¶=rðó& r“ƒ š$ŒšA%)Nç+a…ГrÐv°ÆÁc
+êÃ,áolÄ&œ •kßaO¶àŽaaBàÕÀ$óCRxJ™h‘ÈöÍ„Ï)&Tpq’[¬¸ÃKÈ M_ð™¨ìk!ò72B¡“úRá7R¹-"yÚ@WÃ˾kß—qý5ŽN°Yí¿¸…ݧé}ùg!Ÿ7‚Û÷4C‰QbyÏè¶ Ð­!2°Á`Iãzc1)øYŸ¯
+¼7'Ãû£†J÷e,ÞžT~}€
+²YÔÀÒ!ë‡A¶‘.?¾êhF·Øü‚ uD˜HiqŒ-Ê›Œ¡Ûº§EßÀ±~ø!Œ‰t,ʇÃ$a,Š¿ì‹U~øIÅ¢ã-{}Ý}Á¾%\U\©õX”oÍ
+’»c½>UPùWu!ëy„¥ü >”4h!³Gnµ8F5St=Il:ÞEó¥8íûÖcC †$<æÚ ÓfÛF2'Å$pS%Ÿ)õ¢¶ìãb‚ΰb2¤8Ðúa€Ñ!Ì ¯‚ ÛR#P?0Š*¶ZÁKGfO¥â@dˆ¿"ŒSFêQÜ€…Ÿ£!ék{‚º mîD÷ÅNßÇÇ%9 R‡y#h}o3ø³»’C—*åZ¡j `V1àý ²-<lúvuKÇÃ-Ñò¨(دÔçPáë·µVA/äGõ"†æ*rlž‰kmÅ(ÈÇP7T…C-ÒzYêÆÊ ½cÖvÜü;¶Ñ©¢ðùIüc'kŸ€UÒNƒ,†Ój–y¬;nøû
+uÁj%ሶœ€¯Ñ }Õ´%bI²B"n3fÕŠžÏ¨FåE—Ë£lÙò;ÒÞÀÈž¿W[ÑàŸ*B¡¨ie£Üêßø=¾|ßá^Ùƒ%çJî¥ß¿·¼ÍÏ&”­ÑÈ5ö?|ZP?Ý’¹i’ãY{Ÿp…¢¦´EeþúO
+â*Ø5˜ÑÿvµÈ÷#è'ªûÀ †]nn‰EbPÕš|Gë‰Ôù¹£¾?`ÞžÜýF_¢gç“…ݾ¼¯ {`ÆÍ“›”øh
+äî±5ùÝõÁ‘¿`ô²<ÐýÃäS‰†ŠLŽgBønƒ‡ù#£M¼U"pŒh™ï¸ÒFonTƒÏ±ÔÌLVå+n´º^á—ùS÷˜è»õã2VKÏ]˜âGÕøÀbo±ñAT¦%^üZ¥ºÌfbÞAuüÎñ­•°ÊSX N”ïR4µ9‘ Õ²[°ŽX6x™drí#1Ô âÂeuíÈ¡¿ul¥„ú#?åo ^BÈ:-½o½^ ÊJ%ÓPË:÷I÷˺¡Y@hÛ'È­FøYÎYÀ9hãÔ¤Õ¾‘ÇÞɳoDìÖ°'ÜàþZsãpŠm¯{=-í[dâ5ñyßòUå=ñáªõ{ñqGæÈü¶­„\£uåjŒ€ÑºÃV ~Àƒ(¹¶”ÿ$VWl³<[ílE”ˆ˜p 3òcq€ Ñà.ö"é±,Obpµ%
+0ÚH¹™!óÒÆ®À8®aIªÏŒœ¯7Ù³E¥›×Ȗĺ"kǸàÌã]~G/dU³•ƒ†Ø—6¢Ô“̱ÜÒF„2
+O6~‚ šh€(9æ£Ü—Xû5ölAž`¡E6Ú@DÓ†
+´ªN|×Ï•˜”«¡_küDõ»Ī4xŒ|ÈŽ—Ѧù8ÂwîÔJ[Ão•ïTpÅÙxZÙÉÏk¡øûTêÍ„,Þ ¬rqI(Ff‰L¡„m“ãzÞ5ïnß_ðÀ±¤“ÿbëÓƒ3›`ý¨&¯É¦H1Ä1Tàx‚’ܸRVjû‡Û÷FÛf/{ÑÄø;‡TqŠïð |ëë¶ð낼÷ìmhååRÅ=“EƒéU{ T1Ç‚ëO–*š*U¨anw,UTPTßCé#SEl<š{£È
+–žÄ<œÈ©‚óó¹7YxƒÉ—‹QŸ*F¾ŒUq…œÝÙ^…U! _XÛ­`gD½CËia² F\±
+hR'k!"Ÿ«˜]¬2Å*ž4Ñ㳤ÃQY`«H>®1V±AÈÌÙ*$L¥A1Ú*¶œÖûÑ‹Ø*ö4ýͺŠ.ã:äÛU|i¾w¼
+ Z(ªƒ–^…Á‰Pxî=Há*¯âéIQ s¼
+E£”àGÅcð*(&Õ„y*g¡ƒ| Yò*RZ >ÃÚ¹9¯bÜeŠk½Š÷1Ö)ðÆ4-fFÛôÚ>4fF”Oˆ7¤ÑeŸ@ÈÙ (&†Ÿ¼ÒËo3Àh×ßô§< é˵¼ð”r/cº_ò»¹T.àÕÓ¨…<~K (oe4è˜í(^p481–A0ÎÈ(3Yª9CÍ`u³E¬d%(ö(Œ‘1>¶[`¸öhG¢êRr¬ 3¨úÃÌ›—Õ
+A¶^‚†™‹½¦Ÿë„ce}á¸F;a+ŽR  ˆúÂæòÈ¿#¤•‡kI!e¾·ÐTÿäÓéöîTu¢bŽˆUá
+endstream endobj 12 0 obj <</Length 65536>>stream
+à-Œ
+¨›ýL¡žÃŸíWÿkqÛ¯âbëH>ôMnG †é±e„xòz±hà2Ô¢œp×ÉŠSã/Ø3%Åý¡þº‰bkyŸ•ÄÍY>˜ˆ‚oì_ÑJ)Ì}ÚÃWÏápÃsO¢¡DZ­oÈa‘œËV/íSXKPuAÂe:úïÐ|½¨7p¾0ò.‰ï\ž¤
+³-?ö”nó,"®ð õ~-/(hû³-Ñüc%­§Q`y ;oíØ8
+ÍÎmË"åJ‹®§ñäý núqq)㌋Í~ÒzF)¿9“ˆââH‰&‰‰‹“-×aeÕ‡=ó5‘UWÄÅ9ŸùÒÿxí5fç°¾}橦S=µ ?ᚥxŒÚï-:H$ÅLw. Ñ”ŒD·Ü¦á7?Ÿ°SùX¸ÊÛòj®/]–<Õ¹›é‘¢ íAI3gÈ(žíĽ©äÄݨæøKðξ{&«ãBr%í-òzˆ À U&eg¬Ê–žÃ+s—ñ$#|ÖR‹ÊÁ­­P^ F[„–G€<]HŠ¯ÉÉ@88Gç¬_iÉ1xµÅ°Ng6í”Ù0n=‘L©ÂBÓš¨æO´x,vWrÓ«뇈fÒªE"r£ü¨HÑòÉ ½"—H ‹W‹âyéɪëʨÿÒÍb˜,fS/±¸é;Âb^kÔ_Ä«’Ò÷ì Ðós÷(©z ÿÕCj¤h`l9¯Òø-»GªnÂ)iÕâ¹|Ù&k)éaî7p­ ‹ö+dHýÍÆÉû¥ÖÍÂýºÎOòù>½d¡ÄXƒöÎÕæC àÖ±,{·;ÖŽ ˆÄ5Ô­«Ëhè ¶bjÛB—Ú´2koóJIÍâÒiUŒÂ$é$ÿÉúI=hòil½EYkè¡ÜAm@‘dßH1wiM1žÃÒ±¨0‘Eè¬ÍbdùÑYß1äÝ|:çtëûõÊ¡˜%‹ø·NXbãª[¼b¸gNŒˆÅç*âÄ¢­“aª¹-´wa ‘EÏ = VÞÂ@ò¯œ‹nèÏ[F(¤Ã‹õΤØ6A!#ð"WA|ÅýYdˆÉ<88
+áú`Ÿ¯^aî,f¹™JÓÔ?ßîÛP=±Ò@ ãv(‘X]ú³
+ÐrÆ<±Ò¿4½‘e±-½ƒðe‘ib
+eo xNž²8Ȝޖõ#`·DÂ8ê,Æ7ÝÃ|vk4ò‹ Ý7,óæcÛH‹¸“&Õ­ú>ðãGD×®WoC´ÿaÑ>\¶»Ùõ¥ñ@ `%¹[±7`1š=*O ßj¡‰¨\³“BüK„ ƒ vÅ £Š›~¤ô[qú÷M+N~ƒ ½0%UÅ•¥ü[É2‡2£¬H‡基˜ðœÁß•ôý’ÕWøÙÎxò\ß*Ñ#Eçqãs¥°±ø<Œ&Q\J²É®'y‚µô<’‹ßÎó8–«¼%ÕÝÆ“ð'Ã8Ñn˜¨øw3’œïnˆ†l¢4
+î-üeD+éocöÚ7ûn²ÿ m?¸ï¾ш×ÎâG<û¢ôôÅu”€æ-Y-Ì‚(Z¶8—H*. v‚“€Aûs™,GÏ ·h[þ]Ð*>”U?ç-³‡…ë?}9˳ôœŒ*¿6½C[\mÞc¯{SÏ4fl"Gtø(Ï1‡"w`f¦´a=+}æ†C“Š,û†v`ƪö1ˆâÚÐÜX
+&déõøØ
+pi»D , ëÊ÷írÿ ÚY“FTq•’2 R0Š '!#þZQbñâüïZãT±(}C|QI™çÂÕ´“^+çÀMXòË(„<J®ëáüéˆ9jµJÐv']ö‡¨GØn)꺄4õ‡õå1zý%Ú/Á#}ÈЕCbpxŠOäAx
+yQŠ4È‹)æV&4–ÉÈô²PdU…(ˆ25©-ûœÉ3Î,-ÜÈ9°<Kîs+UASy-;jåZ’{7,C§“U­ÒÄÉQ‚EñgªÄM5õ^ÝÑ
+;^ˆKuþÁX|ïT_µSWª šøª«Ò°\Wuñx%\\<ÞWI‘2>Õ‡gUh6$.T¡(ã®úÄxÉ©\%bÜÕ’ÆWõ^šqû¤{]
+©°FÜÅ Ýt"îâNwqëâ.n …4Š)Õ¡jPãŸw²èLÐÄÈ/›˜ÒÔ›FµâRãmP¹K–»Ë"©F3å£=ODéEáË—’ÞW9~ÿ²ˆü{*)|—~èqåý—üá—#s£¹shqÆnéš1æˆÏ¡~LêYœºÃÊ¢âH§Å·(ç‹ø"Ò#!/gñ-6ȳ>ˆAºˆŒb'bÐ|D‡çÉÆ«jŠ¼YëD¼‰¸­ü%ˆØ¢ÿ2–6 ¥˜£b±</V]õžÂ÷”^6œ]©¨‡ßÙÑE÷$rëI¬K\™ÅcR|‹R<‹/²}•$ë)ÈQ«Ýz·žD’e=‰ëIäû9}‡ü†žÑbú
+’ÞÂèÔL”Øœ*qeøS†ÿq¬:²ê…?%åQÔ)BéGH5õÕ~C²òÊL i×0‡¼»y1¨ýD£Ìž¦—ᜑSžQ±”僗Ä<,A^¢–*v7¶Sâj}B&JÇ©®¸sµÚø8ŠØ8>+ÛxI5jŠJ'*Å£½Å- Ec>×}Mj‡Ôª)>S?^RÚ~—d‹•¥d-96æné%D¤ÞÒé÷¢Š‡ö¹Èí“nŸèb;¥ÕÓcj‰òØ׎P"ÖIDçQ5›6ºÛ-Y½–‚žR qjR!–h©ûÜó'ïÔÝùöºÄ!)㥚ñ©~Š¨ñ¨JD¹ŠS©»“Æ[¥?UW£ñ‘ŠguÞùËU´á©ßxTiƧZÛâ{É]}€¼s€S+oŠùˆF]Zчùˆnæ¥ðsv”~„Ÿ¼í ±ŠæjÏ¡{’P¼>>_È1­y¢lrbìùü§ßPÂTJøÊtRn%_J¯Â.I!Ãïì•võ/æË ß劒>Q¬m”¥™e”óÆVú-£!Ó å<™6o¡D‡¼PÉOt¢ÔúD‡åÚ=íN,ÙvgA‹ÙÙ?ÄvÚNxhvh5Ͷ±ýZûDzýÝTKk/;T_ß0‰cYSžá)}!©¥Üÿ725ùƲ¨‰ENkÍã
+ÕD¸Ú+æ¤âÚ<áBŸcjÀ›<>é'ýú=8‘qýH!òñ«Úp©Mj”OŒ61C~« ƒŽ¥å '–dÙAŽ>?–©?óá–®z2‰¤bïaû*}G{R´÷æLöa¾ó¥
+Øñý²MÈKX÷ðW³íšŒí«ƒ™h°TE!Ë—`¯„´ºñ.Ù@ ƒÆ¼‘áçÐU;2µ²#ã`A^&Øá-ä<âPA:­×¡fÆì¡Èl3-z=¤šúÃŽ¥®È÷+õÔ4J‚,4¦”„9h†vÐÖ†L9}T(#ÁV‹OMNâJYÑŒVÄb°¡M­„j|q‘ŒHèöôa÷>4åÑ…¨³œA‡dã$a &5dUA4{"²Ô¡ ˆŠ¡.O*Ô!ª)a Qmˆ\U¥¼P½I¯ØªJÂô /jêAQ-sWu&RRrš\B%éŠÂTÈO§šY“ròÍ@Dþ2QQQ¬L«ª­jÌ&ÈXSÑ}·ÃÄ%¿lXDÝÕ¢**
+Q
+ˆ&TDäT®·*ªyù¤‰£‰nYŒôÅÌTwLPFgJ¥#e©¥(•K¸`
+HŸ:œ=
+ {P¯ŠHÁ}æž~h2Ö”O
+c7S3n8ÉECœÉ¸fÑðDs"¿bÉDC“)¿D#Ó†±’Ð,Æ!†v•C8C´Q̨¢b&TP ÑHY†ø"š±P]4£ 5Nª† Õz8!Õ™]hU5æX3vø4ýóZñt'-†?×X RûWù¤^Mî"Bä[•[¹k&¸ö™)rÒ$<òš‘@²<jþ¿V>AÆ8¦|Eµ/ršr0Fõ™%¬&§)…øLRa‚S¤±¿±‘ˆ ™Vá3&Dªb_¢øâ™@ã$ª#~½d¤ˆDí"âÜ”Ÿ³îŒR+MíS•á#ëÏÄÁdWú„!ed¢«îÑδvÅ™G;
+F¹qÒ.± Þ }˜"‰« 5A…šûC¡Æ3¡FÆ@C›ð)=dN´¢@C¥ˆ°9:èÏtÂLƒK„4:͇mv±“ i=¸ò„wtE8¼ë5D& 1«z•IÄæÌ xÞÈl•`Åðòáh JÉ"PS3Ï£|¹é ¬$ V`ƒpáC#TFïõÀMFD0jXÃJÖa† Ã. ÃpaŠÂà Òå Ôªêj yè†bñ(PùÞ;B=Ô9n!@C‰Dd‘1ÊEíÌ%å¨/“T×íž#.ªØ>N‹~ˆ5§GŒ übŠhÂudº‹âž®(î—bjºN´´‰J—™‘VÌÄ?SÌAçœdÄQÂÖññK‡¤Ežtûp¦K/42kífKzÉ §f=¯åÛVC<ó1"2SÆJÓ·Ó‚Öž¤“hÝiýº8c3z¥w7æ-Wú*£R´~ëÊ=fw…ef|M7ãÛ5øYÿî­Å¤ÈÑwj‡Ã¹‰uDö`)߃åü¯=XÒ>Ua=‹ÖÔöä*ŽÚÙ×ÒÊâ­Ø¹J&™Ê>·¶
+é÷›Âr*3i|¦/Åg:MC×ÚîÐtŸKíD$O5\¿ª\€³¨[óÓÈa‡8EÒž†£E݉ͅ_f]£+§ûÓBÕ$ç«š¤XHs&bMÍRÞX]"2ócd¯êb­'aÅÖÇ\<'§§ÐESÚL|á¨ÜªÌ%¦¯Pfú I$™ñÌÃSaô–Ï(­âDDú W"ÿØ#ôœÔ—IÞâVÌÐç”7Ò¯æÔ‹Yäý™$ªÓHÓTCÝsÌyF²k¤Ð–¤6Gê’ž˜ÆzãøA¶|£T*å-žÑ}Rã­•¨ÎÎ{±%™]¢"©Åi:/uâŽQ™pÆ䈦3¡ ¡q)DXý<NÒOZñšv¸]Fz¤^ê*_—™ø§1¢éê•w”Sæé¤O¦Ï)w£âúÕ Y[²FÄS’­.™l^p}U£7ñ<Ã_Ë ¥A¤Šº«*~Ì °âËÁ`MO¤(‰¹¿Š®ˆâx«Í%I-3‘^5µ%üúÜm¦Ox^;8öKõÜó˜9¬Îc±t«IÍ+ÌyÌò™í¢æzüŸ—÷{Wù ÿŠÿ×[ä·ø^"b-^Õˆ÷G5ÈæÅeú¨Óv‹Ã¶=“½*9Ä1÷D
+{¯v~µd·Æ´¨E#C^J¿HÓëË‚etºñÂÎÆs•ŽpüÖMp'¶Î‘WÑâîtGÔ–D¶í“îÔ‘8lF©ZñdâA¥©û jˆkB*ñªßÖ‡J5‹u„N®v†â23ê½H˯”dˆâŒNlÓI+¦Ÿë•’O)ŽêxQ…jjg!ÔU-3%æs[FâB]TeU-3㦞V®ŽléÜî´s)”ÛÙÐ¥|;äÔlè²(§“µ—ýìHÉ4¢,e´y_#Ù¸¯(_Q‰ºÒ´V!¥ô¯Òxœ(Oë}ŸtˆÉKˆ¸5íÃÊÞv¯p¤­Åe/mŒ¸;]©¢q'WÆÜ}ÚIk!‰íÑq³³%ž¢Py ót’‹Šçò$7´™ ÏS¢hG‘¥÷ë¨NHó·X"fö4|[#a’äN¢âW$ñ!ýË›ô‚Ÿ™üqŒÁi•R5oŠÔ.ÒÖ*ŽªQež¢,ÁXù,k³²QV°\‘ùxC †œ훜Íòì‘ÃÙϦLÚ£xÆ ‰öû—îk辆ÓÜJÐþæG¬4~ÿ.ß0§Ã(ñ‡ú1'$±¿]Uêˤ ™4o⊄ú™G ÓQ$“™>„Tñ‘¶!= ID$5MÔÙŽœ%dj]CIÒB2â§XãøižNUÏD
+/Q®p9×K²çr|“nx›3žrñ8Å©GÒ86ÇÇlÇ—1Jx|Žs•EéGè¾ÃYð4nß!I+M]™¹aÝ•O­jL:Õœ ï6‹1SÓ,u1zf¨²{(©ç/ûtNý„zÑù_Ç·y QY'ÄdØòÅÀêÓÒ#¶Ñç­œªà9®˜mF'ªûÈšMXhU%«ŽêTÑα÷Xò¬65bl¢ÞD¶OU¦:Ùœ•-ÆUe5©F­^²Ù9ÅoSrÏ:©
+ÉHŬš•]*­E˜·öZýûÄ3cÚŒ$ÖÒˆf!yÝñ 7ëö±œaˆBý@Ã@bzU(©/ &Lä 3ÐyLjVõ·"‘bh,ø5ô õ²x®Õr…–TUp½ÎW ML‰‹ÿM*S+–ËH…‹„Ë*\.4QDFH¨¤ÄžÈLG22› ‹ç¹£S ³Ò¦„"«ŒÈ¨2áG¦–\Dþ&ȨpƬ ¿|Fxâ~}S;ºøòñêQä$%s,B]uÑÏjV,6Höïsé$hœÉ4"aÅÎy&Åfhò‚#¤ˆ(Ÿb8\¨Zfwž†T±Y=4 zÐF»× ³¢õÄoöLJyŤ©Çð‹¦¡¸8‰RZD&RúEBj׳=…£…aŠpª4{ :È5;‹#d"꣤=H›†X§K¼*/Ä òDÕÚ¤Œ|E±ñ‹8&†üëLÙ×a¬“7¨dáÚA6Æep¶É“‡Ìö ÀÀ„M¨!ªÚ‹>Ú†ãvÛwvçâ‘÷"m<#r
+M„e»á=L1—…#—vÍ÷8|Í ÷Ší¯Ì¢ù’߃| R‚8ã—X6غÏú¾Yú™¹TBå—ÁßÁŠÉµÕˆ&ê[Å¢¼‹Œß—ÇÏ}‰?:O
+úg} –ʯÙ™†
+JÇ/¢¾ŒA%y6Æãª\j5]Æ“4˜u‘`ZÄDÄÈKZ¡•t5“ƒØñ²™ª¾c¾‘Å%pfG±úš=‰9¥£GÐ3o^°:®ã‘™…0Î~LûH‘3¡Ì¹}ŸˆØó™…®$+\ ©YÍ\c’·f$5£Ðj©)D¢–»f¯Äzˆši+Ù쓲ÅóÙ Q¤™uHD7A Ñ՜­s®JiÍC¸(Ö¾yÝF§-gÏ+$jF›)=&FczÌ.;~¾jºÔB[UcU˜‘("”çM Õ*EðÕW˜®.IDš-]¡2•‡ŠE†ÁÂ`©Å–R˜ZÍ+ºMA.Æ9ì¥m#±JaHÖbU-°¢¤üp*Mˆ¢Ð¢zQ¡†U8™  Bª@5#­U8´ÓˆÎ¢£ñ¢"S_©X }²@33Á3ÁCJð8xx‡r…1=ÄÅ A܇ººp< UU˜Ë%Ì óócxíä7IÂëó?¨#MÛÅ'bµˆÒÄhöãyä áÇôˆù·cºxûL®” ŠO³¡éÂ9Ä&-i“mžœ/E¨iØ„ÍŒ§¨ÊaŠHAÆA†ST2d‚ŒåR¬04ÊkuªD n"ym‘¡YŠ¯Ë|5ÿ¼¬“ã"HrÞY¢àšDq®pvª^“×76CnšpEMôz*!'ǃ|E^‰C^´C½\ôŒ’§ŽìÔ)G™‰&±?ÆŒ5d e=g‚—uäÑcu¤Ò¬;åÇ°«QµPÄ—QƒŒ«
+RP;¢DEˆoŠºý
+…-;7œ©È—bÃç/Š•+—š%"ä41n‡¢yN Eû…!!rMDº‘¯´"Q8"]‘t£%‡“5³’ËU{̾HÉwP¨B«èÈ…6­‘Y>Ÿ´Ïžµ”Ì…³ Ý殚9À#:ÑLèP'(Ìx‚ÂÌ„‡™N%†µà
+ñ·¤Ç¦ÞÏ9Ž"A[·®iMD¶Õ:ŽqÜ@ŸÐÓ·6˜›Šˆù£¶Žìå.µ%äÓgYhGպСbA4›¼RÉjî.¦f5Ÿ
+‡á5ßp –W3µ—éšG÷`v5ÃÖ=G ¥O-"ü%ŸÄJ$ݨ¦ŒQ­„¢Ò†[:M—F—®ô¨¹tJÌ¥Ob­é[lò± ×ghšð‰é(|LsÚ'øó?¤ rwè9ä®(CÄJ É=ÑjƒtJË„é:Œy2¯Žž’/ÚHi¤¥¶ªÔÚ—ˆ25qâ—F!W¢H!,ºî'$ÄøÓ ‘øOQ\eDðX5!F—Cƒv‡æÒ Ò¯pù –£ü
+V”œnÃYp3 ;•ÝËI¾™É‰ÈCi«“,ˆ"/ífI(¢xêSù
+b9œúbQ´ÖÃÈú”‚Á·:"C¢`×Iýn¬Gu¬”jûú ³æûø{¾ZG«™Í>×ÈIŽ4‘-ö¬¼¸b‰Özè~(•!­A\ƒHë9\[³1hv†ßV“ȹDRÎŽíŒP:”ŠR+ZÑiHEK¬èi ú…úµ Fñ¬a*WŒè-¬"w¶$HxY2ÑW§:Ѩ‰ì'dç=©í‰R¤¹1Ú»xo¾2Š1B –ÆðE¬àDGg]$«8EüŠhÊ9|Da ÆÙr¥µV6[ëÍãu<j[Ž¼s¥•uƒ²ŽEÉA¯A¬šWQžoÇJiþœ*¥
+‰$.:‹hM55õî\¬¡!)‘
+ü"RœñÆzÆ‘²k$‡ZUÖú*3žâi7r#ÙïIz2§J‰
+ߥÈê²|IÜ-öL”"ŠÍ¢C<t«9ÊrÍ‹ÃaÊV•fá4P8ÝÍ H)‚2ŠT–’ŠCFÒ£"¸E9Ž#G…¦™ J'ŽbÐÔÌŒÇ9Íl6Ï:µ£Š`Y‚ýc¿™¯èbGöyâ‰ã‰îIKp&)J­Xjd£(´a·}ÜZ¡ÚÊýVnG8¡x8â8nÜáLòâìŽ,^eüL²Ë:m°[§bÓ©¨)UlúM%‚C²D*Ò°¸©=cEÂçF$Ž§*d­>‡Q´yµ÷\$}þW§©IÍQ
+»òÀj£õÔ‡æ½Ô˜˜NuJr¼X™qI—Œƒ¨UQÓ’ OÔ´/cªTêƒnEc–î"•ŠQJ«t"-©©}ŒõÕGIc±MŦžŽ‚¦3ñ)C¦$’žª6ܾsʉKœ½Ê¹’±¥µ®³„Ãú iµj%¤{{FZ°c±Œ¦9-o¡Ï9Š+¾Ä¡ŒÚÒÚX¦˜©TjZ¹`#È‘Ùµ^­N;í4NØ¢ K´“#eF»3ȉ~´n,£ñV~ÑGåœ|„6™– ·“nº¹]]žÉ´aÏÙ]ÜÚiÊ•6’Âeyº§KF¥”²œ;„l™°Å¯LŒXYÚ¡¬^¥¼¶Úò²²5œ‰ÞDâX ¡uǸqh‰ šmÌuÄ"‹B9£Êåd©‚«PNF”œ!'Z.•‹)Æ£Bô·f››¬èJ"ª7í¡£$Ž‰d1 ¢¢û:•Åy:³œYP·žCR1¥éa¾^¯¤·Ì.<­rpdܨ”ªµìñ¿¥¨ìR5bÑ5!RmªÔ¢LÍE™’âtè¢z¨Ž N+<¢×‚cÐ/…„³…’5fÐö8Q%)æÁ$ͦ®ÉŠ”Ì%¥þÃ)GY_LB¢Ó¢x“üÖü#&ËÊÉýЩ4qÕaIÄ…Û5/¿5öUÎ$ª"äÛF¯ANÈå›L-(Έàrš5n%“bÑ‹)YtÌ¥j‚HS—ýQhŒo¦iü"‹ Ž¢>«h ñgê—´> ÍM5šò¥ qb,4Ó£Ä$tU|#_ ¨fè Ú¼(›éµæ×ÔN)º"Ão4!Ÿ H¯c7ö+é¾ëV¯¾ø߬/gkksåÆ-7îÆeLK­bÍ'2(ë
+),…uÙ(­tôQœÌìì¦mg:[‰DR+‰ߨßGÙ÷5¦qíåR­^;¢ [¢Ñ„¥i´ÿ´Kɪæñ•RtbÎQ5Yt•å¢‹^ffÑ—}«. ÉÙš‰ÑÀÞ€¾w¨±TXíó E “…6CšºÄŸΑë–sµ/‚Q™! õø‚dHk]TÕFç'ÕihÖ(=¹'ÂMN~òÎd(´ ´Æ¨”ÒèÄ‹2
+S'üøDô¶Ýjêð-pþ¼NÆPΰãÍ9âײðùâ8±»’kë
+åé€Ï¥ûB0ž'-ü I¾î ¡¤Ëm·N‚ ·c〧”p¿…:p9#[ÓwÛeÔ1’ ø³Óª¨—¼ÛÑÔª®£_…úFE ~‘Ì;oÏÃF t««ÑIœ3(™¢Dú¢¹–®¤+ʨˆ<óë)œŸÈôÀu`9…eµ‡<°_o~:ã8ãÎŽó·=†0ßÿ^Ý%T<|Uôÿɇ>²Ÿ¤mét …ë7ôå+Dk
+÷t "¸àãæ<°Ú ”$ê
+nØvUj
+Í }Ð
+‘þ@;â¿ü$ƒ¼EAj£³E/ ¹„Laö*¾ÄÙ¶õÊ1²/75fsUôùn0Ï”˜µ €ÖRfË!q‰
+fåuhpýŽ/¾*aî ] âMgtU»LšD+¤ü=³#DI4 Ã’1ÅI9Ú ‰«ß{‹U0r£ÀGŸB¦ºøL¢ÀÙâÂD1ÎB WqYÜú–ý
+Ü赩À¸¤½ª³éŽÓÂHîG¡w€-÷±Á˜#6Ts$‰x¨_è
+ëRêŒ=¶dû÷øçŸÂ¤Bµ|Vuܽöz%Ú‰<I[¾8„ÕŸÀÂ4—¢Ê !Ñ"ªÖ@Š+: ÑnÐüèÌ4ðE¨’L‰ÈÈ áG3zu~6“sac‡ŸÑ‰Ë’Ç–åøJs
+·F#ä
+—ÙA[‡Ë2
+›.¯¤Ž&zeÆ€-ž`K«!gFn`¹ü°VÁªõÙï€exYû.ñCR~»L†<•°oÑW2ÖÆ€k‰ ä 'ÇN|Ë5dHÂØ3i.¬=SÜ.A†&Ž!ѽ¹f¢lj è‹w¢t\îc¥a°1€£ˆÜ§aÃé'½»Ž¬Â1 ˆoZ|SÅÄSßÆú5L®±Á1 â`Lz
+X˲SD‹Ž1k`i-iÅDfNµw ¨õs™µ´–´¬w È™«Q ”14 R”ݦbŸ•Ç²†)Z² ¬t_ohí^VAXÇ€\l¶àš!ØXð ‡BU^`¨lƈÇO³˜¤ÔÓ1`è›ÉÍì8øQ·al˜I·pL p<–%jÚÚdj\«Çj<XŸká 0¹Q`ÑÚGÉ2¦0
+Ù§\õ­zPö±50 Y‰Ìd°Cßð‹:¦··
+P*=jòs ¯˜§ÐÃÑäŒ59Ü+µDW¬P@Øõs¹³Ž|âZš;øâŸÀH
+A§…7ä
+Ÿ,¦>e
+›Nñƒ  Øøã. ‡œÜ#@!’)¾ìCŒ·¡Àò¤,fÄAMñà“Ρ³îç`ÿß“¹Š" ½„…ƒHÃÄæ“ :øl/×!œl_d
+tI ÀqJÝ: @ VÖ‚$%éMÊ
+Æÿ‹íL_)6!Vý×ù?É]pÊÄP {¯Áû?=˜CN Ù}ÕÿLSSÀ3dœùŸýÌûfÔ…‡ÿ¯}+m kO û¯ï©ä8iÊ¿6§»ÿS1ŒQªÙû…Aý?,èÛC~UÃLˆ>ÿׯÝUÙå?ˆj·&qü?’à¡©]
+f´l?5Íî ´ð¿•Ò}ǘÒe•˜‘ú¼Ï„ÿµN+ÏÜ •„«ujHþƒ®,öþ#ò»xÌ—r üÿxœ×ûvÁ+üãaZþÕùËð¿F Âc^rìªÂŠº ÿÕÂØ'ܽŽÆðŸ€E+3í¶NYÒ‚îù¶ Ô‘r††ÿÆ/% õ,,KfKÃÿõÆûë
+>“¥üTQZ)2å[j™¸ŸâÒðæ> -Kßšá¿| ·tý¦xœYPŒýrEÍNö§Ëq(8Ú5~ñ€"©
+Eè\þW”4É¡8mät·•ý¨bd“þ½<üOòÈÝ-j-8jKS§¯!þB{Y‚È0Iñev¯"½ÜÅËá¿&I‘§‹é¢¹M‘#”á¿N ¶Õ³2•´Ûà%(Ãÿ– "-´²b.tžQÑ!*{Ãÿür„T¨*CÓì–]{|¢ Ôߢ›»eðâ‚ ÿ{Û¤Ñåô¤Ò€©P1%>|ÃG#¥ä~o³Ìƒá?1ð É›eñÙ ÿ¾3f½ëÈþ{;Ôûýƒ>Vд÷V7Š§­©O1PyÝ`9«ŽÁòsÿg /õ·À›° Sí_ñT[Þ’4´)ìŸìÓÿ®Ô9ŽÃúï†áo#54œíéß}i|ÁC
+:¡õ×d
+4c ©Zé`gDQ[hšæ pÛúï{Õ÷lF’¶õï/ØqP×߬õw .ìƒ{9`a9VéQÿ`}”úMÿ”…s—Î8g,0¢Húw+›G)Ò¸.]îZTÍôØGe?µX¬Œ5Å$j*ð‡þ3Ú—jûy‹ÁZè¯/~¿¯cAàA-J{ü²Xù>OÒê]UèŸ3Ä) hî ØüÕý'~{µ.!®„zµR|ŸÐé²æþÀSBÿWÇÂó
+uEë ž†™?#gQ
+\®ƒ]U6®_zØ6ôW¢x¡YSÁ#*„‡CÅ›NPnbÌLëîÞª¾£½$SòJ`ázÅ·÷è(JâTd3µš†þ‰|ä=Nv0ô¿?­œ°^¸Œ/¸îì=ĪÞÚ´R9ô?j­uå)Ý ¡hPÊ”ߥú‹* V$*Æøãÿ•Z»nå(
+
+p›üï¸÷V"u—»¬Ï*¤FÐá©É“¿‹Òvò§ íèKÁ)ßa0rƒv/qþ‘Ôº»¨FE | ”ÿ/B*êÉò×{‡LÄ«p‚y¿Ä>G”‰äÕø,lëxï›,Øð/Íay
+vFüÝÑÔÝO£î
+d¼tÏ/W§ø!¹Åøˆ]§,ÕŒ Eâï(?´%ñ7£¼Äž ø½A=ËÉvë•òBÎ$þ#Ïwb‰ûqËñ7`sÅÝ1q-Ú«ŽøKRºˆ¢iX7¤øU^{%ÆŒ#Ë4Žø—½‡º q$ÿ^àh§ÍDü‡Ñ]û§.,ÛïfŽ†!5íŽ=÷î¡Zˆ?_‚^‘8ÔDéÏZŒ _,M¸*¬}ýñ!üÿˆ,(é?.ïwå¥G
+ú¿hÐ÷Œ]3š“}ÿ½ºþ¡—âÚû÷Õ ßž8_³jòþbF9c’ævÃ'×/uÿN«¡hHù­@_îß;! ’ ŽÂýNã‚´ÿ þºýᥓËó¥_ýØ~âð¡“XØ^ѵÀÚ™ùìß«„󌈧gÿÐ'ÙêìïüŽ{eÞ½)ûÍGå3,ç ‹ýŒ‡—!6Y‚ýÁï3*Z¾Š÷äõ_ÊÓûo¡hBÿm&’ <ë?êˆ Ê¥ì•aÊ«_qb#?W»úIS–‚P”ª$Щ~ÏÙ1©~­Ê0Dõûd¡ë~ajʱºOê={Aû| ~¸ïˆ2zY8 Î9C8⡬òf î‚×¥¿Œ¾MÌqã€-WÒÿêËOá?>déG‰¹TÁ8Ÿˆ­‰t.¨ùW
+ô*óù†Lql%ÁnÜðüâ…to&Lí9?PÆ% qoþýó~ŒTÍOQĆ®­ùcÏ®ùíßTþ@ó›_O|[µ¨Å+±ÈT«®7¾\4Åók†Å}XÒ†üô¡2g­“àO˯²ÞEǶÐhy•ŸŸ0‹^PQþ·ÖcòI2ùÒøà`pÓþs&å7€ù뛈ø¢—±kÈ?åö¡ÆéãÏ<ºt§½Òê:þ†'Ž9¾¶(Nºr¹,Ø‚öÆøA¬î0ÂOw‰â÷Ì‚‡ÄkµÄ&ʧ«~øç[Þÿe¦Nt¾~¨{rO¦>§Ä@sñŸmT²ð‘U×we ?¶â­)óAšND[iäÃÁïÅT¢º]Û þ0w­x€Â®T_=?ïQˆÄ
+,6;• (âtÁ¸Ù§-`«Ö
+qK‹#»Ö*hߦì{É Öýª€ì—å î¥~24ûŽa•»G-ôœöǵ~0:gO§·?¸û›]-Ñ"•›Ü«t^–±çõ5€Ž•a‰BW×?¢iA«d÷´ÕùöE<°ë1çÄ+S¿`)²~¬Ã°¾ãÖCEòP¸úU'Ml·« ‰Mq~à™±g¡\x ÷cC˜¨þ7E _ ^;õ7r¨ Ô©ïalžÝ G˜¬R”úH±Ú¶sh Éw+‹úÃøVçfÅñbœ®X1c–ÂfQß"îV1+[uQßîïIò¼l`­¥
+Vcã7@×3¦ÿÖæ:iωÓ÷·fv† ëpú=ÉD&Z¼3}ê#YÎxËà/b“¾¦v2 l •þØ›4Ðo\¯“þª¿Ü
+/¿‹?”OÞñÞy{h¹„Éuò¯QkKÏ.QL¾ÑÕÇs[¯æÒ„P'CÉ_*2^üÈ— Þ <-<N,òábŽ¤äÕ}ñXY ùõÓ4k‚ü×<@ÿwõ¯|¸³‡äñ©`¤qDÆ®î…ãçÎrÞNáÿb»EØ?6ƒ3É~Ƨ±TâyUÎ…‚L‰™–¨£¦¸½øÏÉæ£?Á+ÿ~tH\ÅÏ‹k‘wÜ°¢øž|FA2ñïù9bÑÕ ‰¿
+iöyäÛä!~þ_>Õ/ÃÈÿ
+h•àç \¡ð/(ùÊ]nGøb0†ƒÒöÓ‰y>øˆà™6ÑgŠþÔàæÞe
+N窾 ˜hʲ£€Ÿ¦m%C±ˆÖ:T~ÃôY)ão âBAà»-”è Ø]š«|ëÃ."ýßs¬*Ìu,T|ìï½$§ÕèJƒi¿ßowßo°–ߣRø,²ûž¾òŠhÃ+c†ú¾ùŒÇ@—滑ï˜ïT( rïÓ~š‚=%¨3oüÈÐŽŽëšŠÎþ&§÷ä-f†€’ç=ïÊòd!$òÊû•©ÄàÝÈx/~½8ú`h¿¼ÿüöÅÞ½çPe†´T’»ÿúY<ÆÆ)„¿Ì=<¦ Ì¿O¸$ÿÈiŠ@ÎTh÷ÁD›Sj¦ÏkfC–K¹U#$ânÚýƒû«B‰IÅZí¾™àÞ-˜Ý$åÚ}Õ ¥©¬èˆþµ{Ú)Ùc¥øâHok÷Ÿ·`×!òMñáÄýêgΨš×îm°ÚO:ݽ =25t…®Ýß»Ÿ‡c¸|”ña¶hD•0z%µûJ¬o¶úž¢v¿•`E3BáJ’#ŽÏl[bµ{©/>ôË‚¾;°e¤…ÝÓÑš2ë~?þú$dÞÓýLµ®Î•64»eTù‘Hy€Ñ}+÷Ñ"+_¦F÷)ip$èõŽÑ=Tê5ÕyŒ8Ý7Ód)ºçµ€A^•x®±S°"ç†6ØZa?¶¢ûƒ‰yAs{6t%É
+˜Q¼è^"ÿß­Æw‹½l³v@N¤Åú}2¢Š“.½:D÷­¶ÌgÛ+áhÉ‹˜š'ºw·Þ0½Øн$ŽêRW»ØÊUѽê~'”¡ûˆ`sðPׄî{¼ÊZ®ìàí‘” ‚îã`J0¡û>ôfú@“„hÓïíª6`S©]&ÝU…Cƒò:Dî’ƒÐF ¡{YqÆ6¡ûI°Ms9Žôhú¸ønh‰™Í,B÷ùBÒn$t_†4¶ÝoÇù2
+DJV’¹òdä×äµHmWžcÙ¦>s` kËFB-$Þ
+wCF+% á“+OÏ–};‘åà®æéü~¼`«ÌAž‹æ!;ö=®¤ñÙä<Ì%ŒBž]×+¹D]9|²+‘i
+5Œ&9OU§Æ@’@èó¨Î;W~¿! ßÚ!ïÖh½%fêÛ*j•rØ×\O<Rržná8ãùÎS¢iã?ü«õØÛ¿ SÑáÞüaÐý~çtóØÏúý
+àg^ßÞ°ÐÍSjnÒDò¢è&d–æ•I7ž¾{7ÏÙm ¹¯w#7Ïïî.Ú¼ó{uèfš­OÌ;Ùaç1(&­ó踋ãm;r§Òy
+qž:yC¿=Ït‚ Šä•5¢eσú9> =ŸN#=a¨`ÉOålî6Éä#¿RV˶SÐ()Š…ÌJ3Ÿ§w´3aYᬧ BÒ-ŸlÆ€o5Zö+•—„û’4¢üà„¹¿ …r°*=®Í“åÉ B6SÛõ÷<œ¦A,I5Çž‡Äãzêäƒ]+zÜ'É ÜFXòºXSJ¯ò9W¥¼ÿó ¨Z¦fMûžáôSG&ÕÀò@Ö…Y=ÜÐú$(º€<1JéQ@{¦>”J†´9ƒ]Ò°Y¿†0bÕÓ<¼$ÖÃ<†®§q¸w|ØãÑ©;-Ä3ŽþÕR[á.EBÍ~-Þ!{î1°©':Â{l¥.DÞ’¡G e¨.ò=¶Bx€4‚Kýo^ø¸€¡øxD¦zÇÇr3RM!W7V¼|¼åósŸ{[Rg[>`<è“ÿRÓÈ›ÒÇÔ†ÜÔ¥Pö,×G‡üKt~L(ûXaÒhÅ
+^ H+y
+ê´8ä§ÐÊ„ÙÐüW&MªÏ21IMšºZf1 òjݵšýšåº“pÞÈG2¨mñ®éôK×ÔḧX„ºÈýOÝÚÅÇ&ÐlÚ9–å°³U‰,˜M¹õŽ%!õÇXE†ñT›@dxÝÌf›˜àm¢¨µ‰›zÚGļQ!]üEÃuÓ.GÊs,EÖÒ›¦VláØR_¼ߤiïC«ß$Y@vÉpŠ²cJ úV7¶ÌЃxïNÂB¸jç’´!k¼·|CòPæ­'ý>9‡@ÔyIðCoBN[©.“‹aŽ}CØ-›?=óv\^1ÉqÒ¨(Ÿ¥‹Œ‰Z`E/kH¤³š-rÖЬhžƒŠ0ó“éÄ©¦>4gã< ßÃÉSš—-B Ž}…Óâ4é*ê‹z5Cr(8#¤Õ õRŠ.t~aV8ýøÑý¡é9D†Ú·àNŸïT‘ïÒyJÁÎ<s„€/4C¦P­Q.=j†‚?±"§;H(œŠR ñ‡±ýº¨dÊúš322äñnLÀÎÖ )—–Šú#(DtÁ!e›]ðcEuRÈfdJ—²9Ë ŠsLö^p* r¾RÙ€Sâ8-Qg§ž%@Ó[ˆ!KôÝîÔ&Ò}<)6.%Êb¨^ú½må
+bFiþ”Ó£X"†{S àòŠÕCŽ÷.óEŽ pJîäWTñªg“Ìa¨8TÈqÃnfÈì™Å*0Ãw´…N\'¿ØK4dö÷$ûÁöøŽ†.1ŒÉbŸ/yq²ˆ,¹û†NÛáA%óŒ}CžIIýMeÄqªŸðEÍ°7”ŽÓ{O'Öq:CMÝWÞtœ„ÿjùSxK
+ÜèÌ‹DŠ
+á~¼-Þ“ÿ‡™;$z—Õ¯À2,îÐg‚u€¨Ý¡°‚MîkùF ¬æÉ–T $m É‚Wa‚Z:§ÈÐM†ÃQ?…n>Â;8Ínj/m(Fü#sú¨Àˆ³“=Gj.§T¥%¢¼Ìo‡pã”=dZ(Õu×+kåãërˆÕÏÒ#ò(õ–qoU¨Ì¸%Ýã¬2&ƒ—“aBÄ7u»éÛöMHxüµ“9:T>©?£äÐfu¢¸Ó3>x̼:ÊÇßj…¢€I‡¶Iè´?âËIÝW0cò†N¸¢öÖvõvÈ“ØÐE s‡ÆŽŠ¬ã áj£µnp‡Zo,° MÍÉŠIÙ²r™•Œ­îÏHªÉ?Ùû9VÝ!Ë{xÌtGqV÷ÌBêÐ}&t•Œ9)i1¯Õg8-êÐñgoxGt‡ƒ¶†tS—ˆ
+XW3<ža¤<‚ZvM’:ae£±OóöšzA”_Km¯5V ¢æœÒÙÄNíÈé„õÆ)îN,À‚HàI)D+*mBˆÔ Íæϱ>­”Ÿ’`)·E"uêè³Ã"úHêTÇì±A!â" ÿM+DNDÜmtöð—_j÷J‹|Þ[
+àod·Œ‹9™{ƒ:ô´ªÁ5<$Ô¡Lí`ç$ æ4 5”=ÛîPË~oAüG¥îÐå::I¿Ç‰vNú(k–*âY«®ªîЖÓ%,œSWX(íœò®éÜ<ø¹òaéÛP
+bͼŒœ;T=±›Ë²læÜáå)p@ºsŠ?:šýPPØÂN‹3ß,°yçªà–g»Æº‘Ú¨ Q.î,¿Õ˜½;'‡Àq‘ÌÅ*q‡Z†MQ`c¢\Á;§þl¥r‡iüóyç¤ýO4ÅÀ›µÛ’š[Š) êYÅBäM7§¥©C¤Vá;Ëœr†ei9Áåôµç´ç5ÕÇ)'øØò*È4÷6ôB8p„ŸtP%‡ù‘›“lZ ¢tÈO®†d&ª@Ï6f:Tu­ƒÕ ûC¢UkÑ ™R´@Çò
+&-À9e7W-¹dû-êñ¡/€¼¸1–UXuÈi‰‡“À>÷]¨C”vO­WÎéßçˆCëyÛËo Ô:‡u¨¡!Ÿï”ç´ôñã³ÑãœÚ¶*&«ëIšÕ!ÊØ|C‘ʲ)8}Ä9K8zNK÷¦“±=—Þ;dõ£G0à&’<§š£ ¼Ý¢ðŸiÈsê@Ü
+NuýÕ‡ž„‚N²@‰°ÒþE§/v«ä¥á¡Îy¯SÕO¡Æ9T€:Ü’¤n…‡¬ñjÚ­”CtZ2½ªAŽ@*è4Åxöù•7mçóœnñóî ýèïsõУ¾b²ùPÊ/Á§+HÒ`ÀQ0Õý º¤Õ³"½8D$EWR•Laóβ¨È#úSîÜiRædq
+;|µ>9S œ.ð©f9Ú*F–ÛOhûYŠ¡Ð3Ÿ^‚W Ùö2Ê*b…¼Œ Ï†›‘‹\úZ Q)õ±Çµþ¤ž¬Î~ý÷;FþT{®_F_àb'´Â9ÌhX$²Fh¤Í?«4‚–ël4S£ÃJ€º ¥ì>y|êK7âÁ9©.¸ŠZ·¼ˆ#›{ù+ŒõÓºÙPPŸÕ —å1…w´@>åQmÔŽœT¾#åü0²v<ÚÕ² :dGñÁZïì‘ÛGºq¦W2óêâ—ƒbP¨ú0nlÞ_,2 Õ^+û4á4A1HÐ
+ô !‰!Ž\ 5F½HkE±Dp))ÏÕP1h*oPkP{'æ?zY…„ÿ“Ž\P©¡Ðʵd—ô©,CºÉ:—¡z`¶Ï#tDl aÖ·«“óØ¡JGµëiÒ=¯aƒµ
+Ùo‚áqzZì ŸAH–¼º‚ú‚ô%ÞE,5“B:3jº¡ŒŠ $íI =Rˆ¤#Ó± ˜HY«HX@o›±õw‘#ø•»E©„{
+8¯…pŠGâ1 \¤
+Ù[Ûâ4÷f¾½Ëíÿª'™LÛÚ5“ÕºU2¸NšæQ »—š
+ BªÓ&™Z¢^UƸ—ŠÍ(–F‘ÎÂSéÇXÖ!¼‰cêÇd>Að‡LH&s t!#–M”‰ã¥ªbÖ‘Ž‘Tá烘2Ë­8zUß2ž„dâ݃Á°zÉ„—¼s²8’i7¾"Àz¤O4ÑÝK騙táä’‰ó~Ø÷—òå=q­¥Z&M—qÝñ²JË$¶ÿ—mFZSË$º Jg›‰ËÏ°]ן;‡¡9ceŒ–©a‘¦ÒeV#)Saþ5¨† Ö`êuú늜&ˆGÊtŽÈ\³<~MLMþ¡ùúð+Lm±IXB:Ü/˜êD˜~û¢ Lí4Ð Y¢ô±(þ9J][1I™®"%Ââ2H™pô`‡)›Á¦LSdÄÍ‘Î5
+¤€R:L™aG.#ÞÌSÍ
+.Ú¶…„ç×^ÿËL4 hœLó„æLÄî]<ÊbçLõ,`¤él½La:§…¼2¾Æi&©¨þ r¥h&•ö¾LÍ7|Ö™núÜŠ‚Š#Gœ =HyËÍ
+¯ƒ™Rém);î Vœéi{H 1QœiòŠÚM0 õs+%&"Yf>ª¢™
+>BC˜©ãÇq
+J¦rQüWY-¹}j0SžŽh¦ANòxln/Fh&ŸW1²÷’©Žf²/,å£*-õ"ä)4“wÍÊj7’QÆ+
+q M6 yÎ6!ãIJ™.ëZ7}uÏìy»S»%à”|SO¸ôN0* ¸»pº mR§(ü
+F
+}jÃ#£žb}0¦½+°8-‡dœÜ’O¨§} õÖÓÞøàÃç÷ùD ŠÅùTîþͧ— ‚˜A¤à]ôa®'›ŸíX¼RkcÓQºSd1ÿÁÌ'5C¸«˜ObO83$†N˜Oɲ91o°Y ™O¾#]X@æðÌ'ÿ¹lb/ŸÔŠô¦ÏNªö3TíÊPT‹“*ihœª pjJEJÈ-ÁÒ|V›"Tö§XÖ,Ÿ
+5•&N*;tò'k»i•ZÈ5ßòbáf¬ ÁÓÂ(Pð€õ)Ûjåy„OyhP)ê{ùžì2m"R"Qï©—œÓø¤'¹ÎOà`ÄÐôÐaø|¹Èüi{3éÉ6Ìç‘=>=`’ö‡ä¢ZŒºšÖQ­ŽxÌj*öè‰NFm(»V£§ IÔ¶«°_ø:*äÿiPZ¾5O6UëÛø…ÎFU©yù ×<é­YE\Z Cóôô¨%è4”쨚céãƒ~'¦9Ñ)lÑÓ¥
+@DÁKDä0PÔ³qÌ7U*•A
+W ~ö@1
+k;UeU,EùÈL´ IŠ¢‘|#œD ©Cn¦øp#%Š9çÙ¸Ú9ËÀ4¡&z³!ÇX{JV8`^̪˜¸ê®[üþÍůJN&Ò?c„\ꉂ¤-Ë™›]AO”rÒ2¼º»~íz¢²äÖhSh)Ìy¬²µËâ2+Å9!e*hA
+¾EÕ>ÜÉW CÔ |‡Ì„¬Ïå'ê°49Ã
+,"#Haì±FËÙQ¼@=cÉæ¼*C\T*ët¾¢x 8ùD=†¶¦Cfe a}¢šµÆ„y$å‚ñ‰Â ¤1dþè^Î[PöšŒi(éfpŸÛÑæFöÃ`é28¸ª^d¡ï :è[
+üVú³X£Äs.Ó´†úPxêÐÅð$D¹|KÔÉטÀ (B6W´õZˆob9á5(wdM(¨¸Þ„Ãx(²À­j)o«WÜ1"϶*ôÈÈà7Æ?QBG ƒzoyR®þ̤ï¶B¨‘b(h¼ZÆæE<ñ®ŠÊuà+å]¤„2cÑ´N1ôä?zv,3‹…'ܪ‚q¥“{–(*ÞàUÙ®:||èe|Ig>ª5ª x%Ñàé.(*Øõ ŠÕ¯ª!€RÐ3Q‘‹ aûrfnÏ ÒD±–u ®Ê½L"@áS[W.Ý)p…q=$ÊtÿØÔlÌ´)©RQÿ0‰W‰ú&õä’Ø ‰Ú4‹ý¥ QŽ)ýla]³|¬ÊÞÆ-!j£„Õ/–%"QÓ£îÊú(<‘UiÈ<„A5QB«ÃÐú~Qû974øØ#º¡­Js)AØìèŠÀžqåMeÑ9&JZZk{`á´#ªíw4t9¢2˜PZáQzg;"D17ÜÙö "Qÿ“8ú>‰uú¯‘(ŒŽà sgã
+³ª“†«­c#™“^ÕïõV9nr™‹‰¯¢lfQ8D(¼(-F-¢`5#>ig„i0݃£:*Ik:j¢p<ŠjæßG!LÚMr¬m-òªy/NU*ð„â–€xJjÍ«Â/“ÒFöGz+$vª¥ «Ó–cZ«ZæËUJý
+*•zzŽ‘lG³Bºp¬€ÒqÏ(N¿'U®¿ÞwVM¦Ø5wÓ@Yˆ·`Ì”KÁU#4åR5…íÿͦþ oÊ^ŒþâT{ŠŽ\ÓÊÚ¤÷O¯UÚ:•HÒs{§þªDaä<%°iøNâétL<±ˆËV‚e0=e1ÁQÔÈ>muûCÚVˆßÉÏ_ô1§ŠŸúL§hŸ­J…æ”K|<[1‡ ½iÙ'¢÷”°ué‡Ü£GA”±ÍX;ç,©[á×7tõ<Pc+fø' ²Ã§bm…®[nÇ÷S:ვŸòyä‹õS³‘)i7š’2éýˆM³«–©­¦ãUöVÁ„dæSÔç
+T•ÑRÝÔ0ìU·'ାxÍ?,P|ƒ/G< 7ªîà¨Ìo·•¹¯TÏ2t©ï¥/ÜAõ=ü€’’f%T*XlçÍ̆ûÜÊAš&8à­(U0‹JD5®è¬¨^ÌŒJSWÉgmh9Mçsõ©:Ôª$6a–—+Sõ„ÎÕ ¿üÔTG¡@©Ä¸›S½KSp4ŸÊW‰Óœë*{K8k
+šº‚^<0V«òp“CËô
+c–Ƭ¼¦«„ÛȽ
+†6ÐÒjö«˜©¤ù›¯v\:ë«YËÀj—±J£&õùU%U¥düÕ"ò"Ný«PªkÀZÐ¥V﨣ÆõäÑ
+ ×Yè·¸ÁÌL[q"'üC­¢ÃøeÖÊ
+Ë,/Êа*=¬lUj̈õçÏ$‡#ë€öub9nþˆ lýKÉ‚±àþÙÇCñ$A)8¥b©¡
+S}úXäW¦;ÿ„ÇGáLi x},ïш‚ÙÍÐða½e I*lgÄ¢.pé¹*éc™’òciñ,ƒ"¾”§sŠdàd9¢YUxí(äp½ B&ÝØr3d
+d•5ü0„§ÊŸŒ@VÿÈÚ-”¨e¥½7²zŽˆ « ¾B`ùìÃàÁ)ÙE²ìƒò,YdÉDVÏ®[Àª¾Q?bNY¡$Xø4PÑc&V1Åᢵ´Ì«‹„ƒé>aå„0²ú`wPžŽHa’‘ÅŽˆ‹''TuÂÈÚ‡
+‘ ¢®¶šÝ”³ …³‹ZN½+?ý
+Y&øÆp_â%Rü()ÕÐH%x!«GéÅ Yù ƒ·¡óçRЖ†,ŠœmˆpmÉq£’…,:PƘ ?¢ú±î7 Y&£n‘
+LÕaíå&# âAñªå÷ŒÎ. ²ÊRû’«\ŸþK–·aLÆ€›d-§®to 9(z…,ö€ìÕÖβjõv×&¸j,dÕÝÌß” ²§â"dqz/Ô›j²Ì &“SVY…Ê.
+g )(À®@Y,;6¢h‘¡Q+,î†uÂ6 ­GÍ'%•Î8|¦¨f‘•Ë‚,°\ʳD>RYÚX‘Z¸³c–·ã³ÜòïZ'”Øj ²U.²È—p@V‘Xb¿·¢â¿ ŠSíJ8t„3«Ð¼1µöKd]¬8_üÄxV}¬ §ô­†2ñÇBl)= 0üÇbò–#B9‰Ô{—b>±‰O:5ÐìþX#™ùº¢-R’?äˆ[yìš)faÈW"ÿ±îYóÐ¥&(þX K–}€* ù…ðÕIÀŸÿEÿǪ®þyŠÆE¿ü±†ŸµQµû õ»«·A°—k‰V¬Ñ| dû‰?Vb@VYâ/Å
+7›Y¤}n
+§bCQ_&},åùEÓ†Oê>ïo,«Ò\EFúXF ög»ÝN´‚†¤§B¦F„{7§úÅ/uX£J‹C¹3(—1q÷±6ɸÍ>>V1¹…‘8Œ€u»ÕÎBg¿ÙÕTãjÉ¿PB%–¢+ãøS[v_í°|¬˜œ•\|¬óõĈ8>–ÿ‘c``‹$W_ÆãÂ<KUïl‹‡´ä»ä|ÛÆ
+«Î>Ö+vYëcy¥ŒÎ1bó±2Ð)€Ö¡Zò±’þ-)öLÅPþ&US ùXüc2#ßRMÆ%åÿ†¹5PL¢
+é@>|U„­XFùé§öñ4¬ÐÖ’ww§¬Û¨¯¤¬AoeÓÃÅ>Öc£ëŸ¢Pn÷±ž×$àx,¨0«.{ù¿•mʬ”Å”©íjS
+²Ž(ÀZ\L‚,ƒ£x4ñóHçÅQ¨—ŽÒXº7 ñ¤‚,Òë Í‚,%ÊF¦^Õ+Ü\’…‚ñF'=UÄö
+dýâ}ÖŽÈÈÒÏñ°²zZI ’uWŒûZY†½EËåYõ! Ùã& r…}ê ¹n&déAF.GœÄa¤d¦pÑŠßYä «Ñd ͆ÙnKN®ë™dMÜ-“‡¥
+– ²ÖY‘x®Q#‚¬ò€Põ² azxîf²3YìX¤Þ5žŽr/9W)ÈRÚçÕ–¶…
+Y!û_¯ÏD»08¦óÆQòÇקÆ=™t ¸GhÍi‰¼eöò²ÈµÀqsîÊÝC¯
+–þhV@+ˆdåçº$ë_
+ z’•óÛϽ$ÔL²È÷Èá  !⣜d Žó×bcÔ°“,ÁÙbd{¸oȆã.þø» Œ’õ‰ `ìdi…nkóÈË26YoØ3¶Îº0µaÀ‰æÝ›,ÁAýL>¬Q.l²È°†5"2ÜV ÐÃ&+¢óO/³!›pãdM‚àÎð…5d$1NÖR[ÑÍq²þŒ«ñv¿X´õšÀɲ췃'¤'B'7<š¡ó`QKN–óË®!úÁ'K5Và|
+xÕŽËÉê”ì™qøÀˆâ–“ÕÀ^8Å
+›“õeö@Æ“µb
+òI* i'K«ÀªIøñïdõ¥”Þµàt ;Yñ„ˆ×ßÁÚɪÒdŸ,²EãNÖúv³W1¬ÆQÎ3ÎJ´ž,øKNF8K,z²PòÌ?6ZÓÉ
+{‘ÏçV«–Ø:Y‡h’¸€ïÙ,˜„Q’}·—îda“PL\HÅèlŠæñôí©ª“…ÉÏ?#±w'ë2ëɂ䶿â„!ÛëÉÒuòØÊAzL`ž,ßÇ<&?£3cýÙ“Õ¦{ÐK ˆº<YLk5³´ÈÄ“UÆI—Må AŸ¬“ ûïjÒC
+{Eß¹ß%)Â~²zpª,&‰Ù«×\÷tâTŒ¬¤lý—>רºfñ'˦„÷ù'«®†JǨÀŸ¬«>9µªŠOVý4擵÷¯–âÑÜFqìîó
+>„ØŸ'«1üzȳ5ž¬D¦–Ä®çesO–Ž7î–9üd­n‡$µUéOVY¿ùZdOV6^ €!ªx²hš+¿ú?Y ˆË…Î<YlS´x4€ãSq×oIz²x5ÅҾО,~0 ¥ç¼Ïs øÉ’õáÛÝùdQ›÷KÒ\Å=Y»ßoËÖ“%õg—k•Þp²ÌQ¼÷œ¬y[›–©@Áɲù­¬¥TdN–Õö9~^äcr²uÎÌõ;`7WÒ€ÝE'ü)ßÈ2C´£  z“Eæ'•ˆ
+bäwßqîq ŠÑr©ªÝ%e1êx›”5q‚1Ð}ØHY_%l¸å£„x‘ÁmJÊÚó
+´EA±ý× R–ñʾùžUb)+õóÑñ6ĸCÅÒˆ”jZp×IYãJ^¡œ!eÅí]gHŸ”õúè;¶Ú–ìêHÊ‚µ,Î"e!Ù™U0óI^ž¤¬¶e¦ùÅÄIÊþZÑL•²pêO1:ÎWXÊr ÷«¤-DJYŽi¡m™Ä¥¬£-«Œ£´ñ*e)|€‘
+ç3ÕZÊJè¹ÄÛ:)«Iç””…ôà™ÿVÛü?©Õ`ù`¯Š²€øÓDÊʳ¦Q…Ì!e5m…®ó…Ÿ–!«©
+R–Þø$‚ ‰`m¸.ÔŒDÊ
+ÝÈ6Y?“²,%°wzîŽ()ë½Wþ…½„J%eÝo–hcé@ÊZç´${‚¦hâÉ[Ê’ wÐåpIYóH•Xðñ*&Oʲ”#ÒQ¬JÊzx«3iA’+Òû·+õÉ»'eU<ãµÈRq 0½IYÚü­,R¤+k-‡!+#þ]Æ•ew‰Ol¥ŠfJ|¯IW–²‰]Ï\YT-ÈÛ4G
+BM¹\Y8É=<ÊðÍ+ é´
+búʲoN/›=Ê+
+iCmð칿²®72Ç=p—Þ^Y¿.Mé=œ“ÖÇXVÀÌXD|çêI«ì‚fázGºY›Û+«Þ®ô•En˜&åÒ:,« aÿ(Ø{C-°^ºMµR¼„Š\ÒA’å ,Kúì* _Ydð‰oŒW–w ÁºŸ=€W–ì½=næîÊâ–Á†îÅóùåÊ⪔:DÌ\YïEº
+ÍbkE´ZY å­2çªÞ­R©ÀkeeD3³ÿ("ZYœX?7WªC^ ‚wRi®,9Ä+m
+Š7±,T
+¸ñŒÚ‚DzÔ0h‰+ëÐ5epSa÷6&Ø|;*–õîgÜz®>çøìéK \
+ÅÆF£°ZõeYïz‰OrfY± í~ox­°H”¹žeiï¾ôîà+³,A¡L–¥é’HYPh‹÷•£Ä,«(K–Á/(fn‡že¤!ª ¸u/Ôà8ü%A³¬ñk”÷ÒAææ•Ì²DýaÒ9Ò²"ë©9¶OW–¥*î:˜“ôhYmkÒè}7Ͳ(ŽÊ¡û³™e=Ê€¡áÞ,®èβŠŒi1þ*“Y–tCŠ+â̲ª½®Ñc–%U4P'¡€~2´YV½¶…ÃÛª'ÉYV\’ s2YÖjÝœ\Ñ-Ë¢«ºÌ…‹<cÕýÎM±,§U¢x±¬ö?gU¹àmœþNdStÍÙ4vg ‰Š–E1ž×úæÞÕõ—£³ÊcYƒñ_ìƲ0=QýÄ“Oóa¨Zg“e¥XOŠQ¹<²¬>2YÍôÊC]–U¼ºÓ;§;Ü,ëñ^ȨPaìå>L–Õž“)`þžM–*³| l¹,K9W½ËæˆWü¢¡]ÎCùÝ?;¼C`†3Q T–õÇ6+YV™Çì­È‚ŒøpÙ,k—D¦è°cÕկãŽ-)W–†Ôôoñü,VÈu¾+ “LÁŠäÊ:€nHåyË"ÔÊ
+ØANôµV¢öŒ·,ö>«ye/
+g•f ’
+|éy0‹£ïžò"—¦ÿzwÁ¬±ê ]×Â,ײ®ÝF+ô=32ÙÌBG-®Å¤•0‹‡ÓVl—YÅ„¶ŽK™EÛ= I•YÃÜí(SЙ 2K`Ùz(‡2K¿ÀtñTfqÏTö‹ôúËÛ!w”YRÅ™ 0w¼`ëØ×÷Ê,K‰&( P2«ÓÝ0‰¦‰’YÔ¿xÀ'³L«xT½_ ×P‰+³êI&Oû’a·d4…@Ûw¥2ËþYéüË—G™UÎ È9sBʬ•V@•Yt!e“ãNiO]ÂþÚ7C™5ÿ…]Ò,³D³Ü%’æ_fíîI‰»~\=y…™Öe–øæqsÈ
+æ+•Ï,–é^[ïš].ôôÌÚØ“160€£I"ŸYRe Á˜êÌ•6Uk‹"Þ‚Ãò©¬ÞEGÑ íƒ`?³*šÞ@³X!Ñ,–þàe@Ò,Ò'¢Íf·ý}8£YÜMç·Ãê7hÖtqŽ°RÑ Ír#%”ÎuÍâˆLM£dZÜB³¨„h*u(ï3‹(c=§ko«¬žYªƒæxNDO]•OŒç\Q‘®Ê3ŸY­I¥'Ö~GÈfŽ)
+݇?"eûšc¼7ŸY„!·,"hË>«Å‘kÍ"x§üÁÊ 4«$#BP\·Ý›A³r¹Òñ.î Yç™ýx*$Ç)Fu€ÎT7@³ˆr
+þ72Š€<ƒéëu8úŠËæF«YÅEûƒÒ î:ÙYÍÚF_F§E š…™U¦QßVÍ:—° S}O㛕W«4£Z³x0í¶`ɇ@P·âÔ[³¨“αmlÖl}—çÛ®[ú}hv›Eîª"Üö C”4lÖ÷±&B}€ƒÍBh ÿå°YÌ®Y•àO´f‘kF+.Wl߉š-(¨½jÖ‹÷v*ñ9ªYh’r—¤š%ÐNI¥RjŸÜ£°¬Q=C`5 ³ªÛPP(’w©/ÿ$Ø:ÈÜ )Š¯2­Y<NjV”Â×7„Ó×ɉ*jT³ÈðÕ¬(LãaQ-¹ žsw¦GYS£¸ÌRêy5«6Û„L|ßì¿øÈ[Õ¬Šá`.ϪY©X±0lø»¯Å\Ú‹MVk¡¹´6ßëÅlãËÒ‚”<V(_¥³¢ðY³à™DÖ‘Ã^ [Í
+З@Ô¬l AFæ87(ËE+ 9XP³ø£° ¦ŸeÆÚaD–Ù\‚rzþV³”+=frle5«ìG±î6°FÇæjÖl¸îÜ l¸Ùçð¯ /È÷R}Ê#g¦®Ÿ˜9 Ì6kªYŒŸˆîúUͺH©¢äÀÆ«YŒí‹¡¾ëBçyˆ¤«Y±#JçÃ8qnÍjMÅPkÔå¿É;YZ³tÁ»Ö,öžže΀¬Yšû‹Úý(´ùå›E’€{5­Õ ¨¸ž·i%ßU+PY¾D¦ò½i­úͺ3Ú,¬ûf…Ÿ»}¤mµ~³ìÞ-Èõ•õȃùfYâ?µÌ7KV©¥ªâ¾YÇÎ(ÒJÜðfш¸Ü³Þ,¢!–¹f¥-®yo^È{‚W È›åŬ@\©S¿òÈÇGöKÈK~íÍ*î›ñfEÍú'QÀ×:³g•†A•ãã 'ÎðÊ(yE7‹ÚˆJävÁD‘§qÍÍbVGƒ¸ v ÌV§ßfy‘òØò“áã0ãZp³>ÅÄú=yn–É IÖ1«ð¡'?Ìz~¤“ÙOøQ`µy”›5É JÊǬ:µ &äfÕrŸ8Ps³”i÷¸YEy¿›ÕN_À,|ÓÍzL—Ý*n«ìf!PbF0–d€åÜ·ËŒ_žÁÿ¸º€a.™·ƒAói7 a¼.(ÙŸV X.þ<,³ŽëYd†‹­p±Ü1e îÀ+cx".—›uñZæ–^‹_™>µßvîVZVRï6Ù¨ÀäM²ÖÁ Üfé\­]?´YíZ\КÐÁÐv”µYMËžâŸE²òǦ…8$sV˜ÑÈá š:çS›E¹J~—‘ÙÕfÑU‚½X`d(V›EÎ_r¯l³ø†lÎÔ‡3ƒ˜Œuݤ¨¯±Û,ÁOð¥þñaŽ·®ÍZö^ÔÞT´Y )£"F´°š$àh³ÊIî“Oð®æ_È–™.K"üæl‡kFöƒjš±ý@ŠóÇβYœ ß©¼ðmýyt–ÑlÖ®àY*&¥aÖ¬-¯ã€Ö¬X¨Š[ë—ªY²¹3mâkÍÊÞܤ9¬víÁ¼ÆïÅÃs§5ë…P+jkV#”-Ð5kp“g °áâiáÚ<ñ2a²Y ¿˜Ð@“ð–†1ôvÙ,z6šºlVòè Y’‹¦Gmm@@DM›Óoõzˆ6`³2ÙŠ'êÜŽÊb5›ÅÙ‘i@ßqšÍ"Æx–+Ê€ðƒíJ1éÒfT‚”ZïÅÛÄJ)~m‚°cGY
+;‡ç29o´Y uJŽææ×fA„§oM¦vm–MëK t h³°WA¿³‹?@ñÚ|i³ GO·i ëw~ª¯‚­„Íz]ð»{¼{#
+qS4J“$lÿ/×ä.¨lV—³IYsÀž^ÅݵéžoIm¼.À,£¥+_ œ%Cdð¹T>úÕ’³Y¼‹÷“ý9jòÁ–x¤8ÉfibêÈâÒdûèaúƒÑ¾Íf5B2ñx&Ú,Và¤Ñ/k³t@YãßÛ¬Ö>Å÷ôêõ6 ïkÅW³Ûƒ9'TOUîAF-Ÿ…]%„j@{YfpÞ™þC«ÿžf›¥¾ÍÑ%q.¶Í¸zÛÆy6dµ­ÓI›U}ñð•Ò–rUmÔ Ô6‹¸GŠv¼Xi`>›õD'¢ežé6ë_èóÅmV 0tÓÍ‚©j"ên°Ä tjÉt
+
+[ÒÓfU8V,óÕú©MF6ë–1H$"J
+d{ƒ‹ìþ„yÇ~¡‚IÞåö°†Kö82øç“|̲Šð,à\Ì Ê«
+Ò{0Ó¾=bñF“tLš3„L]ZåX-…¯{ c—ÇÞ­VÑZ`ÞÎñeà‘ö
+­.©)d°
+2|Ïc·4ÃØ^Ç~>o1XceáüÏ»bzPÜw#giÑh#ÖøÙ„Š&fŸ»›O^V’¶0ÙøkHÛdRKw‰gÝÚZáÑL…lbö XaçN=`¦Ð—–L8OLJ®|w³¹ñ_qµèãÜù/x5YîC"%²£Gu)e7mÕÊ?$`ë‹
+;SÉÛ£j:†_EP¦Z„¬¨o3¹V_À-¨·k3ô«‹ OuÙArDÖâÞ€Á’Äm¥­iôÊ°”˜Þ:ÑÓ–’~˜ÌFýT½ñò´¼Kséu‘‚ÛÝ1´ˆXžNw¨F¼!YÒ¶Fd:Ö8ÿ±£¹ÿÊ_1ç#ÈÇ5Eè5ä$žÍäÑB•8è1/QX8Ç"UÃj˜Ù;<Û¬Ÿtå'ˆ êªÞ&VÓ§&‘°Ž¿˜4‰²·±f"‚6ZY;œãkv;Ž"vŠ‡9Ž»øªðj¶¢#RŠmŠèÄ\ “e¨û tJÓ< %…ƒ+Fá[.IÊœ’­ªŸvmh±õ¡Ëü¡ñKhøõb12™ÉÄHWÒàï8òz ¨°€ˆL±""6a‚ $Dëƒl0JÄ"Nd<ç ³ªãÅm•¨ÐñY q E±'³:$2<·³ÝTÚÀðÂeà¨M¼.4b-=|P‚ýpD/µË›
+Ý`;ò%Œ$"üML †2OPØFŸ?*Ýùð1ö—Š%¯Œ@¼|ží18xZa¼&j¹0Rv!ëô…p GäÀ)†Zcd# -3ã*Üp*_t´Õ[G>›P¦°N÷&lÛ×l¤‹«¨O¾ðý/H¼0#?æìÍašínæKˆ0” 2 Ú:ÍÆ-È o#5ÌC…ŠŠáf·!a
+›c-^ÊÌ¢eØ‹Xt1ë‚1]DTQsŒ$Á†Xˆ Muó)cüÆeò…ðÆÜðÛá…}ÄqèVÃTd!\‡ù¨2wT¸ù Ã%3 Ý|Ò¢âq¾uo´ÃIÊx(#^Èeb1MNÙ pPФ<\
+†õôÀ A–  1誼ß%g0†ž©šBhLÞ)†!I¡‹ C!Ž!Jãjú‡Ü~dŸ0s³2ò¨+iøR(KÃp„
+:ÅŠB"\/5,ÈÔW4ÌBЭø ƒÒ9g›4H‡“ ‡„æ¤VW¢:ÛÂóv%ÂjgJ-idÜ'æ“„º'ªX%¿%D5Â**6„ZgB1¡‡ÉX
+šl„P’IƒO Û™ !%By‘¯
+•œáð-9à 3LY¶ªË'•0aû܉¶?„ÚÐç“Ü Ó‘¡ü’n3c™/e -ŸwÆõ„ñëQ"ï$*aEŠå¡y¼A>¯å›Ú킶„Ц–M ä›å›úñÓ­æ¾!lbéÃLF|k^ñ
+§= qpE:ö6[Â'ìðÍñÁr«!>ï'zf ¹@‡–¦÷;&Õ*ùÂ¥¶É-{ å3 ±|öŠDD’GèÂÔ8BÂ?B?rKÄš#Ê’åN ËBj§4…áÈáo?‘„þvE‚–P×m˜XbÞSh
+áp Ó”Kn<Z‰Ë;ÿ"·ZayÅŽ©¸ÇtîM–oº©=;ÈGÎüRê§ô¢^µƒü¯ˆ¼oƒ¤åTnVF‚ÿk< yy­P¨C~Ùuã ùeWºV¦DòÓwÈ+½„N—ï¹¥{ük{ÆÒ±SÕÎÈþ¯•àh<ZY$þpkÍɆ;§Táå<pjÂg± [\ôgö\¦ "¿*sF7•—w,Ϊ¡áŸö®±Ðv þ ]ÄŒ:þ ]ü: u+ñ­9ÇÿP•£éY;÷úX7Q–Ñ £™«^¥} ” Ò…¨ÚÉ+WžÉ+O~­˜ƒã`±!Ùe7¢Ëã¹D­ÒMTŠ«
+¿uX‡„ÿí¾àK9ÄÅg†Œ5ÍBÍRa#’¢…Å„huaW°á^Œ’<(ê\)Š|>ëÓë<Ö7Â硆œ"EC jY‡¸ÌÇP˜f.Ã&ˆBf†m;1r}NÕ!L|¢U;k$:0(„"ÅPP„¸kº°»L™Q™“S8Ò°
+Â:P¨°ìÎU¥£á醸PQt¸é7´âøÕ…îçÊRyß@3ÈçUi9§åúŠˆ?¥Br†ÅáË„»¶„DXÖpõ‘Jæå K%—F„âr„Ç%‹)Ë·!üsÂgî„ðÏáŸ][UÐfÁ¼Ðð_—S…§Câd‡ÿm
+˜oÐ-‰:jÌŒáŠ
+j¥˜ö=½,2 Æ3%‹·¤C Ö ÖhNQ0y²®Óˆ|CÙi¬:° WR¾“/²Ó€$Ç0
+ØQÙë4ˆÌ“ ~ôúH¶ª˜l`§QIøCÈÙ4?á¸Ià¿=õ|*(p—-‚Ô÷Ôï#l9u;9¨Ø²Ö W-Tç6¸È,Gr‘–~; ½èØÐM~j§x¨¹Ÿhëk鼿§ÈõÉýÛ2h§A œŽÔ€CŸÚiÈO @ ÉuìûûÔ¦–åaHÜNƒu> BuðÙ¾MÆÂI}­´n§ámš:ð±n§A¾]NA‡G²À“ŸæUv”évÖ7¯aí4<H»BÓãÒaÓÃ%î4=ýr4M4ï4æ©z‹•½Óè˜Ð‚3)¤8=•ÂÓ Ãgb;Yì9}lX$ ÚU )Ý»Ž§a^dÍ¥ÄäÖäÚHø{ÄâiÔãæ6÷|;â§ãið‘I Á^ïËÇÓÀø3È\2ño„’¼ ˆ§ñðåIÐÎâ£ñ4" ¸äõ]¢vOP›ò„§±)ÖßiŒr"&E{§1ØZñiÑX¦R§Á¡z0 éâNþPÔ÷É•_˨;B—Ñ·†¤
+¹–!ꎢ/&Ô;a.fNÉÛ$Dt$ðêLÑå÷zì; kX]qSKa3yk{ïªey
+;ʬsM>NÝOo`ë0jÜÆœxí4ßF ¤Gga§¡§¹pbø¥xí4l¿u} ‡„Ð5èdO,ìO9×±\†sþ¤}§C9ñ?Úi¬¼÷ÃÝÎ:7ò~p4D4TñUÚ2f§ñë—lÎÐ;»öSEÙi A
+#´]¹{s¶s¼ÑíŸVSú:q
+Ó«">ªrå;:#¡ý^¿›ûôFƒÀéÿ\¶–‹¤¤»*wÞh¸¿™…ð›ž?vïÇ°ðk:îG„ Ã7|þõ Ž½8öÀšÚáØ ŽÊ¡¡:.—
+R@Éi¨u'…ZÈÊil-û»Ê­¯x§Rý)-rN##îºòRNû…g4xuÂM$§QÚ?î!xLN#¡#¢¼wSÁeŒCr2ÜÎh(:qÁ·Uè•ÐLê×þ«¬GÊilK~ÔoуPÒaN#'@éæVcN#ff
+·[…M4xâ:è%
+ž XÞµ}ez·Nƒ~*˜¯Ó8å¸|•èmž\Oöz`¡¢jΠtrmu¢Ï{ª>PnκN/Øœ¡è8Œ¥äVœÓN‹•o1SFóí4Ä{ÝO; %I~²rÀñúœt§!ØÆÃßþënÜi ~þ¤Ûi4kš0£ÂQ±i§ˆã`9÷îOWÄ9Ãx- ‹† ¿Š-Ô¼öb§YVÆõ,ZÀõ—Êœ3Ð0oúN€£*ÆK¨áœaÂì™Z©ò¥¨±ÍÞ~]
+órEÏé$–ZÖŸø‹á¿ÍP€?#â`´
+žlß0)j@Ò‚§ ~ìˆÀßç4Üß·õÕš¥XÒ³sé‚N#‡p:xŽéŠ¨Å9÷ÂÍ€²±î.ÆE­ä/W§Ñ²d…X‹ ?Lr†2M%.HqT+,U,jdŠŸòól3£:ØÅ»tið‚›!ç›:t+€…›á"ô¶ŸW’ƒ½T™ÒsjÜ "½ÙUџĽ¾ú-K² ”­N#xñêoštÖyp3°oŠ-AÛ[[*)Ip3ˆN™|’ªE½«”9§!ñÕu)Ux"û.ø`¨+”ŸÓ`»ê5\ð$)ç4$ñ°¢Ö—+b7û‰,l"»‡ò?ìñÅNðK¬éºp3 X08#·s,p3¸‹¬òÞºk_›!fáb¥àfà
+îg­ÑŸ&胛¡²-Anf
+Š+np
+Uó/ô0a]K·8ža£af ÅG }B™èu¯ f€›2:lzÜøî>ÿ2§ñ6ϛ“føú5˜ñ¹žjôr¼ŒAÀ×àvMVã±FЃ5˜ÁžÆ°•ØIGLÀ{0CÕ€cÚì´;&à¼#šb™}¿T?§‘ô–µXÝ]°s
+Læ“e¨H0σ0¦Qô‡–%¼ëK­Ì\µs1a4d ²ã|¨Kq!èHJ‡1´— 6‚žô[ ¸ýbÐ, áz«ÙòW•bx*n,J×t0‘‰„Ncʱ½¸TÒit¢si•Fh?¯wt|÷ ¥`“¾*Ëó’ë‚–-AÁ+J–F„˜ HÉD"Û~(ËBó›4²Ñi¬ÆvØñÔ–£Óˆ¡N§è~B€7Á ÆœFœŠ~ØŸ/±õ2–(©@œÓœóg‡a¤³ùŠ =6ô©-–›ÎÝi‘É]1™ÓSŒìV…˜{l‘³}YòÖX® %©¥¹X–9™‹`öµ“& ÁPVnÇÖi¿¤2†n@Õ@w¦B.Û´ÛKÙÌW'¿Ä&§Ñ—Š8 ¢óèÌÉq'8aÚ’3Y.àÉÎ
+Rg—‚7I@Æ‚eQ™Ò.i$÷‚Í›L¹ì¦Á_¤œù$ÁÇ
+$ŒŽ›FÍŒæâϵü>pÓP” ñjµ…œ¹id…+MÔÌKCª7˜É&"¸Fî§-ÖMc©\õ Ô6ñ·å<ê¦AŠ0¯a…ìi\ÁuÓìÈâòæ`¦1ìÁ…rÇ4Ý\t `uÓvÆ
+ËønÖê}!Ú[aŠ6 †õoš…¤Ó'é<8¾@ÀJ*yL9ï.ÅúK˜â‰m ø-uwØ£Mv~5]åË!³î…ŽŠû°à¨{!]t?jƒ¤zbÖÞ¦‘ñ~È…ßtŽ¿MC’}
+‰˜Ï,
+¸iäç39¯=4ù±¾ç¦aüØeÛ4FMc,XskmcŒ·i˜1l,5¢0e´/0Ž‹6q_ɼŒ
+dLß4:>´–¥ò¾9¨]Mò ”Aê
+è=Rñ6‰t^èK_òˆ§Ñ£_ñËË['â4ºp»Kv⣢žù´d†¥¬ïõÝ”¿‰7•ÿb¥$rñ÷À ¨f™.NuítÚŒp–re9×…Äi0϶Øb%Š{Cg´ÆíŠ8 T‡(¾Ð‘;ÇHœz(±Pã1Î€í¿² ¼ç!=vœÆŽ|“·‚Sø&J,´ß/ú_“J,ÐQ:÷K{'7† Îi­ºb†ªªþ½Œ>åÈi䯈2 :@\.dƒa¡pZÐÛ¸0”=æ›­…Ú½rÜ Q¡w›½1¿\øOXX'Èiÿxô@Ú‘Ó ‹kºÞ^2Žú†YbùëUZ¢àR¼gÝ°Jüx 2_NƒŒ¢[N I¦f.ÐUÏ„\™WŽ¨ Ý/7Ì©íu”Ó¨¥@ÍE‘þLÊiüªˆ^\SƒM´5{ô¿ ™èq'Ë<å7g‡BÍiÔ¥•ÓhWn÷uóŽ LܨrY=Ì  HCNzÃúå^u¸o›X¨c2aÁ·FizU>9¶3”±¾ëKóq…bP¹£„ÊӘ¸<¡1a¡wA-¤-7àÂIú †±8ô‹Ó(äÆ.¹E°Vž Fqd¨|ÜÇi¨sˆOq5EÙ¿ñ³´Å ^Hž;€xN(—®<æÂT²˜^29Œùîí-\¼PÙ’¹tÿá 9 ãáÂpãå4¢œx”ؼ¶Œ÷›®é»ösëx½›`‘¥Ò›òݯ7êÓ5Õl9 öÍ’"Ι_Zß 8^JtÎ’uÅ94ÿOœÉ•œÆ×ð_\ÐßU‡]¶Ž l„Çy†^ó3÷ä4Èw^’K¹mÑ*pü•ä4ôá\)Â2¡%ÉiøU«—~Ó~!i„…:Þ0=AbêGƱ®?4¥{sèâaAÐ?}ÈÈ”ÂGX@}fxU`¯ò³§Áãë¬ë.2>ù¢@­Ž‰ÓŠ™)–¢t1y§ª:´KœFæÆ6i¶9DEi‘6& fáœcédå“qÈ°NVà*”AÙ4~`šF?KÇ4擬£/ $*sµ4,ª2 V–VæÒ0ϞÚ4Ä3–¡I°" r*HCÉ£a^ D“h„Ÿe4^4ˆˆTE Y‹Æ’-ËØ2z¸l]®ê2¶AãËAèBƒo šƒï”
+4œ
+A+£ŸŒ†‚0«2XOYDęd…+seÉø¦àb(£w)õÉ vʺédôrYOX¹» v²¼{™Y g0ò®dÌN2ØQèüE2Jù²–GFðV_vÙSd°|™"#v!#àÙtÁéË\ƒ £^YÖ÷ž–ùýÐ:ÐᤠN_¦G°ÆË!ôC_Ö~Ç Ý—µŽ)Wv„îÅ¥FÙXèœ8FW#tÃp báp:ÏÑV_VáÆh_ÔC×—ù5€AÇdCž#ðшG Ãùbä.ÆH [ÁZ^ 6'Uú€.¡g‹e?7ÏbØgÅÜ°e­¼b  Å#FÍãꡃ-3ÅÄÐG‰Á}Ä(Âe²ˆ¡ÕƒA t† y[: —up„\ËJl ¡aøÇ0’ûÂ`.¾e->½*a Ð]Ù”\8Þ£0È4aô–<£ËLؤs™0ÂËœ.^¶6‹1ò2+QÒHà
+ #·é2Åö›—E‘0<iº´¨ +agèH/s©1¼äÉ>/k—~YáYþ²à(JÝËÒ¨! |+ƒÿg!¬„3
+ •3´¿c°Ç±/ h~Æ‹†lWZ;©Ñ²œ£õ
+åÂÿ‘ •²¡ðqAlûÊ¥lïq±²1i\+.4d¸°@¸I6äÀ%ü· I6:ûXÔ'¬oñž²mÙ[üã-–Ú-˜G·ØT6¹…þ·…»m‹uk Zeë€É†M¶ÏÕLÒ#¨ÕË*&`C|¡mW‰®S›ï´S´ºý€ù˜’“¾Ù¢UÙº%[Ð_¶>±ŒfláœÍë] žÍt5’»µ h‹d-
+«ÜÍùXáönÞb…%¼© +èD°Boð¾
+byƒåUò¼¡xÆÐUØéÍøV~½mÖ*¬f‚¼v‰UP4 XìvU˜eUêMJUŽ½í *œî-^ª8·ÞQÅùå[p£Š«¾„*>+yûž
+àüfÊ©ðð7!M…ßß0>
+/?œOáègFýb!^þå΃Ñ)Æ+,Xߗ㩾)ž^›mŠÂj
+5ÑQœi¦Àš™Ÿ‰3d
+¼ÃÉÿRD
+?ç€R¸Tq'…Ï,N}Iá¼8×$…ã\t¤°4ã$E
+K5NŸHa±OA…˜sœ Ha ³
+_ÊùNQØT9ÐCYŠ§d³ÜõDÁÞrO—{-0çŠ9Î4™»V2g÷›¹·E/Ҝ䉢Ӛ˷9+ÎÝœœ›N:°Û9dä9Ùsãi}î*üsÚ…ä@×Ð!tÿò:9^DŠ¾Ÿ(.‰1#:€[…‡'Š%4tºÏ܃Žùí'
+7õ€ÏÅèèþtÐÙ (r¡YèbñЉ¡(d'º' ÁèD{ŽŽÉ;’3–t¤A¥{ƒ.Ýý‰‚™éº'ŠŠÞtËu§û Pgä‰:’º³oê¬EªÛŸ(z«Îÿ¶ºí‰Â
+Ã:.B ˆêògê0¢Q´Èº Š9Å•¬+h(P`ò…B³B¡ëЄ‚3
+Óß²ƒ‚”Aá~
+ŠÏŠÙ øø€BÝê
+ÖQx²nØæv>!$­£%ëþ˜´î5Y<ŸÐ18ŸhÕ­²ÒB'YY>AÜ”uYY-LÊ'УuA:iëtùÄ36[wëÀ®ýغÊ|Ôpê:l.X;Ÿ€™×Ùÿ§¨ë„a^'«µn?ŸC¯ûσ<=Í'¤IësÌus>q^]®#×y$]÷ì©ëÈÞ´ŸOðtŽ‚Í'‚à:ÀùTéùÄA/nlC?Åhðž@†ëiO „þŽ¬ž˜Âu¢z"õÖT×I‰à:;Ñ(ƒž€Û¸·ôç ´dhÖ:Ξ'Œ-ûœµNÆÖ ð¶ó„iý®;¥§Ü?5ªž'DéºÀÉëVs2>—hÃŒ`gÉ‚3ŸbüØ1©áÙS[0½NÎvú<jwy­Ýí±]NÍ1nG
+5+ÀËkÅÓðŸx$‘ÅIÅ;¬óÄ`×PKñÎyb8Õñ‰Ç¬²<9i“'xcQbŒx5WÄëÒ  öñ$ïÄ
+Ïk‰3¼i‹œ8E¼">ñ*ƉūNü0^38ñRãå~߯ӛøÈÇ í&® y¹‰käÅÛ&†2m⌂_U6Aá'®ëˆ•—qMÐË«ÜšÀuy®­ æi[MøË<•Ô„QóÄM¶›'Jš°™ó|¢ ÷ÎÓ
+=·™0 ›‰,rì<<¦y¶š 13Ê2a…2!TèqG&Ä>&X©”É2.ôÔ^P߆‰¤PÂD`â`"㾄…BoÜKÃü^b<¡í&×8Ò9ð1âKŽ¢„ —ü³„Qs‰šÄ%œ¡7i·D¼Ëcè•l $ ½gj 7j‰ñûYâ£E–%ú@E Gt,¡~š‚% ÍçAá)Ñ)M­a%X% @ÏàS B±ùœJE%Äztû” þ<jJPXJp#%vè™"%ÀBÏ% 5B”È ”haž«?‰ ]!CÂNÂAÜ-‰“ 'Ñ“,› š U1òI™,zò# ú¸’ÈBÉ¿Žœ’`Í AITÌ¥ï²Qü£ÇP$¾'ÉžË$­ì‰¦o$f$vw‘PS‘à–Hì^ôD"H¾iCB>I¿ŠC3u¿èI?!ç]Ï‹zh”{ 8áŽDà™‡R]êƒ u虜>Šðf>p²G¼Ša®$ K.S5ØŽ¸¶Ž#=f*K(Ugž™tD€qå´k„bôRVѲoŽ3qD8ÂaôÞˆÓ8zþ6È¡g.®×ˆ™ªF¼L#TŒ;4‚™ÑãtFðˆ3Â@á—èù¨Å8&úù\.³a¡'‘`„(OÚË‹¨u¸p'l|ŸµÕE¾!ô€^þ„^׊ G$ÑHšT7ôK1,/Ñóš"R=FŠ E
+ìõGŠëúˆ[°g´g®B ê{/bôïñÈÜŠkÐEˆá+3!>gøºLˆ'_¬gÅGSBØN°úGÞ{ëñŒn‘Ï?„ A¡“ñÁµòqö ø/º¡ù@r$„oƒð)Hƒhc|NÅù,0“ù?¾ávÁ”‚ØÁù'” ‹ ^6°HßP7úP£ù@ ˆ‚8ß™=çûŠóDÎ'A‘²ÿƒ ÊÎù¶Zó‘ÇÙ|ãÖù®t¾œàÖ}¾Ð÷ó…„Šf'Oõùˆæ=BÊÐWe×Æ
+<±8¶@Pã ì¹@tQ#8k´mm œ»@P÷ \ l yÿaT D×·ÓMÊ°z.³ð+œÂsPG€Õ@\Æ:RÁ"Í€°Åáæç„>X6!A¶ÿ#ˆ‹!ýó_ADà7@üJ€xJDî#˜§q›€Àz_R!þ©¹KÐT¡†‘H¶H|ó®“àø‡‰ãnYj—‹)ò×±ý  õCôõG°å®b”(¨‘üMü Ä#H.ñþ* Á7çÆYe—HÕ‡L6 }ØD‚Õù0Ç|`åúñacHЩ ´Hp‘=Ÿ=Ðî{È“#ï¡Ü¶tÅèìøöÀ[{8löp±o Þ]AíÈz˜™ÓJõ°É
+躾;è¬;T7w@&q‡u%èáV%èÔí`\ êÛÁz‹ÐÚAÿì
+kz± Ãfe˜ 2”24óÉð¯ÉpY B¿j– -%#°Æ~à³A ܈ $– ¢ Ìš ÜK¶ÇÐ<;6MP㊿·1Dc·2†K0ªÍÅ‚Å Öáуu'†(1pš ö"+Õ/󒓇Йp._îΚ`€ö ôÂðÈ€c†¼Póé/ :ÜW¡{ås6 L`DÕ=G:Nu¿083€õÄ ƒZïÂÐFáâšYx1šð™[O-/ —¹î=A¤
+Cõ ¦1ah…NÙy!­à¿O0e?Aa¸ï`ØÃä`ÀbN|‚±TC¹v0ø~‚§g0ÄBç~ B0Äîk¥Ÿ`w=`ø0—4(ÈØÃýU`Ø Þô°ëžâVg_@ /à/¼™â2¾ (¨Ç0Eã ‡ù ùä¹Ç`€‚T],Xû ŽB‚ýÉÜ‚•óþ'ˆ«(_H
+Êå €ðý'ßN©õ‰}qî…óËhÜ ‡óŒzê Úå ÎŒ›Ü ÞA3yÛN0CüÒè^kDÁ†¨ªZCÁýSaí„‚„
+âÎy/˜txEÍÞ 6ær…E¢ y/Øý‘ ˆ‚@ï…3Í¢ š(øœ»¾hWjïJ }¹¤ïUï…ái/Ü=9=âÕ ‡D/¤3/è–^àïïÂ.
+Zvèµ]ÐÉ.\aìl« `ZëÂuaCºÀfìϺ(Ødst¾‰_.hHØ¿É㎠EŠ  8ÎùbÀ[@Ï-ÐÞ®Xmá#j ƒl¾,…-tQÐœkkÁVLé…?U¦í=-Ì*-„¥ÙR´0‹‚¾ž¯FAÉ³à‹‚΂q
+úgÌyYà«,Üf²
+Öm3ó‡…ôha¹ˆp Â<ƒ…bÀ‚:ý
+*茯`‡^“
+Úwò½+äÕ™+”®p@ Ý
+P£‚tžØ
+YÁA­´á1+´¨ ?V@Và„_T]`tð®UèOAƒbõƒ*«Âlž‚þ]ľƒ·×°!PÐS6£
+ßÚS¡…§B|
+öL…"•
+ä RáSH…+cT0*?¨pT(ú)ø8Ïz
+C³S€ShS˜-š‰L90…n)ìS°u¥ÐAƒR
+]T0䤰{“‚"I^*è*‘B³V¨‘Âç‚};
+×6
+2
+'¶(èRîIˆ¢°ŠCöBáŠÕÐL(HCla¾NQBṘP#e9rd–
+ò6äé„‚g¸ 02&˜§‚èÙQ*X2^¢‚e?¡É+hñYP(ˆ™P¸“…
+nT[Þ
+ÚѨåJ•ù© ]*ø P(uw*H)J5U0C¡@æ‚8žSYžPð™P¸6UpQîª`Ä·‘*8ñ„ ¬`C
+«zÜ›ä¯ÔPPÀTÁ\ °"@¦‚*BMø +Ï'èÞªTPÕ
+†¢Í+8S„á*èÕ㈧Rý`V°šà „m+¸õe×
+B£šP$È‚_ƒÕKVP¡¹XL8z„« «oŠi‰¼ª&0Å‚îÒÀ‚²Õ„ÊiÂ…<lj{ùuy5ŽÃ¼eÕ„ÔVÓ:¯&ðX&túPè­hÂ9ž ÿg&$Ô‚5™ c«i…I—0D™p#ßlǵò“ -q-8VEK&˜ê½y-“ 2Záf j’:üß— F¡KÌ›ikAv–c¿•“ ]¡m™(|rª‘OäààPU&Ø÷Êh•É„e‚ZQóQË›eBî+Ð|ø¸à´rABÏ¿tä‚ìN:g\pÙ• ¶+ìe´Ú1À¤­eMW}pQfä”pÁØ£²Y".È´»L¨’©¬q–q¹€\“L@Ø^~.xéØ]R,AoLˆDÄKzê‚…x»`¸ZªˆÂ„niÀ1¾ôw o·³ öî ì‚àÕõÃKˆ£•‹w E-ÕLi ÌÌ/Áœ¤­*òJ°÷-“ØÚ,¡>sL§.(Ô šˆ%Ø–maX,AóvAiŠVàJwA6±„÷Ýa µã),AõyA
+ ¼àªË¾ ¾žª„4vÁ\ÄèdÊa ]JØ:JhZ(aä“ÐÖ: ×vÁ–M‚1&ÁÛ·$¼HIÀO’°î‚’`»?ĺ å#ÁX#Ád”,d¤_ö7å®lò4"aT
+ @"$P–ýG¸Ä?‚· N}„
+½zà;w añ4CøKc1½4`´”«˜¥
+üU(äÿÂ'Lï<B¸BØHª ÿ‚<VSQø¨Uƒ°2  ~N1æIi~A@° jýA9Ú$•;ìºõ›Œ/Ð`A½ÿ
+…ã%³ÜÁ£Äƒ¾"hTý;`d/X½w€ìÝAeî €/ØÝVjøevÊu Â:àîõÔAÍÓAqGct tðÒs +9\/(iz`F °ÞF£Î$‹E’wAdzú×ғƲâ€î‚fõ”á`ð‚­,€û70×¾AÚÞ
+¾  é¼Ðù‚Kží1n¾ æˆ0¨Í6–/ˆ…A@œ|qU)_‘ž Îæ‚{òÅR¾
+#†œ”:7>¬˜\㠼ŅÎä ,•/ˆù‚K.˜í é1¢ï¾°“Øï©2 O~®Z“µýTÅ,{цf:ÈÁ[ì¬É Nq‘ Ü^õ“蓼 OõàM
+ª£Yz´zò‚…Ë}&IDfZ‡cÈ«û· ˜§Ü…¸.‘é/f𸺥 Žº¾ËrÑŽs.àÞÿN¦›\
+JìW>ÛK tõgé¡
+Î#`\ǶNÕ•E¦‚q6à”4 b„TÐßxwð‹~^¤Âî*h NR~
+ðÌ<Œau)."J<© ¹`§Þ/òîœÆ¯…ã˜Í¢€¥˜i)€k÷¢{P9#J§<Vqò{Õ‡­_FR
+P
+0BAÃT ©J£F|%ê 짛µ¦û? ãqŸÀ²mê¬î(mSä¯ÌP½¹' ÛVxu‚³ŽßfO k!Á´|Ú—i<“ hlËqQo¥˜Vª•vÖ Ãb!{Lt‚Z%q~¾™€œ Óþ’Dn‘#æ7=S *tÀ0‹6€…J?ú(C™Ê–C¢üi»±ŸqÔbr’&jÈf‚ñS)Šq“LÀÉ#%l%1´'Ç0ü±^§8šÐ¹È]‚öJ
+85W²uR}½Á!ÞÁ7rôVÅ®#¨ ›v
+4»ÍሼyÙ³–òíwF`‡T&ëtJÓþïUÿÝÛ#ÄtzÀ›aR!‹ãîÀaZnÔ
+‰°µB¼;0§»(;@j£ÕÅÀî
+…¾´é ±%+¾Qœ–·%$ýR)îÛ°­¥kz+»gbwÄ6ú©)¶;€‚ìÙHðñ1TÊ2ìK䶹“Ý®çÄ^Í®Ñ\Ùî@!82ï)S¡*äv†ÔþÊaQî÷ªe¤¶;°± u‹öYz°;Pyåî}šŽÆܲ;К}pSÄÕÚ1T·#¬é³fdz­^PaÚÛ·|¡ŽÅFƒ¸Hq&1Ö!Ï “·Táíè/Þ\±DË#³!ñ¼/›@µ°;`ë˨V"”4ìàž#Ógª’ÙÚˆƒñûn]XdBØõjLÆav<Ú½0¿»Íz²;P¹
+x b&:?o"‡ wªxÝUIA#É!e¼€ŽLœ«;`¶^Û7¨ê”äÇ„—‹9º9ú y­(ÍEÍÃ5w
+Y3âwS›³J±‡b[Ñ\±LŸ6µkZ‘DZp¸ ³Œ—F~ëõÕ7ׯ¦“}ˆ‹T ÆV'1,aÚ:DâŠHŽq6iƬ2©›.™Íns5»q±:ëšùÔÉ­éJEŒ¨1¨ê/YS_ÍuœX\[…ÇN I+Ç£k‘¹Ä*„d?%*‘¾r*1ôù÷ÖŸîïíÞ²_/½fÔ®ë°}ÎÕ™wü©ŠÐ†D/]¿$z ýÛ ‰ÕËÖ§:¹E©Ì«ß¬Y™`WªÎlLŒ}²±Ò˜¸ùÏš¡žô´j¤ž¼\X×é¥Ó‰þ¯¥}í‚<Ww*ꨩämIÛÕžùeJ¿ÛŸm¸vú?VÓcí|ì§6ý,ÅkÿW=‹Èjõ`ÉnÜäÔÆ>ù‰‹?¤òõœ1FÒ¡i§cý3jš ‰ÒŒLOÿšF»Ÿzß/·ï¼Tukj™}¦Â1×x‡JJ|ý‰YY*º:Y©™ˆ©éHƽ%
++— åB Òÿ+±*—gÝ]ï:®wíƒÜ#òµ CºUq-¾x×»:|uËùß‘Zšµr:S—üƒÊR^ÄÖdOeñ`N2Õsu1óŽO§øOÝŒìÕ&ìïé×
+e6½2Ž8]d¤ä.2‰)Ö>?bD¨#ên"ieR\Ï‘›’ÓC¦2,–žŒ×›úSù¢þ«T
+†_›‰˜¿.ù‡´Ÿ4CÃb}Uå0vÝ¢]™òE:åÅ°s·ÝòRÐÛº8¦Šj×Oì.|©Ò]^¾K#ä~÷å­°xÂöéeiTŸªß&ªfx{‚eºl¤ÌøŠþýu÷PÙ*“©;nªÚŽ°ƒ:æLØŸ—\YÄê5Y˜D¤3ÅŠ ZÔÙ¾³—.ŸÜ:gAš^6›ß˜ÛtÇ%&DŸ¢û‚JAAô’Œ+‘ˆŠsŒ#H&³ý Ë"A…ª§BTaMT9$¡ ”ž¥üu@Ä‹T$F±W…š0qIUD1çnXÎÃðO„ß­ª¿:yÑÂ5T„Gê„C U«ŠÖB™®µ¥ %„¬ r
+ “V˜,(u &ª RŠªƒ™6L<«¸h…™@R®já!¶¬àðøH>®ê¼Æù…éÉù•‰ ƒR+ t2P¨%&jXâð`•@H¢–JÌô bdž†¹„¡ò©X;ˆcÈdX]ï…¶€Òˆ3Ö`
+h DC¢ 4cÃOÇ2µ‡ÔÌ„ÑGn}ôMê`|xP¬JHÚÀ¢‡_6\•\¡Jáðʪæ”0ŽP&FØÂ#z„ÅD«uÎjÐKˆ(h¦Q¸gæáïƒÿTŠ:E–9Guð \Â*z!
+äMV2 -„æˆ. Í0 gœ@$ä'‚C  .|Œp„;4,á¡'Z¨¿dÉ’ðÈ@r6ÖƒSyBO$EH¥¡ÅfÁóÓP'ÐLhè¨4¾Gê`††.–q;°Ó2±Þ&:ÕÂé §B%BI5™„ âƒ{bUy 3_ G Õá¥<è}IâQ $ôŠ2áV„i+¾Ë|
+‡(
+AŸVC¢±Ø¢Šb5!„Z›T<èPˆx'„”D‚ç!H¡È9c„¤žx‚…4HÏ 2«@\ê PÂ8 ¥v4Œˆ4,µ@i£¿™¥?¨$”äm@?
+:^“‚8R
+Xv«2» ‘àhLÕ~c3ý
+æ€T„Íúc‘¿¬„'/‚âA…@“½J›¹f‡$¯*h9NõÞvvLD›³²ýýÏ• ¹c)(h”Áá­i;ÀÀÏä_Q}èe ÷ápÎe#l%Lu,[úÚüÂ{¾Ì@é¶5Ò`ˆ1^1V ù%rÒ”$6Ö?}õ<û©Â+r%ºQ­7ë›NhŒ
+endstream endobj 13 0 obj <</Length 65536>>stream
+Ìu¯s Hæ]Tç¶j»$Ï€ÝRâ"âî>ºÛ7“º¶W³;ÖÆîþƒ­ÚTG^2£Õ Ÿ&sóôžTñ©i¯NÍÜ`Á³U>s‘ËKp\Fµ[€'âÕ÷ý:D€¦KŽ¹å;器„'å¶lê÷aN¥ü)0$á
+“(ЋÈSÛ½Ži'h‹ÐKñåºÌæ²Wõ§½)»WÙê¦èd6¡dâDó©G‘ElwÏQ¥æÜdI1+ªÂU'® a`æáùG.^zç=¬j;äß‹Ã×I.@Áwfz?/$(åç{sÔW‡ŠÝ$þBFµg oMZ·ŒS1›lÎû@ú¸t1.§Œ:> ÖiüyÚ¤¹°ºVD9"Ø`W
+SÈƶ”ÃNÍùpÁ4²œ—õa&\hÔ€Êû.1oƒ}Nb7S±äçõ²Ê ž•ÙÁBКƿöfy=ÁÔ¹ï½=°"%“íP¤¥«£—0æ/_ä,t`$ïÙæ‰svÊ»#útñöû`ñT\}/¹‚ >¥ä%{¡€ðÞÀvxËÜÂØú  ^Wã¾Ñ§•­^(8µÉ³a}!ì9vlÞd@H&—j.n\,'q:ö‰ŸtÞ±©Ê5ûEå{ú Cn8Áó‰jÂ5->GmŸ×ÄS¯`·²ô؆‹Ï’€úCÔä„Ž=/ºe(äÀ?]ÂU šÉŸÇ°tbrÆ+æ×o/ [±
+xéd~N…d0ˆQ«èV®vp ÃÁ^vìôYð!Êß\½gv Û‰Pì×p@&ÍÒ
+ì2(7„•¢%I,Z”üäjë“™7ÅÊn #‘šñ]jTÍØ5i²Åb1­r ý„ Ï@àfœ‹d ·!*â*ÿÒ3#t3]ÖØ›÷
+€£eŸàŸR®¨à€å\jã€.YÀ¤çéÊZ`кÅ€­Bü65Ò-.È|I{uš6@aÐÿžhúP3 kJ[ë’H>wê#«ÀÙEÅûÒ6›¨?ÚU°Ìmx³{ˆÎ _0ÝvÃC(ÚÉ®qŠ¶{\œh@ð\ؘL!Štk"<‚JoF‹tOÉŽ
+SÂx岯ã¿ø‹ìç5wÿÑÓ×d=mÁ”¥ýAFvÞWDí , ]]~Íaäýdì¥W›Óar<…Ÿ™Ö~[Y}A„žs˜ ·úošKM2P0$~O`†êbG¹ë‡'*®ƒ>#ñÕíåº x=°ÐŽ³­ć×Zoäòvý~–Œ­´“ëøUƒûKÁ[ï)'=”9 (ã%³Â"À•xèpF` KåT+«r‡6J{4†ïžo‰¡{Î&'§]ƒ™C¶™=¥»
+ æt@A’8DŸ¢àý½ó@Xô<ã}ˆê'&gé@¨rQÀ‰¹,_Ø=¿Ÿžd3Û¿½%£qVǹÎFyºièáø ó¯SDA…L“µÛ8B̉£±´ÅJ­ÿxÚp„ €¬´Ãåwƒ—†@˜DGb/üð†/Æ$Òzvü译ŠD÷*(×e­Ê%JRN<_1KÊŽë¡[ûÜ¡ZD쇑<?±„Êк/&o®½d$N±[ Ï)ȘŒYÞ X0ÎGÍ…‰Ü¡–€ кaðèÖìÛp%lØ€!™oÈ:šƒ¶ ê¼ïåˆ)ÔÜ¥¼Gð02©Hôxpg8$¨8q‘íÍ$4ÌeNŽÎ7S?[¡‡y¹áAB´¡DìýrÝ ®[¿ˆ Eêäüc%Cç'ô@vÕ§jkŽiáQéZÈnU$E1f¼ù)g"æy Þ+<»¼äå×sG’Åv#×RçËijSª}¢ØE¢Lî3Þ5m°ã'†éÒ¢ £ŸI»g%¾òÀ‰¯/Ó—
+h»Ë,Ø{ûÝ•9–fSéä¹HÜD1—¡c×5ËvÆàõ¾`BÉùýÒ°Pè!OðÚË W¹=óDlÆ“|{ ËÀY£WöMͳ«ã;Q2¯@C\,}Nr¦ä‰)gOÄ︅Z—O^Ú´œÖE…‡–ð>ÂMÔ|sè´švhˆ‹YØÒE<ˆ… 9æibÒ°œ%é"Œ·®‹x‘7¨»NW÷ß‚ 
+µ‰~j ‘‘{HY†ic—ON¦<©‰Ú“£úK¤GÔ׈ë'TòYÅ,H
+DûýéÄô1qUÀÀã‡ä‡:8 ÜŠïº<qFîÝ×¾;¡&(üM¦²|ìŠöb]vÌ.ßk7ÎÙÏ„d•%;öqy´hñjf_ìútMÌ)›˜î^5¨ÂÞB‘‹úÿ øˆ@ܺÙælâ;#¬™ƒú¸Ô˜ÉD:…D¾*J¤ð9˜X9zééR]ðŠ&òŒ=yuµ¬G'|ß2ňJÖî`P—à>¹˜k( ¨¦ŽR€Ù;Ë­¢òãÖ'pܽ3o¬e³.$Öñº
+WÔî”ÇQuSí(ìDd§¤Ôb{Šw€¿v5Á±rÕiÕÖ!L’ùh<5P~ª¼ÓÔ “ŽèÆ(èuÔJ¿aOÁzŸùG0†°ûãpÆ®
+Ž§&Q¦#SK°Ánì`ýÎî }å þÝ•ÇZD¡ËŸïô耧L"§Å™ZQ' á>!Ësßji†ÓiÉŒ:GïçJ?aôv¨wHBÿ£Ùk{·–3fDà|²Š3"èöé‚ä8#[Sch-Øu²ôdY£*5¤j-@Ö¸HÜjÜwp ª=>ÊKl/ WõèzÖ„ßh{¿o¤¨NHwí:ù!nr!0ÉtÞ“
+õsìì´‘VM.ƒxÆ÷ºEùiÂE6Jö‡øPõóÙo–xó˜™ÞS—&:¯dù6tt¤cbU¸·K¨fAó°]ðÛ™!ÿh&ÐÌVƒRØÙ4zžÀ-‹tΛè?u`ÊC
+- Œ¬
+ÆÀî ;°Õh†IF³BåL‡|¸øþÑ¿Áj&ÜA«¿6”Ç–î‰Ê<’!ÒéwOÍ„¿mÝ
+ß͗à¨z@YÖ3áñO2™ØÖ33zùµó“ áãªÿ6ò eÂ"+ÓGÿá”i> ý5ïF”e.ñj÷Ÿz–Í4Êø¶”©OYcÂst‡nÝN_e
+G!Cž¬õ̲oRˤª· @ç2ø¡ÆÂˬ 5jt¨ñ³óeÝ
+:ÄëêÈ(¦¡¡“Àìè°Š…™UöF1›¯i¸Ø13dµ¼Õ?G1<1‹+1»FÌäCvüöb–ˆŒÖ¼†ðY³Ü:Ô@Θ±Ž1 t1ƒZÌÖq–ô$‡™fÃ#¤ eÕ‡F‡Véx·&aj\f3fDfeÌê3)¥/ ËD ”¡dÅ f~•屬íaÁ˜U™)1éý*³écdÖÉ Ö62“¢|×ú’Yoµƒ~Y5@Ì$ü””Â-+ Tr$™é;°â‚Hf£]—Ìní2+Î’™ ˜Ë,ÿ=QÁ&º< ÑýIQ# Z…?f.ÈY­þ™|±˲çVQ@r-0Ó‹✧ÐyM-£†Å…Y´;_a¦r¹Ð-5Ú³ƒFuú›0CÞÝ»#1ƒEw³okÌ°üÆ èÇìþ0/£[p5¾±¡kº¦c¶jÔ°øn2[hÇÌøÞFq™•îxXÖ1#£“”4(3!½˜!3 )U`IÃeÏTøefâvf†¬y!¥ǧfpÍÔ”Ípßmân& à¬î‰³Þ’3ÅìÙœ1Î8x?Ó-òØt ~gs #LuÖ®=ƒ8j05>ûwÔ`ŸÕ×ÏÌï?#Åm;!h†3hñ(Þ{}F;CcV††4´ÁE}·i?÷¨a™D“Ê•0E«\-ê‹=‡†&ƒÔh‚´y±ZðQÒªwÒì ˜f#­›¸´)Ó©L30M}ÔØlýº4zp è\;jNû:j„ßiïʧ)5zµ¦–¦e<jð6ÓîÓ;jwJª8j;5ÚCŽÔ°Ôò×Ôª>jjMªyX˜†3ËZä‹<ñÈ5šêj
+ïËgûrÏÖç¸`À9¾£ŸÎ¶;g£~#rJŸMhcSñ³aïÿ?Pv¶~½³-Ž÷ÓwÔ`æm7Ëp}A:‘îÏf’DG$m"Ìi/jÓ{Ô°oÕFÖ†?jðY›ƒ¬ÖÆÙ;jD´VÛ®Ìò<G ^z[[ÿ[Ûì¨ñlk ©ÕF µ!nÚh´é3ßqfÂwö$ó‰âYÒ6A=j(š¶­T¤Í„”»á»~¨{w;j<Ñý ÇÎæŠ@ÿ+I»ýÙ
+O´5ÜðlpœQã±KJ×©Û ¯Õ w6F ÏÖm@›nFJ´æãmë®-ÚlöŠÑä ÍˆÙ»„QêvQc- “
+àyÖØ¡N~¨Q‡Ô jŒ3uz‡êrQÕ9ѬŽ¯Ž`±=œuòC £kà: ¢²ä>ö'¶¨2’싨“×i ± ” Jćº*`‚Èu^—žºŽ|]'&§ß…ØuŸŒ¿®ã5ŽÔuá:D¢;uÝÈßRÔðâ=¯sºº¢Fÿ¾%©ºpæuŽù-j¬¨ðë,çu ϯÍë~5j­¨Á/ô:X5íy¢†
+ºô:—ø5¤_À ÁN”}`‡ æÕ+°»ÇÂ. Åζ4ÓïØ ÀÔSì
+ neìyY¿Á FßÅÞa›”=«µ½+¦½ xíÀímå®ëåž'Ô=ÄA ºÝ» jÈ5Þs&ÔȲ÷T}…ŽI€¾w"²æï•Ü4¢x¡FL=³ìÅPãí¹G4ÔpúÃE(¾´¨ìüZ8ßh;äŠÉð*üPc—Ï¢Fœù掠ÿyiÊØPVl<Ÿê¡†èC<Ñ€¤o®¡Œ,…[¾ìMöI}hÙ·¦j 5!µ€Ù
+íÿ>À}Ò
+D ˆpÐqA`¸-»)†rO «.”"dˆ 1­ênN·úüBjpѺC „Réò‚hC "¢ y¦®“ÀîvM·8U‰»#ˆ]=‚›œÆ—Y*_;AÈËÄ[N#`œ†Bä4L$Ø<@–ìžq;J¢*ÖMxs‘öþ•â>‚<Hð6‘ v96pªFÇ5ÄqÜ=r2"Aþ ÖŠ$¨kƒ’ Hžúýv.AšÔÞ–F9P—Óˆ!§1Ç÷u;X‚ œ†Åq_äY vWQFâÀFs>t@O` Wºº(Æ9?Ýû¿ìÍ‘%Z
+¹¸R#åÈäÆÍ^ò@1 TBI£&ï é44@Ýu ¢yØ•4ÓÈ°kC—7÷¼|í4´g• 0ßi4‰$ˆ.%„¬F6ž†dKãqd€§A:
+¢èlÐÄ[µü
+¢bX&²úŽIðì-èit‘a‚(= ŠE‰òíij§étú¾«§ab›§Qú’§aý< LOã™z/C‚0"O$˜ÁH
+ö6A¡§a³¹&øª§±LOCeÒ›§Qh¾6Adë”<íœçiœkMPz£ŠÎÓ aR· ô'h0z 8G­ ž§q‘í rèi˯AJØ"ÛŸ§Q'i‘ìMÚ/ÓCŸoÖ¢ø¯í=b\òoÇÛï>A-5Oã›ñíýéiàŸàLê‚>½ó4ˆŒ< Ä(?Á–§á <^<XŠ–~¡M¨< °Åçïûó4¸eåiÁù¬u¼îúŸ§AÁUžFíôó4F‹A‚ªTž†ò  ËT
+‚b÷-ñ…O5wÜihROFëFìIˆã¸'h+f=Ýi`„'8[raÚi4ᶪ„(˜0L?<ÔÐDA@¼NÃ0j÷ûê4xV7ÂÌ‹‚ÖDA øþ¢ EFAî(ebñ†ãu£ànQÐñ`ºë4ž²WJ‰i‰†§\3í×iÈr4ãKëé³Óèl¦C‚Š|¬Ûˆ°Ó˜©†ª£àͤ0Á‘ðɆ³éßj?ërì4àëVí4òk;£`ÖŽ‚
+vßÊÿ!’£`¿Q°êSÐ×µy»zt ‚0w€~§¡+*è €%:’w©ÐÃÓøÃx(QÁCY›
+‚Ä´T*ðÊÓÆÓ@ RA¸àiרà(f}ï4Ô â€§ÁÖž‡
+&x0È;x<xßiÔ§ µÐí»Ó
+
+ƒ¸­ MÛiøŒŸºnFÖ
+âTgKU!Š€ot>¾: ®OGûaÁK:A§ *û
+¬Ã¨æ•ÆJÊiœñÍi
+“S¯>Çzªå.h™fÑãð‚£ô9tÜÄ¥é4,zÁ ©’æ<ÙÁ úM§Aú¿\t˜ù.ØÒæ4ŠÁÇ l¶]Љîç_NCä5øÜõÌ[´šÓhoð‚Sï» !å4P ”—d¬œ4€¥÷…Fˆ¼—9 l¾äÚN*2z täÇ/ûÒi(P‚´ÃÐjÿYì7FMyAÙÿeÖô?`päF}©´:§a­Â*¡0Áð¤ÁT[N# ã4ˆá4žL%#5(&„LyÓØ ìD7k à,ýóëöyÓКˆðïÍÔ-ÊKú V% `õsÓ°”Û¦ÁO›`$ÖÙ/x\\„}Á¹Yr×å¦KÝž”ÚÜÚ4B|AW6
+3=Nã Ÿ`PäG>’fäÁPœÆà 6#뮀´r;b ("§‘ô}Rθ’Ó ŽJ)ºùÝ Y¯êCu‡Á’ISV¨¨…Âo|ÆSTì]Œ ¯Á˜ìœã<ø#3ö¶›£güXN£Œ>ñE±- ó ¬ôr`°ä\°†á^\,:€@Ü „RdDäÓÐñ˜ž¨LþÏi$,·ê9±ßõ™ka5°©ÀÇ ¡Î³yú9 Št4´âCÄfm38röhMÈ(\–Ás“|F”Ú¹KQ.6³:§AEè_¬DGdZ¼ŸÓ(é.KŒç4Raš¥TÉYòñ×ܘKa›è9 #5—æ.‘ÎitTJ02‰œUd÷ÐÄ‘íu»°Ó`Iü·ÎLF
+á9ê!1Ts1¦²ŽWÁšÂ™sÏiðÖ@ P]AÊöiIÜ€UIZq””c9wt„÷%3öY–ì' s6lÔÇ(™0æ‹ÇÚÝFºST_ ä0 ÑMè4‚‡%>îuA§æåJcŸ($A
+:ÍZ"înàÜŽ.— ¸öÐi8k_.`È
+¬O3tƒ2ò”O#=ÂÌ#Fª+/Ȇ\¡³´ßX.—é4’²¥:OŠß$Û¯!Ÿ™Ì°­Nch;€¿wó³:³à ’?!V‹ÓQöìÂ+ôp® Wi%±"èI#³…¢Œäwþs…-©V®UªF:‚¦œªé¡£ÿƬxîßšaB ó¢,–怡 F]±Ó¸=½†J³¾4âèZц€¿lŽbv×Ï$áN£OþÖPßÿ7Ð>p)gˆ×1ÄÕG®©ít§¡î'ÃI=¥œ—I -&< ¸„6f5n3Ùð4^ˆÄ8©R3Âþùì±ô§‘7S
+q—4(<–TSÑW”OÂ@t×<)lIÔÐ(ï4ÀòÐÕ1Dyp\³¥ U¡î4"^Q!!¼Ê ë’ˤJi»â¢7x»"âÀî4TÅß,Q™ww]ǃ»ÉÀµ]ÅìÙs×ý&Hì3K‹twIS3ö{L‹Þþ¸ÓàbÍ׎ PUêâŠl‰ƒF øôî4쀃š7
+ª,ÿ;ƃ ‹‹kk˜(“Ò%úé©5ˆžPeÌÀ†j«;b!fº|§ùŒñà(8̲;ñÓ_µ­!fhõc<Àfܾ|KsÌÃp§‘ítZ k.{¬ê“²oNìã=2MOã&?ëú+2A|}ž•}§‘GÆÆnÞ>ù= /+—†èð¤"d<  f¬ɶCïx_+ g §Hyå%E <
+Ì3ßN3ài¨Û[G$–/< CH—1á\a.陌1¢Ÿ¥xñ4”5^Á(R¥ìòpÚÊ•x&ì>1ñß{#CÆC†iøÅŸÛÝÍþšŠoF‰ñ4²cˆ :ÂÉ*)OãV¯cÑ_PBQ*ªñ4Ê?ãü“áön+§qïjˆÿ[ì ¤NÆ*cÌÿµDƹänê)_¸±-µF“:·xñÙ>¤S …°÷½Oƒ, HõÛWPU.7#I‘x=þdõ­ohUò¢ˆ¢¥d<0H&jœ‰ô*p³·OY+È€o£*0"ãJHÁ‹ ËuP”Æ.׺‚ÈäÐz<³$e¼!(zÐ;2|ŽŠ¾q{ÐÌ—xš'©¼Ò¿á|¨¿N “Œ´GÿÉ 5‚Œ‡v¥¨gÖx7žÙ\¨,%‡Ó^Çx˜¯¢Êxñ¼H |‚ AÇxȲÔኊ§a(¼Î"ÐÁl< mÒIŽè›..Ôäi$Ò§ÃΓâaž†S^æi4VTv"`f§£LÁ­À
+¯°ÝÔL_ˆ,!=Æ­9t—å<N¤¢˜+PF­h,ü»3OC½ªš3YÌjçqžÆn`
+9LOƒeÐ*#Ãö#Ýz`À²B¶ÊÕE°žF¢“}uôôB<,Þ ©q âa&üòv¥:HÚÊÀ(4Uãrî!ÌPÊ)A° à
+ÎÓPfÔ•¡‡‡{ˆ´Dæi„Œê/|”yÊX¹ô¼c|™Ä
+›Kütó4
+uN¨?WµhÒ…æiô9XăÎ0à á#À$ fi¤ì¤Ð±t±9¥0O£˜.Àf´kx´«RÆÓÖdÒžµåo{¦Q
+£ê@ßã·êÙª®qSv
+ ½e#ýår@NJL9 «ÆÀûsÂ< âÉÌ7s­+ìn<Æ< @W‚C¥Œ5Ÿ!õ‡Ï![ú8O#±d®«Ž ˜Fä–$½ãGOã©5 ª1žc’‘©’ÃÔ•{UŽB«w#> Éaêq§ÔÅB‘éz”ÆÈG{3àÖŽ‚ØäC‘ÆäÏߟ[áíî ¬æ½¤%Â+gœ–šJ<‘æ*QŽF-ˆ€­V2ñ4|+ŒüÅ.Oãú€HäN[ xFžF’æ2ÓÉÓ8Kêo^æ‹’‚¢NKå¾Õ¶8,ÈM!^òþ¢(¬(æiÓ/ãóÉCÿ7¾[q0ŸüúP“X…y&˜‡köÇz3r+ xÅìøÂW
+õýW´ëIjÅãþà`ôtäŠCÑàÿdžFDÆ3n“ }á:»â 7:=i$Â_Œ(Z’\“\èŠÃ¼1ÏwÒ…ò4â®òz–+ôr&ĸâlX™meòcúêH&Ð{‚ã Ù;¾–5¦2† ¢µd,®8´ÀÝþj¾iÿ¥Â¬ä˜+Gâ¹ÚÝ‹ŒOòz
+ÃàP ³;W!Õ{À  t:Áa]»»¡.‡ÿÕíh]=Ä¡îò4ÂÒ“9ªiäÔþ©< úA Ê” þÚE*Oƒ
+-kô,
+üi$HÜ\ö°ç$™§E–§¡â(e9Sà0g[…ï9˜#Oa¢%ƴݲÄÓX7[à/5ô"
+8ÂKh…¶X<1Œ;Í.G›\Ç%*\—µï4„À¹“E²X¥ÀNÆA‘4n;a;-84u4Faòï²ï B+ª†ÜF?OR² ñ¡¨N78ô[Bì|þ“ÉŸ‡å]†; jšy. áºÓ î,,{â¼M™)d;|î@>ðÖÁVV
+à0F®–;­CKZeeþ8´Ÿ|uÏÔó-8”ò#Ã*ݵ™ÚNƒ0¦p ¶œ‰vµÓÈ(Ú ÑTÿçDÈfFÐÕí4diU=n¯þÉð…ÚNƒÐºj±Ûk§¡ªâ@ªÒý“­jÉÓ‡
+à°“A)Œ_8Sƽä.$eK&Զɇ Ñ>1Pê6R ñÍ
+ìT²Û"ü rW{†ZT%¤ñÂ凋‰L*tòÉU¬B2ª+ŸK>92@TÀ, ‡ %bÌäY­ yVz[´¢²Ç…ئ$"Á篓‹ÓßÒß.—™d~yž\UÅ«Eòó$:ý¢EÊqÒz ñ)ž{EÔ£èx7Ï[W­ó9É—?ßaÒ’}Nr%ŸSîø½¼ÞÁÅŒ‹)Dƒú…xÑâ£Edæ‚í5I÷ê™rZ¯–±ýæI§,V¡ÈEäݳ%ïñùì3®TI³`‘dˆ¨Xþ·-nÑ°_Ž³\‰JpûÊk«fÖU¿&w ©' ‘”*#0‡é´Œ
+„c¥…C¥ÅbEBáíÐβß,‘+Zb¸N."ÿÄÇMðZå®=)~TU/IùãZw\ýˆ|&P ax\‚SZ¢d–¯%þýBò¼<2§¿rùù[ï1ôœ¥_’8è•Z5ÁôBgyjUjþåx‹^«†C«©’Ù•>ƒ€øo«p:GÍìî2¹å’›š^õ²®Ù}Í®
+o»xöUÜÒŸ6¢Ï<XJN<~lvGê×UøÚôª¦˜UÕq §W˜þœ¶bUÅ·Mv»uÏ¿zæ¥øq[ý,Itú‹DÊeH˜^»pú¯+yÑi–‘‡‡‹‰“˜ÐÒß+ú%Ák¨Mûs4Åkjf_8ŒÚ]¹œÒI<6‰çùsÓ£p¹ÆUšANú{äŽaµË²_ÐzþhÙ›ã*fQs[âß"ø,:KÏ’Ò“õ²ž2¤CUÏ&ž¼eä1ñõÒ»ŠX”䪠ÅÇQî¸úëR0³aÒÏ"uŠÙ5ªUIæ¨s)5M°zrQ*.\hœ´jX¢˜Ô‡e˼YÎá÷r™.†¡´ìIå¦,V$*¬–?'¹“aN†µ ©gì–Itz³+˜}Íï(5ýr„¹Íå6Ý$SrbÏOJ‚Ró?É}K<yLjt2úwåÓrÄ“»|Ø «ü9ËGWùs—ݺz0‰n£àU'ÃUÜŠØð*nI¬ª‹^¼mD­)ÂÓ)ý%ä“ès̆Q0‹‚Ù¼âe(zÕ˜^Ÿd—ôª¬ÙEÉï»~Ðs'ÇÙë^-ÓEæ´wËfëMò½]üF+šz×ûãvÑ‹ÕrKȳêU D,!Ï¿ªv9Ú$øQÍ[üøs ­'É-£à²K³ðwi=ñSéí1*†KpÙÅ“³ô6 O‹Ð®
+^G-JrÓÖÜæbç1C— ÇtZD·Oa~’9jšZÕÕ¦­7mùè)Ÿ=å×ìvÊUýµé
+׉%ô9½ljnQ2ûºã‘œFÁ,êe='¹š]",-£d·eÃ*T (uÇ W5ѯŠvO|œDŸW3kòã+¹©'?†¨6UµŽ¿¢#—m½- ü’Ð0l~Kê9rY^MmR4Õ2 ~­ÇÑbh‡ çios³ v©¸ÍGï“–®WÙø¨]Gh©ZQ“zî ×‡IN×ÀN)|ví5ɯ³lz´®Yæhw?Št§¡›ú(ºä¹¤¦>)îdH{Û r¸§ù¤ø1ÉZ†Î²¤¦£4 ¡) ~¸8Î`({ž ~p·ídøAIüëN+ê¢aTÜò¦˜“ Ÿ–=J†T“Äž±[nÑ.i5Gjé›c v´§±YÇsŽ.ú±cO)ÞßvY_‚$ ¥§É][µ»ë‘Š²àöĦ;êcøQSøëäÐÃEÐc–¤©æH5=¥Xwþu¹ÆÖ—â-~²·É\ö—⇠s°óI1d–$MµjÉ=£èõ«ŽOqÛ£d’øøíf¸›¡JnGªiZO‹zÌ0Õ¢0þ­²Ûý9šøx
+n?è8BG¬ŽÐ®H̲ZTtŽ:ù™à†ôAÉ¢¤5¹ë=‹N²´š"¸œòáaD“½V½«-QðZÒÛ©Øý¤§ë–GnªšßTÜþhBÍ¿Om:²ßµ8öæ˜rU—¡
+±ü;‹†?ë©‚U‘œ>½ëH_»˜>*˜Mµé§ñs3λmqçÝŸ§zÙ‘ü&Ám[¡ç~6ع[6†¤ 7›“Ó99›|{Òc -O­Š‡^ÌilÖ ¹î^Žõç^v¤žzÖ]W{^ÿjºn[ºi%8æMôÇž,ã¬Ss\Íq(§Sr8pç©Y¶fOŠ¾þcȃ`›el‡3w\Üuh'ûMôcž¼(zFòÕ4”³A;œ´Ãy9íÕ²šÛNiºzÛÐ9ÞÖƒžÝq÷ò$‰fØÛî°Ã=íS†Ÿ³ÌÅoî47Ë\-›½Næ´üë|2ÜEïö¶I‚Èq„ž!–¥AÑ¿žG©éjÛïï‹»îÝ´| o1|¹í䮤ÔôE÷¸YëˆÔ³©¸­ÃOÍ:''„Þ2‘w}Á,èÛl³½Ý&ƒh-]<:I{ÈP¹ÏIŽØTCxËà¬CsÚ§YðÚŠY”«údè)EÚÛØlCƒÛ¨UC)ú)Gì¦XÕ/EìtR¬E0¿2!Ð Jõº!Ôü˜¦iMO+ú¢åS Öt»ª—=µªH=E©‰zÙOjî¥{œüy7)þi©‹ž_Š!´¥&z<ú#(2ØÎÒß%9½‚ÙÕ«šXT´ž"{½ªc˜-›ìöêe{ð{7ÎÜlÎNÇIyú£Ø‹áO’þ(æ^kYâ
+2E
+–Ñ7Îî6Ï‚Èp︽ëî®;7œq³‘¿Næ6tƒB%·(øýO3?—Ó1CBÒ§ö:Ûãr°S±m*UùÏ›»®Ý¶˜ãö¬C°Á ù•³y7þ>vÛZn“½îþº’YºØ5†‰Ã4úÍÑå´4ë¸Ü¶‹¡ˆ?c2ÉÐYòb¨fpnRôçxjÕ9þ¥¨ƒž§Eê©‹ Ë a9)êN™c-EkJrÙIâÝkt“¢Ý8öÊ j׬Uz.×y¹Îšû8ŠÔTä®W6m—cÉmAè)JÏ9¢\Ö$Ã$¥©>ŠÔDµ«Ç$sñ³Án¹ùóê0´Ã°þ<Xëààç1IIÞ g¼eð–©d–E»$WÕGÐÔª¨6™£>†v~RÓô®"½½®î8ÚëZM3CB@)àîÓCåpàMóˆc†ú×ÑÝfn6dÈfÕ´¹Û^­ƒnP°M?Š§5M±+Šg§£jÚªi$‡“á€bÕ´VÓ\-›»nåtTNçäpÒŒËp@ÁÁ‡û4Sð}Þ¸´Ã¡¿ïät¼!]Š³“©šVn6«–Á]§r8ꆓr8aHȺu±÷Ϊe"U•9¯ídeŠfH›eç†s†„œºÙÐÝv]œuà§ípRΦîºç37(T¨L#|ß›%ímo–©œŽÚéȧv:hÆ»möºŸ4KçéÃÿ4Aªz_þë¶Ù·Ím´ç¹Yfßož6’’‚Õ:&ô"K~av JKï”»ŽÝ¸´ÓÁÁϤ¦%•·,ípÌÍ8tø©Z‡Õ:=)Êœs™Ç?çè“¢è$E'É` vüiŠÞ—GZëàkYÌeî–¹÷«(Ê®³ö´Z–Ô÷)ew1ÄE0?öëBìŠbìq¼øØó Ÿ»è¹þ>5ãÈ÷ŸfÜujÆ»,'Á\ ët5MålØN'Þ¸˜ãd¯“;Îþ¼ûûlÐûOS/Å9ìä¯ëÍr?ÜãhÏ›¿/Ã= I¡r8m–±Y†n6/çÅݧfZ†äfc¼e;è­à–Ã"õdþuòÖA9)ÒNHÊmõë¿.ÿ¾[,íÑ<;Ù‡"DÃEši'‡“rBìÎ3CBÊâ&ä IafÙ¿²ž3u5íA‘ªîao«qh¦m8 7›uÛÖ,ƒ9ÎÕ¸VãÞ®k7®ìd_Hˆ™qþTu·nþ@ЉúâhwÊéx8 øp@‘v8l–¥ŽÜu3ÇÁYÆr6ô¶ÉZVwžšqJöÇq·ÁœæfY¹ÙŒ›Í’BÕ:øúåÈjY«eÿY†Ö”Ôº&·ÕCPÞ´¹ãnÐkµNÚÙ„V–'Q}4=&zƒá=’±çÝ ¨‡¢ìq1'Q‹Š¯wR´=¯Õ²Ã1;œ=}1œ7í/É}án+9!â¹ y¹ Õ®1§µ\礟kBìªÖÔ¿¹ÓÞnËÍ3ųÃÁ¿Îî8zËên»AÄ®¦vµ¿®ì¤¹éÈ}ëâHw]«e·æ% †d§ÓvZI=ÿräÅpÃÙëànS5NšqÄ&'ä½;S'Þ4ºë⮃·måtÊÍÆÌ81Çå h{¬uج“v8jÆùO“þ¼´³q·ìÅ[$anK3ÎÛmö¾ÚÖjZý2×ML2†„œÌq0Ç­gÇN3?Xë¸8ãÐݦƒž¼i:è©Ôÿ¼wã\Îk7Îíh-' Q (CBP'D¢CâÝ8X–P4TžѬ»oL‘bö<Óªþbˆr6åf“fše¬¶µZwv40$$´¾»¹Ò ¸fYÈáŒYGå8–Óf1,5_Y•¼Ç¹-Î64·ÕŸ—ƒ ‡$Ý, S¤@3ÍþB·“ÕÛö‡béLG¨
+sœìmï–¥ç<m0d7­þ@=õìCï8s“Âìl<*Ú§èüu/Ø]ÉíØ‹P3Í6b'Ëeþ‰¾ÜwäkÎýòÌÇ2æ8¶³Q9›Zü~À°äàòò—¥z$³•¤‡$S¬:"Å9ãÄœF²÷Z§»ÿ,Qm›‡á zï–ÅŸ&£$Ö¢ë?Ï”Ž¨ÕÔCPö6Uë¤Zçý:\ c-ûů¯,ø-­ê_Š¢µ µè~’±–Zçô²)¶¡)O’®¶Á^~xøÅJϼÖÅpÕ8f'$Õ:ùø…Ît–.§ÁÝ›ßSšºÚvnBÚ¬ójšÜqõç©ÎÚÙäà‡‚ï¦Áš–r6m–Å×n«ik§S]‰¿ÚF†¤07VÓÊ=qÌ¿PÞ¸µ£Ù˜jc]Iªú¢é? }ðÛAÐ#Še
+jg£ƒß›ufŽ£=/Þ8uÃñ0
+”Ã19l
+áÛy¬×ýÓ4'IÞ$].#9!dgƒ“#)UA§IÃ+óÌZÓzÓè-ƒ3Ϋe¬¦™Nˆ"E¬izêáXwÌi­Ö‰¹,æ2vÓÀMHÛuw(®N¬e|öbè!Eüìð{»­þ:’9†Áî_Ž2§ÑWrÓÓ»Â7v6¸I†ÒÕÍ6´Ó¡¿Î¿ÔiŠÔS?Åý$ï𠡦 E{¤9Íî4ÝÛònÛÁo'I´ÃÁÅ°´¦“ÌCðÝ2\üXpû›¤)nw³D­êJ>“Ðð_ŽõÇNQå†ctZî8xÓT,:2¿µÇùeȲÛ1N!¶Cpå4÷ÛLòýS¦lÖ‰¹M„ž-·=µí¢í–µœ&bUü¾AïsŽ0¹­ñ÷:n­h/~«¶¹Ëpd×4]³Î>†¯8™çíu0§œ»mž²”µÍíd*‡s[)M}q¤»nͲ´Óq5-æ¸Úë@8ˆ!(ÒNg CR¤Ì9($emk3 åpØm›=¯ö@ûWM ­k«¶å‹³ Î2™ÛÈkÖ™½Žípðl3íälÊMˆ’âäpÒN IÑÀ
+=‡H°Å¢"“ ¥å-[®Ãr¸ÛêÏ“½íƒŠ$÷;#ÒcDž×ódønì~´äK%³ ô©)o’,—¹œ&{Ìm³ÇÁ'æ8 õ\Õ´'=û“©¨Ç,}´,áëW=ïd8bSÑŠ¢à6¤¢·øádøz]üy4â"èŸ#KfQô]¤®·(Ú£¨fšÊé´YgålâMËÃPE}ÝMK;r“ö¸^Û§Õ´; ñ,9œq³I3­Õ¸’Ãy7 =Y†Æ±Ý¸/d“
+vEð:´ª}Yî$‰‡"»m(×Ñaf%õÂ&6•ùø#õ¤ÁnÕ:jÖ17)>-ªBeêM4'EPŠæ£X‡ í}q¦©NÚá”^XÅTËhð›?OG^wQlô‘¤?oöºúóî0¼ÉS&ÏQãºÍ†$Jù|j¯“¹ÌÝ:­–ÁÛ†f3ËÀ9>£†
+Žáms³lítÆÍ&L‘¢ÜlÔ,“Á/ݶ3ëüeY{ÞÚá¼Y¶fœuËR­ãv›~–2ØÙ_'s›>Š8øÝž·ƒ!«ióò¥Ir8î¶Õgw^EùQ¬;.æ¶8ôVxŽ“¦Ëu8I¦YÆn›šecŠi‡\-~2ÈÌ•¦¶×¡›5ëÀ÷n[ÚÙ¼Ýæv›Ûqÿyþâw8o§#kšíy¶×ááW‹ Éerèåchž»mg'#9›‘³¹ÇQå83ã˜XÖÄÎ=%©‹àìuª¦¹^—›b ¯ó1D¹æ ©…‰Ô—â np—¹\'í2“ݦ|üØûBëÉÂï#3}ßÌm°–µíq¥·•;Õª$výŸ¥Üm­Æ‰·Œ·Õ¼ªø9H=W<·Jn‡Nò9~ôVoº²Ó3 þƒš}ªõ2ž'Aäüñ;±(%E¬iâÁM° «e¼ÖKòí8—Óânƒ=.»˜^÷äÈ‚Ù_ßG0ö4wËàn«AGIRìº^ZÍS»Ö ×n™ìuõçÅÝwž²®œÆ›$Üuq×Éçnš–Uî“EõÂ!4…¹îÛpºÍæãlRnëÓ´CVëˆXu/ÇùëÔNL‚Ü„¤Ž‡[qÌ›e.Šn–™!!åfÃj›.Ž0DZ›&r6\
+ð¦¥N¹ ¹APCjþ§ÉnÉ ‘?N$~I'úr\«emÆ™¹M4ëPÜR¤p9ŽÇyãÔMÈ«u`-ÓE°ô¾|ºžìõºU-[µÌݶVË>gé“ã~ñ×ñäXŸªe)'¤Í:³ÇÍÇv8(gÓjÙüuõç±ZvnBâmÓ½ÎCen[³ìq4è½Üvv8¹(æ"9{_Ú霎’‚ä„ÐÇ‹¤ýy=)þ§™‚_ÒªŠRÔ/Å4 ©Á­%³¨·•=Í/ÅÏ9ŽÔ’¤~YxÛĪhÆÉIpõºü(²ZÇSŽ|âà÷r«uàN‹Ù² ~Ów‡]%Kb–…vYl˜åª2¸ñfè—c vŸt©Û¦¦ÇQ+z‹ v&9=rÓz ®àUŸY|x–«öf¸›!v»)êe¸rìi¥V5Éí“»ä>‚/×ÙÉOõª3ȱdE¯K. jÙ=ëRT;íWIÕËÒg‡ÞémUî‹b[O9Ê_ÇvÛëq¶²™Öjýyú(†›N!g{Ýšuº(b1Án
+~ùp<9œg'D‡ÉŠ ÿÛà7¼i¯¦­ÎšqNÎfÝ4üÚ¬Óv:ú÷á·r8cHŠ1$ÉÙ¼Ûö1ÍwÛN§þ¼ûëz1¼ÁÝ4³³©?嶫¶}»'EÐYöãH«ig§Ó¥@ñvIeóÄ?Áýûx¼½îìp¾3~PÉi’P”EšÛjNóŒâhU?ço[›udn‹=nÍ:´×}L2÷ºÿknCP›“³17!'gãjÙ/’¥ó|­*‰Ë†Jòݶq³y·­þÀ6ÛÞl³Ã”š%6ÅC°Ülêð{Õõ(UéŽs3N›mìÏÃG¿{üf°³ÃneÏarBË)‚PñGÇÞA©Yr××ãz“,‰aÖÝ6{Ü zþI–ØtÁ˜ËF§˜JIT:Š¼*
+ ¯ÜÕ=¿õ¬ÁN»¦ô|½ëÈìšØq+nE)iZKŸüJtÚ† e’Óž3 ¥"
+VK®ªŠ]zò'éz*nc4]RS_ï.ëÅOt’«Vmá÷~Op[d–ëÏÛ×4$Ãýzîè8ZÍìÞà÷ºeQh´º¹8Ò^çŸ#ê]Q­ZZÕÏyÖžç“e.Š8„-‚½9®§å6Òª‚N“ö:3d3nBJÎÆÝ:¤tÕÃñ?jî`8s\«ij§ãnÛËm¬–¡œ ËáàÞçfÛé¤NÉÙ˜›MÛéàß÷jÛše8øåá·z]OYÒ_·“¢EUn+BQúë>&IJOôänãGqÅxÛfŽ“·MÖ´= ám£;îöºôäŽK3ÎËmô¶ÚæâOŽ·×Å›fwJUSpµ¶ž²§§$çÏ;;œ4ã¸[gžãò®«?Ï/ÅÚÛØŒ“rBêÏóE²î:‘9®ÔSdŽ{Úßo[še&‡sož)EE`TŽ2·©§7ÇØm{Ltípæn»¿®„¢£¥?Îå2ùãòôËÞ²½ WB}̆Mò™ët‚×})Î_GbÕÐYö#:Iìºä—´¦"ýÏÒ/E’)z@n¥·e¤J-;^rÓŸÓÔCïþ:}M.Ë‹!Ë]cü¦ÇOp:t’> î!w¢Ó-P¤Q›¢êU†ŠF,CUªùx@fY‡^üq+!Ϧ4è…Òò¤†Kbw…žSêù¿ëÏ+¹¬éu_­£š]–çuק¸­¨H=ys”9hT,_m=(
+“á¥Ï
+ßbÕjö¤È‹¡_’©–Ýcø«è’=ªÙ¦Ã}Û,39WÛôQ¡æÇ$ÿr”=AWüŠPô/KçŽ{7MípÒGípÒÍFͲqÂf™›il–½šVs\¬im–µšæjšÌm óôI²½Iþä:ËOYÖ_碇_.›=§4ÝÁoåpÒ·Ó©=oÜlЬw[OŽö÷µYÖfWÓàm‹7­ípìÏ«¿ípòÏ;©kèDEhjZYŸ$eÏc¹ Õ2’¢fÝó>ãHBOÒYzF½>ôøÔÁPÍ8%wuá÷—Ë’N3¿XÓT-Sµ,Ö6˜ÛD&ù!Åß,}rDµiiMm0lµLÁôtðãÅPÁzª\U„–6Ø­Ú%´+aƒÆ^·ävæëäuªUC¨ùAÍœUòû
+•Ü·Ê]Q­šr[»†L2ÿ¸üÛj¼R2V­Wü–Ô2ËeQ*
+‹Ý—ü–Ö´ö:j¢Ö””–°ÙEµ*ªM]1ë’Ù’›ê§ø¢„U€8V¤L*~£ãO9‚Póƒ–-:ž=o7Eþ®ùô"o›ROjþ&Yƒ^ˆUEm[{\ ¨Wá{ ûiÙ›âç$EªiÂã°½ÏÓ5Ç0{Ãç0wý!O‚ûþ&©‹"ºe,F¥Ðy²5 Y·ŽN’°ç…!)HÎCG§‰bÕ›zJ±ÿám³¿ïþ@µÃA9!6î$ o›’" IQr87Âk[Š5DZÎÊ᜜MÉÙÌ^7RÓ$á®;­ê§$KéérÙIÞâ×r4ÛØàWÝ.‚Ÿ’´=oÞ67ËPÎÆKÂìlT­3{œýyñ¶ÁZkÙÊÙ´YçÝ´ÙëVMK;™ãhŽÓÁdž¡óôäÛmi–™Nªiî¦É݆ƒžg S+*»%±+¦½Ç¹Ú ¹¬Êu{ðË;.Ö:ÿ8êâw‡ßºi!'¤'Cñϳ»÷8Üãö1üOSCxË쯥iªmQ¬
+*Åîã÷v—‡vBXm»j¸FÔ¯ù*@}‰-ÎR9Ó¨G
+Š Ž³ÜÔõ²­·õ!îq¶·áßÆ’_ªî£ØzW‘—=‰]Ùu­)hïÏÛEq¾||^?¹éÖ»–Øó.Áü ¨Pi@C”ä»JvY3<JÓÓªŽÐ3¿UÛì¦hÒã-øýKQ”ž&55©((5ïð£AoA&Q
+ŽïQ­é«žGdXÅ¢Ÿ“¬E±ý¾_UQ-Û“¢gAeƒYÔšŠÒS/I6üPGî4®^vÅÿy²,7q‚v:j–™›ÚëZ2|rÏ"m
+*ŹãÞM“·ÌÍ8ggƒ’çVmƒÌï:9~Ê¥¦»ç‘)R¬YöfÚ’‡"/Ž´÷ÝaXÝÜm-·Á!øŸ'¼i¼ø¥RÓC†ô·Ñ§!è4s$©¨~4·Íœ&oZím%ÅAÕ²1Ýuñ¶½Y‡ípämóIÁ´Óa3 Þ8xãÎ Gæ:= ?&É‹¢-‚5øõåø—ä›qÜŒsÙ+fKl?“jÊàt+=K¤"A~ôlËÅÏ'GütŒ¹­Õ:/§ÉÞÖ—#~~I–ØÕ».:¾âï]¯ënÚÙèåhra¸ÛäoûQòÅ)ôcäóë#MÑ° Vc8m’×?:ŠØ¯Š>¿d·Ä¢¦w±ª5=fÉ¡-QkúrÙzê'süu4èÑ'w?‚?)‚JrÅ®|º\¶rý¢ð¼§Ah™‚Wí¦àU´š+ûmåuë–gL!Ø}Ã]§‹ 5S¥å'%Emš‹ ‹ŽQïË“$Ìi/—ýe¨jUŠòåHƒÝgE_¼\¦ÞÇ0¿½}“¤Á/ö:vÓàNëÇ¿ úã·Š[Ý&Á+‡‡³(…b³,BÑOIzÈq”¦»(ºWr6¸â㨋`©}ñÛIÑCŽµÖy³ªeuíϳ´²%ÓÄ=/÷:ЊH²ÃuÛÂbŠ¶÷Ñ]×j™>†»(þfé›%.†hÖ9)~”tÑqØÛŽJ‘T’}òcxƒßÇ4m¯ƒ¹n ¯T´¥¢/—ýKqö¸³ÃA93$DåtR'ípFªzr_ºóbn[;YÊé°™6wž‚4øÙ!H‡_üyxÎݦnR¸Z‡î6XÜ–Ôtå²?SjØTŽ£üE¯þ2ÙÓöäG±Õ²rs\.†?)ÂZæn«ur1$­j?Š"sT¥äè[1{²×®ÎE±;”g‘Bµxx&ÑJNŸØ²U³¬ÚUѯ¯š8®`vˆ¨x°L&öÜ›á=~¾zæäøzœNŠ¢²üG‘d–žð•#OŠ3赜†fôò1ÌGðcŽ<øVóäŽKjùGÑœ]ö,ãձ甫šTs´š?k²nXåßM0<ƒáÉeSv<&æ´ôGÏ'ÁßU1|‹"»mvIöfé²Û1½î)ËuãÈߦ¢a%%}yîey‚]Õüæá'wÛ»eo§ÙŸ—ƒ ¼mr·™Z4FÄ»(Xp»¡ÀaÆ
+ zð0Ñ÷’š¦ÔõÊm£Ô´†i§ƒv:òçµ\§“áª==$XžÝqrÆÁ½nöº–ÛV-Ãðöº“³ÙGr„ž"Ó´A±ätÂ&gSwÝ«iå&AÛ®\—”¦÷÷…!(HNŠœ cs|RQþ"ø!ÇYêbhƒ¼i,–m©+HG¤˜JÏP)îâ÷nZ¹ I;œVÓÎ åpLr=E÷÷P¬½Þ6ÚûfÎ{·-öºZ õ²”?OÕ2¸Û诣=?ûãLª2Çùã<èø)C_üüìÉ”’ ÔüM?5ë Îìy5øÑ—v6/§Í —‹á~²×ñ#ˆZËÔjÖ|v¦PLveoãOFl%e”¢¦ÕTµæ‹o¿0yVx:³óøáéh%¶¢$’ñuWìþ縓àG=}³œA/ö¸6ëÀœ¶‹/~¤ÔôËѽùë䯛A½:ôtñ+¡%Œ^¿øyìæc˜—âK ÄËK_Ž9é‘T³$wÜòç.ÖËñí:Þ,A):Ë”Z¢ÒÒåª"2ü£(Ëm,·b™?ÏT«¢ØTCùãîñ ­h‹¦ñPLÁ±kžA(*s›ímS4©æ‡ñl7.ö¸ÌÂn9Õ®ü¶Ÿ÷ês“©Ô²(V-gˆEf™ƒàÚé¼›&wÝ™uîð¥æìu1·é!èã‡o¯³AOö¸uËÆMÈÙéСèrÜìy½yî¥i‹"«ilÖA9!('Dæ2üxRDµni]kг?OÁ\üÜ-³¿î/Iü@§9JQ^y1ÌÁoî6xëˆN’t’§õ4¥g~7øÝßW{ÊÙ¼›&JÓ|ñTéûÑÊÚ!o[íu ÔDµ,iUa¯‹¿îô¶+®Ã/î4˜Ël ™"Ê]»ìû§ÿ³­é–$õü d.z7ØÙç!G]YNó¨gŸ¢v9ÊbÈnÜm¸zÐ2¤žµØ‰Ö&·1že¯W|[Fēܵvøèý¨Xb¿,BûKo—è´¿špÈõ,z›ânŠuèZ”5·'ùz]]㎣ï7E<ôZó£¢>“V”¤š¿9Î^—‡ è$U-úâ‰=Sr[d–YoË“â(vQõ,Ün†¤Öd½*ln_ô[zW5]ï35 Ý4;ô\ïZRKT‹¦Ü”E¿¬›&½­¯ªµúfÙ›£^ŠtèÕa§“¡ˆMMíºŠaÿ,gÏãÇ1t–¨65¡å/‚ž’üQô£¢žô´Ç½Þ$°RÀ
+ëÅÉI¿ª";îbeZ‘"¹tºOËOjæ¥ø‚35PNFö½GUuëPm 7rÃiµ-þ@|,ÍN6n6è†cwŠ]ItÜuÛ®WåúI×sÄÌÔ8QqkåX)y)}ZL >ïPÑ⃠ À
+e¢ëPˮ꺻­Y±l,†j§ý*Z‹ZÄhíþ¨MIpXEêC;ÉÀN>̘¡yÅ~~EJɪ÷è¦z‡¤.’±ç¹ÝÖrÉÙТHƒb>’Ÿ45¹ð_–9øÍ_'ÒçKÌxA£„„¥Ó%=-SògŒ¤bP –ÓOÓ:¡€<®{Áç“»‚Ð3ö¾Eã0\9.å8ÖóÌmsÃïdÏ$¼Ž`Íis—Éaƒž†1¶w—¦ ¿Wü¿uÏ{³ŒÕ2×ëvÓTͳ¦è&e¯%0„(‡­”̧‡Á®Ýq)Ïr0,\¤„@KÐhÁCŒH
+Ëi{ kql=ÏTÏ0®UŽª ®IÓõ¾Msó¼G2ö<b´`QR²›&I¤„ =`Nx¬Ð˜D¡um¹Îõ<(%.ž÷¨kJŽWsüªé¾$e¯CB\Ï›Yµbž_¿Óë–ä¹ èÓÊíSüž||TM³òš
+Ì…ÉH(†Q´\#…¤uǬ.ñóØ^§xø“Ûîåȯ© ‚¿ë¶Gr8îƽœW‹céuk˜ˆœjº&G3/8LHN´»«$~Šx9ÎaøiÛØçŸK“OÑOͱ~dgCv6ð×±æ˜Ël¦¹[Ç… å÷˜\w7Ë›$å”AoEÔ1†
+Ïcû 5\*ÐÒó6Iv›`ÆÑÕÓGS•ëˆB$ÖÝÉRö<¸ãfðs½Î4Ë,ÛþMST–/=ƒUŠ!ý$úÃuJy>/ûâJõðÂ…L*UãZù˜Q¡á"’²¡<yÊßs•ÍÓÆç0¥ÑJŽñ„¹ÍÃzmrœEÕ2üž\¸/K\ K-[‚ã»,]ðÛSÔg_9y’dñxDôüDË¥yîßV‡Ûü]]Ÿô«hXÄÓ(aÁ @-&PŒ¿Qóì‚ᎪŸtÍÑCvÓX-[5MÜlDÇÛlÆ,³Å‘Ķ¬yfÕ4JŽO²|Ÿ&(~eRª_]M°º¢Ô«ñ)œ¤Ç[x­›ænš#X~AñEüèÁEL ŸQ/û£&ÜyêÆÙç™rÚáˆÔò‹G_½+oŽô×É_gv8˲`øm8g‡cƒ…¢±:©ä¶È$=d{[ÙÙæ6;ôøOõ I‘ŠY,0"X¬<ÁãÅt°#òaЋC°$ÒSÔ»4ª‘”×
+i'G´ÃIµ,/É¿u[Ïk9mdŽW˜F4§ )TÂç8)zXV‰€ð¼ë†… ±â |äp9™A‰`FÕ,¿ ÷aÕ“¿çSuÝ4| Wp›’ß0:îɱítPþ\ hB¥ïsªj˜‡1?f^TœJ>HD`°V¹(ªæ–+1+Ôi¦A«:rÙ—~—ô9É~×bˆâˆegÏs·îå¸jžô¸ŠG­hšqÊMŠRŠ¡æ‡JÉ
+SV¿¡ÕäSótÏ8€L
+oÏ€t(j†[óWU¥dò¨nM«ô«è-‚?Š†ÔôÁiïóP„¨™fZÓ~ßÎæO͘·üøJfë0ÔpH‘¡±K“f•ÒOåpÜ-ÓG1î8ùó`zÍâÑi¯#±è/&*¿Þš]–Ì’Ô/ŒÈg1‰b¬œÔpQÁÏÒ³¦"üý¯h^ŠýYÚá·z[˜¯›$ÉÙ¼›eM“R„žsèíæøQOÔ × ÇbÊÀ<„ P §Hëc¸vš§9žEÑ?M»zÐ3ö¶5ãР§›c
+h7!}äµ(˜’
+$у>nÔaäuŸ¢2è½vüX€6j¤pÆÒaâÒ!æfô«äWÝ4½,A,[‡b»i0Z6ÑmýVéqÖ«Y‡Ý2¨SIÿÓ!h‡UB:–’'±&°’¿‡íxX̨ˆ¥p´ˆäh!qaÍô(~Qþ5¿§ÙEí®{.µ©JvGnjŠÛ‘~÷¨éÊigÆYÝrŒêä®WîúQOû,ñó4ÉñÈž³œFÒÛ=¨Pá!f%I‹ˆ.FÀ˜9…ntü±Bæ
+
+%ä¸ ÙIQŠ”z]Ðižôúç<g¯S±:m`‰"^Ž[µNoŠ0¢Þš•#t¸x€=‘Ÿž·I~Ü6CØëTx|…¿]ŒþÞžCïKÂDÓªún¹ÎítFéYzÙÝ^ù÷z²gÌ ˜ø¡‡LŠ k¤òÁY||5»(úMÍ®EÓ,£qb¢22á¦)ƒž z$ü½bUú‘ãÑò =–é pмR©;>Éj)^oõüÝ5ÆçFøÈ¡¢D$æç+Z%—?ÂÛ£õÑ’CÏyÆŸwv8ö¦ø:hMmìͱGOUË⯣A™Jíëv¨E[üš
+ÕœD2&’^’f'¤ìlB±kŠãZÂl8%į„~GÆ4RɱÊe*!?Jì…+å¢_–cú˜æ¶oO",''"8þÅÊ3).^©ÌÒ#XŸ¤š/OoŠézUtŒ¢çžq”9®ö:<í1ÄÏÒd×8ªºÇ’Ý—¿e„¦ôÏx™l¬TuìqS"«–Ñša¿Mm2¬Å¯í¸|]WÏãSõ&KÙó~ó$¹nheóÒìO5Í81d³—¦hEE©IJË^ôt1”Á/KR,÷©J‹â-Š*J!\J\2œ‹aËm©–ÍŸwâó6PNÌT i)1@Ž¨1ó£%fƒD•òÙu“¹-¿².(ꥉ¢ß$¶ü’_<ôÜnKµŽÈu?1 r:·æ B’
+Š”½ž)}dÀP¦ù-ù`"‰,-4©‘
+‰UË%¸•ÅÍ4¶óè‘t=´²)ø-¹.*†í0|½Ïìhg¶¡—á€Ãëy%=Ÿ‚á«ê¤h‹a-Š-×Ú6Ôº­»N½-(M?)ʢ竲℞%‡CŒTII+e¿U0Ë’Û˜‘gåÃZÕêo(žœ‡§ëÚuê¦å%y—åëq7)¾]7‡cºqù‰’xôš–I¤¿UölŠ]Ü®l¹¥ãºz–ØsE‰Ñ‘ä1‰=?©¹"OŽ:I¦ZævÜûyuøᡇrUQ«Þ&ùyÕNûn{bŽW*5VAâ…eãdUó:‘f™í:0/?Z´øW”Æ«>üàaæ¥%Ô»_gn ‚¯7ÉrÈegF ªSH´JSVŠŠÐqÛm+)˜8†IŠz#·<rË"6ì#ÅÄŠ$"¸ª®œ>+Nb&¦È'OѱˆÃxÜDÇ%Ù-ÅìK¯gT'-÷¦RKUÜÎœþ.#9¨ˆÄ|<ª–Ý$ƒú¤|zÏŠö©é£f‹–aD!žŸÑDÃLì ñ#¦¥†ÉHÍÊ”òÙa:²ãSýÂ@ytÜÙ™”¨FEZyHò;“#«iu~TÕýD÷ëHtœ„Ï[6—%~šó8–Î* €V¼VBëZ²×0¦ÿä¿K|;¶Ï+#åßw˜¸^@>êm?éI‹ š59¢]×q8¦]'ñü~Šºr›u×)Y&±íìㆣá€C=Š+ûn»îOQÓÛ’V•Ä¶h'ƒ½/7M{$IïëB*µê;”¦¶çé è!IÏiŠÎÓ'IúûH¯KÂï(øeÍoŠG?Í-
+é?!‰üöE¹nõ¼Q,ŸäyäpP-ÃI’Ô¶¨™®G²Ì´;M_¨°Àz¿•ß°¼­iNŽ¯×ù+ú²a’|nÑ/iEY±»’a–§Úvö¼4ËàÏ;³ÎÈÙxRFry^¶<Òç¹Y®ž·nš‰n¿îØU·6DL3(+·û4e¹Nå¸X }õ\±*Áˆ±FvûeË(ùmåú¿¶/ÇÍ ˜ªë”ÐèIøóèÜE2%Ç~zö'yŠÛ›÷1œùø.~~¢Ë/ ÝEÔ«lzÁh\p„*\®??Á-,¯Yöü¯é vtèù¨ ãyEû””ù;œÀhœB0>^RÇ¥4u5̓š0`,Ž»dx†ê´b‚Õ´jžu4¥K‘
+É‹èGñíϧã(VU6\“à§MK±ËŸg’1ø©_Wz]HÏGsrÝ•ÇeÏ}9Ò]÷nÜNš²çÁ^·rÜë}2¥QŠ¿£ÖUÇrÃ)9¶ëÒ3ÁïÉÿû¨s[/Š%3‹e’ªödùnÚÉ<i*ÂïVUÑpˈ'ñéš©¥"Á¯‡eE0L‚ãÏLó×5Ôº#–ÅIò̲/EŠ’]€ˆ"þw–ÁÐ…Ï(ö¤h>Ž¿šždÄ¢&¸mÑò«žIò6Ï*–ÕÅÑå6“³S H7­GÓQë’ÚöÚÝ®ÓÑS/K|U.+ÁèIŸ“àWÔºêÆ•™ÆvÝûuûY¢h¸³6ÝOÄ®ýiÆÞ§›§‡moó¼Q3Åë¢Wµ/Š§OÅo>†³×¹×r\>†zŽÚ†Ë'ýÍ¢å5M<¼«÷O4M£B­júô¾!5 ©i’t­in¢c–Ñcxâë(»®jY“zšÖ”Ä®¼Y¾§›å ‚±z–ý«øíÏò.ɺ,yõÌ¡beˆ@ì\Õd§s”¤hŒ¨^ÀN-\%—È4·*û5ÂÇ#Æêfå„ßE©º—eËqbHË®q¤¸|Ø—Ý8(½#µxþÓmE=íÉ€"€
+}ØuätLmã׶IwãÖ,s¹7M0Ö ’Ú4CòëÊ°k–G«Z! zê–Ù¤¨ªiÓ,›x~]ÿËÒÜ„˜›rRnBD°«£¬ÙÙh0T}bzRUOŠÖ`¢@† €ñv,bHñ˧¦È~‹Zµ&EÖëN-#3Ž§][xz_[WrÌšeÝ<i±D7NäpNJ#"vðØÙ÷ý¾Q«®àWõ¾&–õÍÝ63ëŒä¸‡3^{OjYѪþ)z“#Êqñ(¾<ž>U_wݪé¿^–”–;¦·zü}®ç©ÜöiÛÔ\“Þ7Ô²ðçœMÈÙœšæY×›õ´äÙT…Û+û½/¯š"þ×ôeÏ+HŸ”þFÉmÈU[:­¢Ý‹’âõ3»z«ªê¤žÔ©*É}I®{‡£šeøXÒaˆ‡LŸ‹Èñ(=?æ8‡],v¿{:ñƒ$*):f í0\%©Ï‹o¿äV¹µëèই4òM9!ç&E¬uæ-‹7­OÕûTEN–
+3Ëf|¿ïý:“Û!è£gK×gqLµÜpÎŽFr:߆£ro–óþêÊšim·v¦ç.z•&íu~Š–æXC mˆ°
+ðBE‹Rt’¹Æ^ƒ <–"'C3MÇÑÊŽÖV´¶'÷½-¯ž4$¯0.Ó ¡¦(Esreð³ÇÍ´÷êÊfí}üHºdÇõÒìRl8RŒXWÇ«–ÁžrÛUwRìÉñ?üN3ür"ð9I5á¯3µÎNŽŸ ­k-†.Ç•Úv'É·ãPm³K³CÖ÷•µÉ2Ü„Ü&™ò÷ºöcè“£«mh¦µ7z[™•‹O]½,I쪒eMy•ÅPõ8sË`ýÎ2"RuEÇaB>Ì–C.‹Ÿ§Êq"‡“$kç?obY›vÍò©mQ®+Z׺4cr¬AóƒÆw4ã0ÔM3Ô²|Šî)j“cÛi-|æÁ¢¦õÕ&I‹J^MúûÓ¦µIžšfv8°ÜNÍ4šŸ4ý¤ë¹á´šv¢ªikÖÙÇÏcŠ)˜=áoGÅoªŽC±ë‚Õ8‰©|üØ^³f·Ä¢°×™[ªßOMßfYn6+9FÑo:†Ñqˆmç±;™™i¸y²[×jÜkž[œH&ø]j×?5CìÚnÚg]q¸È¨]è¡Qµm7ÏÕ­›à™Ô¶ VS„ð&ª%ÅDãf–ÙàGbÕ‘šÎ>8H\,V(^U÷’$Ék7¹«‚xøíc¸Š]ë’íwaª×nªf'C:íöÙa(zWX¯{ÞE˜Ÿ_éwšâ#˜—cÛq↣rªi¨¦õc¹²{ò‘l·MípÈÍæK±Iµ®GÛ2ÛÊ,ÓÓ3¥*ñôŠÊÖj”²›¤nšoçõf9bÙž=¢è“ä-Šj–)PÔ 8‡anž –åÓ5í¾RãJ­ûR„t)R°ß—£èºi6è¥ÜÕ7ÇQËÂr\ä¾ñq$µ ³±]©+†ö8îçÙ§©Ž¢.ø±žÇr\k¦Uõ¼ŸåziǵŸw—æÈŠEòÔ4ÏyÖH™Dêù7É]aÏ{C0&G”Ï»D¢ÐûÂòšå³« …ì²49ŽÔ´2ËHò+ñßù.M“ ÇnÚ$·Ipû† Øé” Xkµú×ÕÇ$7öÑÕå:–۾͆K‘"/EjæåXrÙ?Eûó4Á¯iŽ[N&”(IJ,%I†C¬ª›å¨m⦅Z&f“ÃAáu’ü&µ+(Ew²´E²í¼9íR 7™Û,PL‰ä²ïÝDáïc¹mÜ„ðhªBJ‘m ÙØ5G‰ˆ–\·ÇQå6E
+»<OJª9mSŽSñè?TFZ¬L/N"?'Éo:]ŽÃÍÑ&ÃM]·ýšaW¼žXôÌ´/dÓ·ð 7,a±ÄÏV•û0]—9•Zznj[~ž&žôÂó8âåhâëy¯çñ¨Jv² f¦yNõÌ´p³q=OÛž=,}«ó§yªPÉ '¨TϲûiÚáÇ—"ýu³×Á˜j-’>šÎÞ·r8ç&Äí8·ëì0ÜS–NYžO÷¸}ãö•\8v6SÛd²œÇQÕ´»¶ø:êm}µî-–â¦Óu8==ƒì<GÑR ‡\E×(ÚÖÓuA6 Ó®£IÒý¾7{·ÕW•OÓ¼,o’;œ.$Eªi U}COÍ’Mëm;£i½¶,Y²$yºoU^Ÿê7ä®.¾#i£k]¢3)¶bhrÛŽ¦£ÞIÒc–;Z¦_Ç~Ëm·½rŠ¦_¹þa]»DSm#³Ž·Ùx›]’¸‰¦w“ã¿®¿šªfÃ#ø•ÅÐ6E$xØ`I­L² bÙx4Wï#³Œ Ùx8¤pÝ4ÉïÛ'
+‡¡Hï¯ôeë£9Õy=c;ÎS ÓÇ<¥ÿï5ÆÑ6EOªâ"™
+±hQÃC•–jõLnK£g‹Ó9Ç>p{6Ów4éSE=ðË:jèé*Ú§é †ªÇá¦hºåû}iVËó$Í”<G{ò?µ Ϩ{£î¶3yæéj“è¹y\ȦÁ}]kó|E&G=ÙÓ¬e×…L IÉÈhtÒu9eßmÇÒì@×íQ¼ÑwãËÕbEŒ"Œ(€<0‚
+ 
+<
+tzITõÂy$=hÚn\ªi$~_#¥ÄÄdBI©qAƒ@8ñ
+Z
+äÈÁš˜$,ÿ™Yg[<Eñå[—oÝôcU}­V#"'32vàø‰ ˜àY
+\@Ì ?èA°`…&D
+1àAÀÈðÑCËL ““‘V‹e$f<èpc‡/×6ŠÖçÙ³-j×C=ßÃâˆ2`,¡
+SP˜ð',¡7Ø
+&,’«»Nùø'»&y|˜—•~È %
+H(<HPB’P„,pá
+f@ƒ~P"œÀ+H V°¤à
+T@J@Œ@![ø‚À01@A
+N ‚¦à…0xa
+X°Â‚à#$¡3˜ p€ÀPÁ¢å¥ø¥çë÷­£ø¡g‘ ‰#Š€€*
+ZP‚œ  €lиa’ZÅ´^%yÎKsE·û^ïÛOsEÉÈ" ÌàVÈB²à…/Ð
+M(ƒÆð0x¡I0B
+\€‚ü€.ˆ °
+SÈŽ
+^ð#”hâ=xôàá021•Ë§o‘M›î:&úg¸ )@ pƒàÀ
+[°‚Ì@)XÁ
+LpB ð„$ŒA ]x¢`ƒìà6°Á
+bPŠ(
+X %0ÁaÃtÀÀ€1‚ " A
+W˜Â¬à
+Rà„# á Z°ÂÔ/„á S Â 
+LX‚ˆ
+L@¨
+2° $‘„6jÐpÑ¢…‹-JZ0Rÿ©W–d×#ÿKI"p€"ÁYÈBÊ@0¼á n
+9ðABpX€ Àðà bdT´L§S¯·¼R/VL`V)”’‰z˜ à ^P†2”;ðÁ `À)<! :Ð @  ° $ØAf`Ä ˆÀFÄ<ü¨qfRòŒDöîÒÿ4}“„1ÂÚGŒt 6¨ °…*”¡ g€ °'(¡
+W ‚šÀ¼
+xB #|ð±D?
+S€Â¼Ð0xA `èB\Ð,‘E±Ã@z¼À¸äpCÆ̈ÈH(¤Ú÷“‘ˆUÛ*Ÿÿ¥ã¾ë¡8£©Ž‘ø¡‡œ €D°ì@€Àž € (€p€€€%6À€
+ À€'ÀAì˜ÑC¡¬F-/MŠH‹•<·žç³,M«…ù銋dý‰³(ŽŒD/ZT
+„8ÀÁ ¨P) ! GÀ@$ 6^Ä´ iya!™Á&Œš–)äã6Ljõž;]ßQÄU””×=f¬(,ЃÈ >° n
+Qˆ‚z€ƒ¨
+É@‘X±{j1ÛŒ“ç)ŠfZ»m¨×¥9™X´X,º¦=ät¼ Š"|/r[1$„ëq, ¿&UŠ1‰NJ¡W¨§)Q©Q­ø{…Û¬›VÝò)§QF(~ç’\¿n¤×}€Yña"‚jUÝ${ÔDÍnª†e\&_viTDåðë¿oµdCί*/+~ØQ#eÓ)†ô1Ñï××àW‹bëu5ÝÑ“í83ËÊN'Ôª*¾¤¿çòCÃÍ5âШñA}Mñ24ÅêJHoᵎ–³øÑ£¨£è§}}žÇsädoבÚõÅÈŠd¿µ:ÆhÒgWéïßÔ_ËœÂðxå“Ó¬D*¤On–¤Ø]\L$9GNGäph ¥iŠgéq½5]¥½ ˜š—”—Èá°êÇE:ÝÏ-ë“ûu<³+—m Ä^L!_E_³ÎL†!X鵫žZ}91ÍÌŒ˜H»§nó– á.K¤¿D:J‹ºéÏ‹¾ˆô&,•‰&Áÿe=ð‹zë÷QN(eçSLaù*R^¯‘gd»šEq‘|È1
+fI­Z‚YT §Œ< ˜¥Í»¨z–1^°J$üýqU?5{°˜Èqý+
+bÓFlÕ‚uBñäúY†Ž©q*šÖOþ¼ÜO3\’_Ȇˆ«»¼jªjø…Ï$=Ž‚ÕSkº„òO¾ÒíÿmcŒCå¾'7Þ£d^†û9–äVeÇ;XLv`Ñ"E$â!fÅ
+‰„ÄŽªÛµhKf]þ=™ZFh¼L1%?;˜tÄŽ¬œßMñeä¡‘B"³BÕœDC°x‰ñRá¥ÓïW^ŸdWÄ¢°zeÕ.Ÿž V]ñ{Ý$qT”kAòò´hinIt:Õ¦,žýE+ÕÊí%Q@»Œ‰×Aåecdeã$Vâó­Ç¡ÝF‡àÛqôþqËâñ˜*‹ØK d’]”¨‡Ë‰Hn÷\!õ%ü$Ä—|úü¾ˆfÙ‚&—ÕMÒGK?5EôGÉê
+–ëtÛ'?s‘ðõ
+–HÅJTâED½ª×í}¿†Ï1ù}¹+ŠNÃhW5¿4)²_÷òø&9öQÓ=}ž!‚Z¡™ì3Š~q³LÍñë®ï2¬K°oQÓüÆxöß,ç¬!ãr
+ G
+LJŒd¯U²»›¢HQó"RË!´ôÍ1媰;>ñk#))&¤8.ù?,¢PËèC“•è´ h7ésý,ÿ5õ¤çHvWÀT¸X­ûæSs·¬{Žõ¹ÌŠäCÆÅäÿe¼N.Ÿ½D¯_@¼ÌIôªã›¶ P=Ô ñ‰ÝÐ^#v‚é0>Î’Û‹ŠÜ³&A'"GðÈq²m”HÆ+E„‹š$b°èA…‡ËH «„ªg,ÔÇ]é°SAò
+½x|oUEÝ‹½Í«-b¦"l°°Áb’býòˆޖ$~‚]\fém–ONŠÙŸEO>ØÞ÷¨Ùbe‚Á*­xvWìŽÖs¯0­ˆ+EÏ?À`qÄ .Z¨Wý¦bvD—QñŠ“¢Š—Šä acE°Ä–’ÕhF •’[ôh°«q€Ì¸˜\…·¨QBÆÄJ‹‡ÉJ‡I«·*ª BàaDˆ][@zŒ•ˆÆjDSú¨bWĦùøQ£E|pƒ $@±¾oq2r#&eÌɘ“*T`”¼Pt¼C…Œ˜Ø~˨L#ÿnâç(¾.Òã'|ÖUsä³X•P±ZzÓVN³ðÙE´çHéPÃâ²*­ú»L±bµ2­i+^gR2(4VFb}Žúèg™—a S(F¿>Ééa¶šWXo‹äV„¯´ÄH1Kòã;XF^D? —EôY´»ôÚÓ¦30WI‰4£*™ÜrëM]ñ
+£_œ±¨ªqL2 $Æ¥åÃÿ SB€+hŒ°bH?ÌÈ1‚¢i•Hrë£=xÄvGŠëL ‹U Æ+ÕBE*áñ‘«6` ’ð±ÃÈMá¢Å‰5Š˜1c„ ìpƒ*Tw}£ÅD0h™—)‹ˆËé/ñà+ ž§á.sý9”B‰@Hø›¤Çˆ‘~´ÄT¤>0b&0”¿žBàXÑ„>T¼V°¾†1yZ¨H4^©.U*·];Ýq•š ‘#-™Ô(TÃï)®úü†É‰HUW”ü$( .#0bª•ÿÂmÛ,qsô¬inŽ§˜BEÊ © ´¢5RÍð¿gÀ
+¬•WŽU‰¤ŽCª)bO¼öP‘b@G ‹˜”ÉH‰v
+ThæÙëžö±2‚Ceä„"Ñð
+ÄŒ‘ÔËÆì×GLÉ4hxpƒ™|<|jŠÚÔå÷”\¶Å·Uüö•+V\´J<LV<°ÄH°Šã%%O01£D„ÄÇIz»eä9…b=ŠÛMs¬ˆ¨}HèXJI”«¶ì˜uÏ#˜má󉇟9žàÁ‚‰+^N!þÙë•_òë&Ú½1“ƒ‹=T`ÐaFpÔ
+2+%Ÿ^GI%¬„@/PVJB½Ëé—Y^\¦˜¨_!yPúý“š8HP9JV/`&'Š¯·nšgUEcУG…äYÑňô'?e§?æØRµyì†Ç0£士ü·É.ŸÚÁºgb­V*Ô«@b´B0T!\H„˜9™ íï–­œ¦’]>/Ñë"^¸äSòƒK*+DZ*ý¦Ñ&Ej‡Œ­•-c4üRú¸€™|¸Ä„xñÒeeË1(  ‹‘J¨¯i•l¬ˆ¬œD1?§q•Lu ³"¹(iü8 N[H= Ø æbÕ°
+•èÅ"Ùí)±OrÙUnÇ D-b*–HeË*¿»å>¿ˆ©VFž“þrú”üxË)D‚YR«ö¦ÉšåN\1D¼ŒçŽù*½ýòÑUüû“Ž6RD`Ä@ÔÀð0£R£
+½xl¬T¨F!˜ÕOR,<TTFîùÅJ$–ªaòòƒR㶢EJÑpëžyhSDŽ0L`,^(™H…J„òÑ[øûù׿Û(aÝYÑ”þOŽÒß"ø¼ªãSýªŒ<&ûõ¸¨ Õ臗”âI<¹ifK|<¶×3.ÑŠ” HŸQ¢Š!‚Š1ù*N¾Šç~Õo¯š©}&9}^=þ%ÅD ™-"+¿=VúŒŒýÁÃÅš(¯ \°üˆ1ÁòºQ‚ñ:•€<!7e3¡@
+èÁ’r¦Èò¸îZÅž¯Ö$©"JnG+jrÕØNß%8âÉe˜¼\J!‘œVñï‘œVÙ2èEWDýÊŽCðjòï-\)-U‰-›èxH=?©ù£å–*9^Ñ4 †q÷¹èxų›ä¶èÙNÈ—"E—b“fY?†öç¹\_l~kN"‘ÜÎKò嶘ãö0Déóÿ4?§yrYÏ^bÓ«–¡'NŠ1Ø©(PvûsŽ%8Üòç$¸âS°
+ËéR«šXTÅ“÷'™“᫆Svú„¯[5üYÑuÓPp+#ú?')s~ô>èBË{NÉí‘»¶?Æ4ZqÑ>(ûý£çn–,Z&ás—¾Ód©£'«†WöZdv[r[rÓøãtR4ñy)(ù8†TÓ%ÄÃ`•V|Ûä†OíéaOÏ«¾Œþ•¿¡åÉe?ªº—%z4¸Ùfi—d vCìùAIRZ’ÈîŽU‰‡ˆŠ¦ôéOqå:¯¼fŒ--±œ6áç’FÁì‹ŽEt;UÇ 6mÁ"ù8qýHÉ`‘b>ùëŽCnʧã3jPD¥Žó@ÕI áâåˆ3C°`ÁA‚jÙîKŸ‹°ã…4Š!\°N2^ª«RÊÏ·l/É^=Gp:õ® ô,¹kÉ-¯õ3L ÝÖIq7G”_)…R<¼H^›ÚÓ¯0îâÉ_¶¬£%Šnmˆ vXyù0yÁæ—'AYänTDý,&‘©ŽGv{åß]D¾Jȵ¨èMw•daòb¢ÇŽ•QÈUÃ#6¬n8©wUékÿfùuï¯í™qªæùꊓ$ošú(‚R´Ç× WS¤@3Í.Ï—¾›Ü3Œ~u|;Îsš*å÷M/ënO’£U=¹-ˆCä(BOÞ$U6Šá˜ãÚGE¨§!ù.ž›…ÉsjϾó°3µ©GEAíêºi“_Å,kfUsk“b,‚)ZÉó5ÓV-#µê*vGê©r×’zþfYÞI¯·h9»$V]ɱ*~_´üIÏ›$UŽ“Éä¶nÖÑCNõª+úµ*‚{9žäY7ÍÒÛ¦f¸?ÉjÂf76»/X¥+’H^{P“eË-ûžÃqÅ•Û¦àµÊ¶mÓtÃï=½?èéáÊMÃ`vå¦"7ãïŸqbâÚ}5SrKrUÏYªä÷öÓB®„Œ9RHR~¯¨7}Ýñ‰'GÍoJ·ó³„Ýt.'¨z>ùw™Òÿb¢ñ*•ìtj~_zžªé¬ÔÉï ±*L·YHŸ?WÅnŸš½Iæcˆ¢é~‹à´Ë¿£ä5iEÿô±*«žýU•EP7KÔË®ètŒÒÓߺ\gE,å
+ZS¾$ùÓÜÍ1´¢©ÛÑ5ÌßI<|+¿[vÍ¢eЪúišªm’L‡â9
+©r=æñ]»ZÕüjЛ?GÓ’Lë$ÙŸ¦oš'×mÍô'ec/9Ô uõåOU'G«ŽXÕĦ¸Ø½x›ÆJŠi®Io»še•^gù÷˜-Ç_7fì`LP>µ^«kÿº2¨‹
+¶ë·¥ÕÔòÐH)AéyÐoCDU²ç,ç©[8’=»ü¿oŸº½©òæYãô‡nóµlßÓ á¿Çöï“=3NÃ!…&
+Mêú]¼SÖ]päUÖ¤dªÏT.Óù\¸.A‰Ô²ÈT=¦û¡ý—|§Ü¹ß(ÿ@¯ÄÊózÚ$³èšs5ÛÐMÇÕ¸ø áÓ•[ݦIuî‚ý”‹”Fˆ^r€G1¹hÅ7µÌ;̽W²'Öê+Ø6¹L%;|ËfÚ»ïߺ$¾O²q\ÿÿšU \~X@
+æ‡'¶Bö¼è~uïªÖõSÔöàˆT’gÓLZÖ7ͽ,}Æï?ÖIù)å_©±Š1DjE(7ˆØ!éa€œÈJZ±áCŠ
+<h¤eõÆM~Ñ
+Ì4šGrä¾$ÏwÞ[.­D¸(Á
+'DÀ²±„©†RÌýÅ‚&±À~,ÎQ«Ûä;Šjl¦¹YæsþI±°ftБ
+€ ~HÁN†Ð€ Æ1N˜ìÐ
+HÜ@ 8Â& „-Ì<10ýP€üDøϘy Ë‹¬¯ïË» è´Ê}ŦW2£N žXAŒ á
+a =8 ƒˆA‹ `¥ã
+5U“þ'ålbç‘]‡âR}UÙÅOô°
+Àà‡aGÈ%à ¬àE^ÐÃ
+Œð£A1‚®ÊjkFJ±=¯oÂÑÔ$…JYaÑì;ð€´`€0Á‰!PØB'àŠ @FbðòDR9 À—„/ƒ W…O7û“ùôþ[²'8jÙö¯¯2cG`ð€&pá‡(¡D Â €† <! /( p0Ât  8A RpÃ9Hi"{ 6ŒÅÇäÅ
+…ÅZ¾îˆ¢`ù1‰˜d
+@VP¨~ð0Á =P`°
+B8€ËѦø8Q4*?rº=ËÖ ý¯_›îM&@(`‚  GÌ` ;@ ü@ 6Ø )1à˜
+,!ƒ Ȉ@
+\?žØÀš°„4¬!Nø/B€ „H ÈA|l 'O¬z€%ˆ€X¯XœŸ“Å®]Ê{Ñ͇@©üG™`lœr$ÁJB>ÜÀ (Âjt @H°ƒ'vÀ„ ¸‹L`’„|d@LÄà92ð‚PŒ#V”L‹À“VͶ>Ý’ G%_ϯ¢‡'ØB¸Âè@LÀB  'np„&<0³D ƒ4 ´r8
+È@9L@1Œ8@Ž'"
+@T R
+B°
+ 0Z™a“HD1—>P25R]5û©\¡H­‚[nmÓló÷y<Ÿ8þÔ5K’-šŽL¦.üÞ79AÙñƒ‡ÓŠÄŽëS4Ï1MÕCRP±¾/¡(¼IÁ!;YÉuIrî<qÓ³ Ã=OH` XÀdV¸p¥¤˜X#4í¡I‚# ×þŠoжiáY%üÅ»µ_¢Ÿ¹nLßïOB©€A8Ð!
+jž–‚3§ªçÖ?ѯéY$$.fÌä`¬ßur›Rÿ+¶z¡ñŒ¼Ä¥ØyÄuþ>SÓ¹lQîë6ÔA0ýQß–ûZýм[²ðÉž@$T<Oot=‘Ý>rÃS¤(S„´Ygr²3ÛFŽÖ¥Ød(B>N%
+ïÿצ×8vkTuÝwžãû Ï•Tíy¿k·?¯:·#kna–n‚!œl
+å9ŠÈ‹eRѬ\j_ †öEÕyßÆÿî׸~ï~NçcGS5MÍ:-·ÙìŠçzÝÿ!+ÅÌ
+;`høèO®úS2Ùù Mî×ØîA'úö˜dJÏúbø¦ÙøË´PÛÆm{!!1" &Rf^ø#ÔO:õ(Ÿéœ}i²L½Nà $SS:‰Àtÿº6ÿ€è˱£›Ík"Y¹<úoš«Yšv “> ˜Š%âý=gŽ™˜ô";ã¬;Ç®|v ü‰xX¦[7r´öe°D9[܉;úžî¾gß×O­ 9¿º£ùöB:á¦3âçT=( cÊycg+·¯6U;¯ÞHfÝÒáRlL­£ÓÖ6Õy<Õ<á:Ê™xÎÊqhŽàùÓon8„l(BÂM&ƒ¤M¦gÆ‘-ìdãÆ}îÚÄÅÒo>tzý£~ÿ%H~íÓº,×ÐäɳþD÷ûwö´±ë‰aHæ¥à”Y'r69eüðQÈOLÅ’N_;mk´Í_zMÛu4ED§Ø‡¤r'eWô j_]]çò|?±Ì¸²“„@5"°ÿ…ãPµíL+û}EQï;?±vëO*Fñ‰¦ÞW“çg}ët;OìtÔÏCéüˆÆstµËSIš4÷ôœa™Lyî’&®º!ÏÑ•?O½,óóüaX4϶yr!›RËhQÌE’¤ÆMóÍ’&:Š,Æ*
+òü/lƒß¸’&ÿÂ.ÏçV]V<WòŒMTDçt+·çªÇrLšûÊzZ%Ó.)ÊÚé[emÝ|?ðÃRÛæ³ô1Ã…;|ìò]òü!\ÿcûÍoÃé
+äºízÛTnÛˆ ݼl¢.~?ñ{’<gÉño–4è}œÎÉuõh¦Ù6†„ÔãY›)Žu(úîKÚû»}SX, ¥{ô2u9Ǻ<]±dAqýÂ;US·Nšïy,áp\·WQ<«ø>"Ígú™k’RÊåUò‰éxC÷|úü:,vßngmE­5ŽÜ8•Ëb ÀJD¦;]ût5­í Š¥ç±#‰§ªjßKJ%–‹‡åÙ,íTMÃr Ù˜ÚæŠ#ë}­7b¾ÿÊqSƒ!!'×ÕêÊËt?׫÷­]—nÉ鈛M‡"E›jiÆS»¯êþ\q±<³­Ì¶mS ;™qººþ­Û«ëŸª÷…ÅÒtç1TNj}?‘S}iÑ„?/QTãÀŽêÖ•Ìõ¾ü<?®ëYY<eMñ¿‘E2émœ‚$;ž·Úþ¯ìyáMóí«~ß+ôIQ¥b²ÔOÔ³¶z‰Ú$ªŸ)J¨¤ÂûrëìtåS–Ã5]µÌÌ4tëæ}òìlùyŽü>¨mÿôÜIÒ“¦?zæ¢h’ã¨?Ï$CjªjÝÒºÂÞ7n8_HÈg®}GÁ±Ô:;Q'c×%’JÔ§ê†1çÕ{ôEÉ2ŠÏ¯„<.¥ õÂRá+ë¿nì÷ˆ`Ù&É9 íqÔÍÒË÷ epL7oý@<mõ´ÕÍô&M=MçrÔÕSO(‘ÃŒ%»ÇËÔôÊ1»¹n¿¶u‰Ò噣*=ž«² —¨¿®*F $"/$Q†M»N8XÈÙü}ž½ŠžîX„ôÑÝ׆ç5mmùvÍ&Óú\ÕîÓÕu_Õ‘,Ø—ǽ}Eøªs‹|<±Ï_²éÞuo4µÑ”FWMùÕÍÏ•ÉhX„pӃńå÷Qokr8+ÏoâzÍ/|»®¯ª¢vUÁlÈ4ÙL+9ßu_P¬–/Â_:Vß´KN6n8¼Êê(Kv´õ ó´5áú(ž?¬Û£©èuC®ëÒo&®»4qõ]Ù—ošD_ìSuGÓœ,?,+ƒáû}:Ššè:…ã=»âêÊ’¦kž#¥H®[püÌöi÷˜l\4˨üî]w6Í”h%e‚EQĪ+Z¹pÏÆ/8¾_ÈzàÉ}¸ÚºØrNŽ9šöí««l®²9û¶`¨£)è}kr¤Eño]ßsâéKn›¢å¯Ëß·—åˆey’¼A°C[$ßÎkí}èæÏ/|ÃF×)ýC„žùiÎaH‹cŒËU3R¢£(‹„ã„ÅßeJ!1–ÈS«(é®E@Ÿ\]?1,ŠcPëî'Únœšmi¶ÁÞ§‚Bõ/Ý~âë÷Höjگ鎒?TÈȼHL3ž¢sÑê‚ÐS%Ç&¾_zåÚúé‡äŽ¡ÙÖ×—Ç›ª}ZHŸÖ~«˜B@بáBå¥cÕü¢ìwËg¯A²
+ɱJž%iÖ±Ž'¹y¨Âå‚áRüºìz/Í›c^®34ùÁ.ˆkÆŠÉ ’–Iè½n~¢»ºÆB"<»ŸH‡!-Rl¸N/zž½ Ù¸Ý<1‚lÀ”÷*P$«Kfò›¹íÜ„Øä8
+ìÇ
+¬…*Ô‚ú°«‹•””Öë¯uÇr\Fbñø¤U½EqÕ´»¦ì:/M^UU¼ŽÊí‘Ÿg)…\J#ʶ¤P=à¸!bbõ,뢤ÅC :`XJ|žFEŠ)…ð$Ñi(ÓñhÒ¥¹«j†SµM‚e—ãh$Ù8H÷€tdÛ¥ü.)‰b˜¼|À¨Ì¨L¦ùU!›1}F(üÁð'~{våÕÕË¢¶µÅ±_]Od›EýDsuÅͳäÂ9Iª›ƒ`ß¾;ãèúycŽƒäÙgW“ÐO2üþ2AÜ€q&…åç$:ö]Á>~MH¤ØÒQöËP §jü¥ë1)Mš®¦hœ?Ó›$u“œ?n7ÇÚ$íóD ‰<(Š’ß•éË·HÊšò§ÙÒuÚDáðs‘
+õ0cRD &|ÈÈÑ"Ò‚­t}&ÅQ«–Z¶7ÉÒ«†Ú´Áo7F?&ÏQ{ÜxE¸ãjðÃIq?ÉÑÿ‚B媪Bòƒ ±ÂÆŠÉŠUéä×C/‹£¦}ž«¾ßWGÑ ÷ã7ROК¦h9¶ß+{†¡r’CE ¢3ÿ!G3E
+o
+H Bœh›‡23)
+†C(wÉé”œÍ K¥
+ÐCFŽ˜K·m“´Ç‘ÅÈ«`€Š˜ÈÌéƒ&t¨
+À
+n]@{Én£”F$ÛFÑó‹nžïÖ¡Hƒâë}ºi¢|zŽë€IA’ІeäßGp»«¨·¦zÍ°ªçårTÝ1 “VÌÇW¶ ZÑÖN¿0qåP)ñÁ…Š+,°]ÏAEËŽ܆Òó=wÛfÐsq2âDôkºc%ö
+§Sö<ªeþuc³ÄWÔ[:=U9=ÂÛ%~ƒeˆšÔìæ&™ªg/– ~ãŽÛGñ¿]ó0üWµ„çó(–ð÷Ê¿£Ú•¢+F dTx¨¼p9öñSžžtÏÐAGŽ’X^XTă«<$¼ž²í”ße]H`R`@{ˆM?îjbNŠ-%P—Ó\·hø‰,(!VÄÄp¡^B|Ì ƒ}@
+`H@
+¥ô¸ È÷¬çøaJ qbB±<ø 駥~Ä 1ƒ…*X^J¡“PŸÄ@b\/‘ßÓòð4"®#rÌX@=–¨Á 6d¤Œä`1³@àhÑ‚ÇÌ Œ‘×Ë ¤ÊkOËú,ë~`™ikçÉ øYYz$Ýï[7.Õ´Õm¿nœ7M{ I<yŒª&EJá5ªÏs:öܳ Ëû?(ÛNù3FV5VHhb,–Ô©—éšÔæ$ò[–oÙÑ<·{žÛme‡†„°\á‡"äÓ¶"9ÎSVGU—ë˜bc†pp±t¹Žå6P»Öä(rߘ¾ŸZ7¤ª§yö¨jÉÿQݺ ~¥w}ý8ZLth!󂤵Ÿ$=–¯÷Íx‡¡ìs(öèiâÙK0ì§*-’kçÑ#ù¯êžš¾ËÂx½29\RH@¿)¿ív}O“g×™×nãÏRdÏW¹‚_Ðë‚\7ÍþDEl{Þšeu‚V57MP,Ï ¨jn¢¤ÖM¹îk~]þ]fuz‰‰”F­>×Ó’Êq»$Aªê²kþÁkOŠ¦áw£'‹ž{R|Ù1J†GúýUÓ¤Uõœ¥ž/èédX‚×­9^Õ±Ké$¥ pp`˜€B¸ Í°Lp‚'zð‘ʘ3*2.’Ž“X°ìˆ) " ADM Fc©ø7©Eññ³Á½_-c øÀ)ˆQR„ —è@Ã?hä»aŠ1ýþ)¦|r $€‰'"?À˜qÀ:!£fFLÅB5ÂQÂr!ù% 8Â8ÐÁŒ™0)Hàx!|øÀ"ÆäãÿnÛ~ û}¢XöÛ·.Í•ëæÒôÛ—Ç'9µ«KÇi¬¤Ä|O nIx»¥ŠQ•`´˜äPcÅ8Z°½ú½5‚"b£ÅĆÊM¬e¯lÊ>Ãbw7IÓm¿paá]ùä‚}JtüOQ¸ëÆNªq-èÀV˜¨´_H†„ôæù¯­é…_²üAÕ4Ë>l[£„D&É·ë@(ªò¤ŒB4¬“• bZF,{šç,ž(
+
+Ì#ÂÍþÀ¹4Añ¼¢iåÐ3!ýKè°sZ‘ð9/fNЀÑBÅxÄøÀF(à‡)$ÆÊà„?|ðPѲÃÅäF
+‰*Ç]³›ã$c¤%Òã£æQÕäÂyÚŸ÷9͘ëÔl“»eTªI3L‡aªeõ²„¹n Ùp(PŒ-†"·mr]× yI‘€@
+µ˜>«{NÕñϪ/ʣȫªG]KN'ÜlÖm#­¨‹_Á:½Y±|z˜^¡qi@=áÂÅ‹Èã‚úáR£òâåð"†G
+×,ï%ù†a~ž©YFÍ1ˆM_7 cDÕƒ š
+H”òã5FNC¸`1b†Œ2d€Ì ´ˆ¥Zü=#…¤%EBát=†-ý¤·Eô™„·S4ÌÊo™
+†×'û-:ËÚóÈN‡CŠpÃ!9ºWw†„ìcù²õÞµ°>’±û^Ùùžªp@†€ȸZ,b+!pÌâ %jÐèC⃊ôpñƒ ?\Ô” ȱâÅÌ
+µ„5FV­:žç1T»ígÙ“<Ó!x‹á'E]²ÌŠá Ï^GsBrƒ¬ ‰!ŒÙwL·s¨‚†‹*$/b*WNÛaÈŸ¦JÊijlü}÷(šŽ~rWŠÂÛí+%üYêg)bÕ’Ë~Ú4U»2,R¯¦¨XíQÒÊQ"ò‚öÊé{æf(ƒ¿®hâˆ'Š
+>€‘(…ôç€Iá¡¢% 2 У†1fV¸Pxz¶`9Ñ#F j¥Úñ–ˆ…מôÌÇõº2ëè—Å4ú´*è,ý’ô &˦Exݳ®`Èfäï
+ «4ǹ)ž€x$|Ì0À:ðƒ"n´Á²R"
+Åx±\<û’6è±YGö:9çQìWWKPÛ†XöOSž$=%¹‹".†3PH€7dbF>ü6X¼`™Jr›¤–#:=Âãû(ªd¸JC…ÅdßChZn6ñÊ_Hƒ`«i«ÖqRÒ²ïš$õÓTÍñ‰G_õ+¦Ì×k1|¢‡p@RF§?=gˆ¨fÄR+R •ŸïÕÔAQãØ%ÙjÙ›uxQ™¨EQzäºf‡S›äŒÊ4ƒ*±ìù/EßClê£f*f_ÀP3+“ª~!PDKüÈQ#ÒJí5ìçKD”NçjÙ£†‡ /,Ö
+ ôÊk’«‚ÔÓ?ɶÛÈNHIÈbF ¼$U|ÝÆHLåóÿgi“âÚq(n¨Œ|àPi‰´nÚEÚ¡†ÆŒ‹ èO½®¢¹Yö0ã…?r(Õ5ë†eŒ”p¸ÄZN O.súìè(ºßẗ¡Z1iÆ¡`ý…ºX¶¸¡Ha£$Œ–•Uï!Åñ vG¬jzÙ¼¾ôˆ ‹©¦}7íÁ‹;f±c†,-'ЩžyVýÝVWÓØÿYÑ4‰Ž“Vµö>úÍše»ëâ$ùâñœZÕ8õsÍMÕíÂþ\oe;ÐÅû*T)’Þÿ¬ª'EGëª$=’¸‰†Zw/Mž}gRVØQC 5Ž°Ã-zhc"ò ðUÛ<ªê(úiÝ{$Ý­ÃÉÒWU÷_0ô[÷ýÀz$ýT}ÑõÊïϤJ*Ϩ]=k
+yXŒÄø³üUÓģLj¡tˆ1)Ùí—ÓØÏCá÷K××NwÞn¦óGî"É#‚ôçÉ—¢k×®Xõ.ÅÔ=ÓpQ‘bRŠ_–j†Ê
+©®_û­ED…)³å5ïQd9®Ì:lǵœÆŠaËÞßwŸŠ8Þ ¸²i#°~÷*z’ß(vÍÒë)!OËžWtܧ¥ÉŸÓ
+W_‹ `é†÷Âèwåß_H,%4Jb*ZŽ±r‚ <€F ‹ÊTÃ"¹”|¾6ѯ§]ß„@J˜|@ •ÝÉ“öD½DãÎ#7 ©¶™Þ8'M¾mK¶~—iMž®ªYgv´ÕûD~ϔԷ]gb¬žIêú5×#÷u;ïí¼”RJ2pA,X„Ps³2£¾ 6ŠðaˉǕ]PX½gí¼Uëj‘ìSµG×z$S®S=/ÆõbIµþ•-ÁíkÇAk¯·|ž$Ë'z¶áÒ2C%EÔª¬úmÕ/hEc1¼QTÉq³‘arBÂ}ÒVGûwQ¼A°Ãß‹êهٖW+†ÕºÑ²ÂÊÝþ %j†A±KªaÏŽšc?=órD¹¬È4u,9–ãNoœeÜyžó„Ñ´Š®›Ôvo–69ª^g—#É£^·ôºþª–à¦ã#Þeä'±C$±)/ (Áƒ<lQóÂãö¢eB õ*݆¡‚„ §™vùô2¡ÖÓÔlc›ým¼9‚Vu=¾KnúDÇQìJBMOjƨ(Úk!rÜ0ùˆLüÝÇ‹Š*\˜˜QƒcäE3
+±0‰Pr+ê Ä8‰—‹FËI ˜ hWÍñ¯¦¥zÕ´
+ÏÛÎK5­õ¾ôAmû8?=w¨YñqãºK1Ûâë+ÿÞ2úaR£Wn£f˜5‚`ƒ¸XQ!£öce¤jõ";}šá»M6¾z`I(ãzõ,lšï{$QMóB6(4)8)@p쳯ÙÑÒS=PGW_e뱜Ãñ6Ï2ÓÎL½nìž}³”»î<k,¹n-fJrŒúÈyÍœ@?j`|¼PÁ²Jõj ƒ!(Žgó;™MšõHÊ¡¨jŠî÷¹hªi¿ÓÁnkßõS¹.ÈeIoËŸæH¿û
+Éã‰[’ˆG‹.‡´ª,Ø=ÙuUm¿_ˆj[²ÉË”ª½iêãX»iðçÕäHr[Oºªªë“)Ö÷4.Ö•“Ÿ¯çŠ·ƒQSÄ +:^ í*\$%xàá
+<@ˆ9PJ#û<ÿ·U‘*™^¸Á:S®Š‚ǯ˜]5N’"ܤ©iJž{H÷ºjÆt»äß]ÀP2Db#9võDÌœ
+:KÑIú$È—a’ =ÅnO‚|؉FƳ·øû(==¥XƒÝ=Š±º6±«Üep—ñ¦›aÏîêInº‹Þ ׉
+JM{ßÐÃQQD§i˜D5žý´ž´ÇÉÝö1Ç=ô`NÃE°%³1"~•‘©©GUqr±i,nWæ×f?døŸå^–ùêã7sš?‚1!ÈJÌÅi´zWÓ«î§ZÍÿ&¹iæw6w·V•ä² 5½CP¿W*ŠRMŸóËÉ.þ²%Qz¿ö<¾$G쪂ßËÊwr6vúéÙºë»ú ·{ÛÉbM¥h_’ðÆÍž‡ƒb†ŸR4©]“ÎÁ¤¦,øý˜$ÚÙ´š&JӣܴÊ]ýRôÉĦ§ØíMò A7!ªv…Qú¸(ñ*PŸJÊ[6w[‰]]nCRÜß‹aך²ÔÔ3Š~)~Ì2Õ¶¢T}7í7MQŠšÜµ‰ü²X”å®8)ºðû³ªë–Å\¶‡ÊEa1{RO^å΋½ŽÄ×]¬J/ÙÍ?Ž=Ϟس…jÄ’ã½<=lûÒñ(±.'%þ­—!N‚ŸõTÉnüyŸÓ4¹g?ÿ1Òb½«'-C2<ºé’ ß%Y‹à-‚+¸UÙíXý†Ò2Á\üRïÚªãúëâ-ÃAO”š=ÆvzÆÉë¿>{žô=O–rÇÕ]'s¼i²×`Øõ8øë<몟¦I¿ì÷-Aì9²a—iÅêTƒ~¨—]ùùÑŠæ"({œìu?zš\v%»'÷“Û”«’ÜU7ÉÞ$í¯ãE烙+wLãDBÁï} Y­c^iEc¤<C¬¼jD½©=uô #i5?i¹nûãö¯ ‰b =]ìú£ç}–wIŽÔu?]ôd.ÛÅ.增2¼Coíth0¬y±Dú}7MŠ–ÖUDÇ]¨D3¢FÃ!5ÍÇ2M]Gg9BGÛë^-ËC'IWÛ\-{Åðˆ<—п±øÍAÐÃÔëžä¸ÜqfHˆý}(we¹e˜Ãé9¹ `5Ó2%’Ë®c°ÃGÐD¿eJ!Γ"«eô’Ö÷ŶÃ;? Kì£a’»îÍ3î:‘ºŽÕt?Ž¸×éŸ2I“{6ÙóŸ·KÒÌ2\{R,­©ŠMñýÿ<VÜÒ¼P"˜íÏñ5Ç'¸jU:žØ’D¯Kø¼e×yI²vžh-IrÚåïIæ÷¤žmÉriU÷Й¢ŒˆßA…•™T¨¤ÇC°û·+ˆ][ò;"»(Õ ¡dÌžUîºrÕÿ$oðKµ¬ÈŸÜ3-s2½ìžžuêd™jše°Y^Ñöœ$ìutþë‚e¶ãÔ'Þ²<VF TH5»ôøý¬Jz]ì‚Pó%¿¬Øõ˜åNŠ¹zÐõ¶.ž¼Å¿S|œä®9)ÂçnÏžÐÒ»&õ¡ýy¹Þ_׋^Š=SojÂÓ2¢~%§C)I‡ªqzR¼=.÷8{Ë`q+C
+©„B%¾ï›¦ËióÇõ¥hrË"pëA¹ÓR.[Âã1b*-ûç©z߇‡–’±“ÈÌ~Pò³¦?ÛÞain6o–å^GRÑP~Êпr2{IMÇ`øæ8OºÚ&Ã%6=2Ǧ5]µÌìtlð¡fÉñëMÇ1
+~ñ;,†ª×¥bxª3ú]ô» .†¡ ­®‡"…¹ ¹?/Å®,ù-µêŸž99ΞNjãìyi‡#oÛieS- “á‘Z†ÝsÊ–ù4¿oÕ²VËö1¬At–«ô4±aOþÃlèeO-ë1Ë}qÐKÙe#hTf°J¯ÖÉqí¸»=©Ij×S»ŽJ‘Å¢,L¡¿ÿQ=XÜî YÝ Y­@^ö+ƒ½xÝ4ÇÿªÞãBK‘ù…‘ÑX•Zoû)C~üD¬ê’cÑYŽRÓä²/[Vñì+ع î89 m²lÍ2 vcó ›]T«ªä¸êiQL5mÕ´ôhB|.#/Ÿ<§(-ñqÑÛΤè«é‹že@ž›îE°E¿)>ïªëš†R“¤š¦6 ­h~2عœw\ÜmüYšZöd–AfiÝ«uâ-ûœ¥
+nSø{e·Mæ—¤–žSìËPA7ë¸Çö6©+";F\1¡ç<Û-{¹ì;Z¢R³t’ŸRìc¹8…bôÛ—ä'MGîšÃ\=dhZË–T¨ Èc{Þ˜"Ì}ÿ©®Ý†jvÓl1¹góñ€ÐSålZ±Ëâë#´ìïî¶ôD*jrÙÏyâ èfß,ÔÌà Iáj™Ûq¯×Ñ£X‡ßºejÆÉGbÁï¼ik§3†„ðféŸ&›i'§ãvœn’©Ö¿/Ö6´ÓA³¼mq×±Zö9Ç.JËä¢çX ãQ´Éqä¶÷(Æœ"Å—›Ò @.H\±=÷ϳå8Ò‡Öµž0˜ýIÐAÖËö帗$Ûqe‡£‹ I¯
+Ñ?ŠÝü$Sö\Š[ÏI–شĦ|I®\`ªmbHÈ-†!5 ¡¥ˆÜ‚Ìqþ<øór“ôœ¦»mf‡ã9I«’iÄŽ}s¤C¯WOEíäM„’ªW}ÁÅpFò%¿kÇÕ#¸‚[nÉléM?,Š›dŸªzyšœ»q¡4•Q½øz fK+ª›ã~š~ªžšvv6yºbXD†K§É‹`NŠµÖc8‹á]–ø8zвÅjä"5:­çŸš»9’^öGÑžI©ÉzÙ«ªÔttšvèÉ`ZS<ôJ­Z“BÁð¼?Oß,q2±éhUý’üQ3?žÿ“äÉðOK;ìô ­h‰MRĽæ²ÚãD,º’]׿jzžW°+BOQŠö%y‹^5d( ˆÐCŽ).—ö`Ýq%z¾#ÅDÁÏY’TTä¶Y˜H&:nz] ÍÍ^–ø8–(PØ—²ã1\¦',#V¬Ô”~Ñz®Y'TžµŠ›Í²y»îC¿,QjjR×s×ýçéòó¦ÕGA½ånƒ·lÍ8h§¦H‘‹a(=E¨É“# ~ﶵšÆšeü¶'‡`HUG(JJOÕ»Žàs_Ž+z¾aB‚šã• —0'3&Ð
+fIh™rÙ-I2œBé*z‹ J¯Ó”B)ø…½Õ¶tã\Ï+Á0 ŠcŠÚËZ´bÑÓ»²}ZrÜz†+ŽÅ¯ªUU,ŠbS3ò¬hØ?-Eòº„¿It:媡´¹ªM†"·½I1ö8-Wü[ÄÃjÙƒ¢°÷½bá_eám«Ã¯ä®/V¦X-Ï!¸~ß—en‚Z·ôµ(îä˜Òï/¢¿5¿ýIzÜÖwa=e{EÅ-ÊM]ø{Ç ,¶×$Ù½M³ÇEû4ÕÇq¿OIºè÷eô‡ØT'C¾O0;jYjÆhå²8)Þ 'oîq(´œBǪÍ?®äª(رë)vOoÛ‹ v$sÜr]Iþ¨¹"†~Iº`¦×Q+ú“â_Žø‚À‰#|À°ŒðøwP«;mCXMψÁàMCÁ. U‰¦4*­jÌqm–É·r:´Òˆ¦HQ3aàKĘ™ ñ2"?¯·ø‘›ü ×L3;PÛÂd˜t–'…µë ~ßmËÅÑäºiÖq» Ô¦(üÍbÓRZîã—fÙÙÉ>iš¤še0—éåè1I«¾ìE¸ëX¹ ZW7ëÄ[vƒÝz;ÆjÙʼnZQ‘ÝöAE
+’ÊßË_wÛ‡MïrTÍñÊïߤèAK"-1VHE×,c9 ÅÓÛ¢ìz †8Ô ÷ºåjzJqD~Y/›‡  ~ÅApAV«’Ð0ËŸÓŒ<±:ÖI?G^—Þ¶/ÇÔÛž^vD·Ek*rÛZq ­§§Cç8RÍìþªj‚cžضݸ›$5UÍñšùiòm«·î®®!7îÓµ.Ë_]WôZÑÑjÊŒü-UÊHôi]»ò¨ZQ£¿ÅÊ4ÂÛ¦ÙÝQÔ&Éú<ÓÏc».E¿]óûêÿÔäM½¾?kêQÑTüŠÖ´?I\ﯣ9-DŽÐûEPdŽ/ž½…i£ç‘»öè¹")=Up{嶴dѱ©eo1¤¿.;{ªXTÅ¢/˜íKQå¶r–µ›M†¥¶eÕô*Žý’Œ·MDB/I-ÇÝ6â0€<Oî+2ÏÿXÖWw¼m+L!_çÉÐþ¼>U=ijz_ŠöâÈv:3DZìvˆ[Nˆ«e1×Å{Nšz™Š˦wœÄTˆz>ìno»Á.ö¼œ,a¼FB+¬v:3Ý%§Qª¹ƒ] ~î¦Á›vòéýuÅðdÏ]õ JË”›²`µ´š>ZÖpYyb‡Tù~“cÉ]]@ý XŠå“Ãd¸å²n—Ñ ×—£ ~-§©\–¥ÏWò†¿Ih˜î8´³ µêÈïÇà·fšÛ>%Ùƒÿy{IÊßBQÓšæ#˜n|AêYrÓ–wñì´”A¯äó ¹²'MKkz²ç$µ Óã/L ôhе¬ˆ®ØÕÔ¶¤–ýU´?ËкޢørÜn–"»Žò{Nóü§ª^–ýyªäØô¾2¶›VrS—OÂã+:ö ¦©UMv›åßK.+bÕ—=Ÿø»
+~Kn»£§ ~5Iúæw«uîÑ3Á­éeIlŠz×_=óÓ´ÏrCz~P3Á5㘜ÙãDkjjYT«ªøy%‰‡ßUµý>÷óääÑsUË(>å¾óç±Z‡Ý6:éyLQ¿ž Q¬:·s–ùe¹ùA·(âßç)Ë’ËŽÔôÝ6vÛ·7Í]¯ÜwÁ7ÓÔ δ[ ÷q,S Qà0Z×”÷èJ{à›uÚ,c9ÏYÖ´Zèf“ƒŸ
+vMî[RQRZš†v´±Ó‰=îºTËZN­«È—ܺIUyó\5M'Ë“ûª› ’¢Õ6\,UmëzYÝžÇ0þ¼Šââ7{š_‚(7¡fªMw y9BGU[@þŠŦñ–Å`BÍÒŠú'ù9MÚóX-kÍqŠž—R´æ¶Xëô¤HRQ’Š¢^–WS÷óôÇ1Õ´ôDêéIÏþ,U|^5Ëë¦Ý刚á·hù«±˜ýM²»0RRt”­ÁPe×Wx=Å¢%S„Ákè$=&)‚ç¡öÉE¿Uxœõ²/—uÉq’š®Áï$ÓuHžœN-~¬¸%¡eZŽáq¿ßŸ'NŽ VíÏĦ+˜éïŸÝEˆ¯±*}RóÁ?=?+Jrט=³`ö¤¢¤u­C0ö::ü>­ú³éÊŽ_N šUiÆŠá¶GMuRôOò_Sý<g1¤I‘GÍÝ,íqÜM²§Yþ¶ç vEͱê®Eñë«©GUy?Oö<M÷²ôQó&G8ó’­(b¦HaòÿX¢PÈít>äˆb×OIÂNÛéÌàקg KUjݶÓÉÅP/K\eï«?ðö>ºëtT½.Ÿ®_Šr³ÙEQ´ª¢¡'(UÓNV²ç. ¿Ç/E
+7ËîÏ#¹iPzÎß·jšÊu+*V ¨D¢s_mcµ ö¾{ßîc»¯_[”h´Â3ÇÕÞ¦Òç"x-bÕz¾œÆjS›ºxx“›N¹ji==ç8ܯž•¿/ítl1DÙo•ý^½«+v=%éj ~u¶šÖ‹!Š-û8yù{U«þæØrëq«½³~–.¡Ÿ•‡éOÙñÞ,áŽ;Á÷Ê 7ZËûUéo“|†é8~¦xi’\þ:4 I;)ôÑ¡_XÌ’ÔpOŠ!S„Í°(=ó¯Ã»t–«–­A¯èÆÉ]KîúCŽ3·ÝŸ×“¢ç,U±‹rÙ] G츥¯i@»+fG)ijѽMt»„ß]w­òSz½·#tô”"–¼I‚Ø´$¿x)Æ„ø dPrp!Yñ计hè4KòyEÇ%þÛõó²*:nÍq‰eߎ“¿GK’-£h–Û)¿dÏY5½Ÿ¨M’sâçÉ«ê‡mO<ýIŽÿ–ÕÍRþ<?Yo›¢ß%~{žž›ÜÖ´õ–¥äô éWéë•>—¹­ A1z],³Ý·n«i¨ÔŒÍîKvO« Ûy=šŽXvǹëdŽËÅPö<SÃö|ý¾ u²¦ óâG{ìu¢™¾ÀÐâ"‰OÖÜ:ÝDuÕÍóÃùíÒ<‘f˜¨Àœ‡v8÷Â@6V¦¼6­¨Ûmöªàös–rÇñ¤ØŠßÖ ÒS7I8×ÎÁ±'Mû³ÄEÐî6ÝvÁí>†ñ×±\—f™=QnZ$¿s1Œ¹L½Ojš^–EâÑÔuÏ2$ —J5Ç­¸…ÕqhE´4¹¬üygg3ƒ\̨?ÉçíÂdĦuYŠ^w„ßÏ-“ÃÎ7ÇS›ªèµ+fS-jÛ’#v½?ïÄ¢±Z™ã“ÛÞI/ŶëØ®óU´7Ë™Óòð©))EY/‹rÏ°ž½Ô¢'XåËлJ®âõE·8`(—ÏϪgRSÙÛîpƒÝ0ËèGÁ/‹ŽS><–­ù…ùð"9í«%oŠ³t½íJ>£à³JŸ»jä¦-;>½,=mðëMr·% ¾…ןv}å5 dÂóüyÖ£¨«(ˆ–Mw½Êq^‡ÚµOO½$÷’dùuU«¾Y¢ÀÁ ÙìHIAÑõ¸ÙÌãó.¹¼bOüÛ^­3â0€6ë´ˆµ^>¨ýB˜Žß IåHyLêv;4$…šu|–}I¡F/L¦HájÙš&ú-rÛ"5åÍrݶ>]ñS]¹ŽålLë«c&Ë(äR2‰à˜>ÏP]f7S{$³tõ5&QËçSzã³Ó1Ñ1 Èó³¬v S,±©š½I®d˜/IÞ4Mnë‚aÖ ëcØ£¦m–°ŽdyÇ•ãt³D½ìŒ¨e·Kªé—ãšeí¶¹lû¿iÖ±Á/Õª0J$ߥ¿[>¾~¢0ªSŒê´ÚsöëX³+M‹.b\Œ<1[ÆKÒ&Ç~ÿ¸,ìqïÖiÁë/bf¼Z<úH~³jYU Ÿ\ö?ßO®jjÕT ÿ'
+2ÉbPbH ÿ,ùrÄE烙'÷¼Šáš[ŽS¹+׉+¥j[Zîâ·“ oŠ% ?ÆåÚO³·<HX3T¤”‹¶üw‹~ÿVÉ°†¯Ç±f÷
+endstream endobj 14 0 obj <</Length 65536>>stream
+Ÿ×rÙQjÖ߆‡ßHMS±cUj¡"¡üün–l–õ&IbÕß$QÇ ‘;nEÓ‰uëh¶ÑßêG( 2E‹¾Ù†¿’Çñ²bªq^uyT}É1 >ŸÖs?VÛøjÙR‰ð¹Ëq¾I–è6ˇgñ쬷=µjoŽ5PJzpQbÙ˜ÛFj™–“*%.:ÆGî8›U´V®\gµLåläŽKÁl)=íÏ»Åå6~»”L#ûî9Iÿ _¸\ãR± yÝ8qѤJ  ½nL)tzYÐ9‚ÌPÄž&úUÁ:õ@3Ã~^z%]Ág,Ó
+Ò?bÇ(˜ÍÉ0%¿zI~ÖÔ¥ë1~OͱH~›ÜÁÛë`sŒ›$ÈÙ Ü&jW”:v1òЄü’ËÞ£ˆr×”ý¹m z5øùgY’Ó2"Þäç[<¯§fk~Klê1G“‹šðòŽ• ˜Ê4Ï6IzÖTd·Eôz_Qôãà”1…|¤”äµ fûv½Ï²ezÁr•à¼Íeÿ¨Ð«‹ÈoíwJÇ벴ï‡Ø Õˆe§Qr˜äŽU´¼²ëÑËÒ çòëFÈ (ÀF‹&h´`q•àt vc1\·­Ü ÈE®'É‘üöÍÒ¿6ÓÜ'/Ž óÄAE~¯Q¡P°ë›¢_†vÇÁZÖ¢íu!Šø¤Øòñ¨€þìŽÜµÄ¦=â᧓¡o–sÇNÓußzY‚RTå²®yþQæ8¾$u1L;œ;SðÛ—ehEKøûÅDò]×óÆžöUµmÝ´œ }´,µ(Š>Ç`™P²û¯(‹Ëô¾qÇÍ_WbS;.Éé1 •J'IPzªì8jM[°»‹á ~'wmÑ0 ¬ïÕÎó¤ìÉ…Eò;”ž5èÕág“bLê”CÍLÌóÁÉRÆôéf%‰0€h1cÃ…™á}cX(—ÔŠGÏã(iùHy Ñâ¥ÇJìd§o|í¶
+Ö©T×»Y¢ì´WéFL%bÏ,ùÝËQô²1­”(†E­šòï, ½Ä®þ8¶dØ.I´ã>0¬²q:}R<¹g¥E·Uvüåãûé©Ÿ% ~?+Û"MH¢…L+fZL •ìæ¦é·«ŸðuŠgf á*P"˜RÈ…Ó-œþß–NÕzey·MÉ/‰«øuKhí¶Hv·ÍWV>×Ôëb¸Ìƒ
+¬ +\´T»Æbx!ŠUqÛËÏE”ó "‚C¶òû¬žÃîô®):íŠ×Wýºb–åª%VµIRå8³Ó ±©ŠôŠ!!«¦Í_BÕ’ºÎ]×f’”¦8XPXP&TË⎻ÁpGxÛll³ì€
+RÏ–-×8‰}Xõý:ÒËÆ`™Z @.\õ®³œ=ÏUÓ- _?דÜ6ëu[~ž¤ž£ÔÜÍÒÁV}“VÖÝ´ôJ,zÂã.¤ j4S
+‘^7ä¶5PJfT¨Wn³f7%Ÿ]x¼ŸcŒ¬D|?ä²+Z©+$°;ŽÕm
+/»ê–ô¢<XD€p1cºï«®0}h°J)û=RÏÞ=(É2ò°ˆ­P4ìêoˆ¬Dð¿™ÓB©‰¢c‹èx Çcqü¤gÌǯÁ:™Ô0‰-‹Ü4©UIôz¤çí±¬=zØhÀ7FZ.ÎOÓTÏ.Ü­f,wu´¸ðC†Ž“Ø+§U·œÊmSžîü^U·ÛJk Äe$,8HV-¤Oʦï4MÁQ Åß]_øì2òœðx ^Gp RQž$׿Ïý"%¢Fì¹%»?J–\•%¿<)þç(B¿1¾Ä¦Eþ>ÑÛºÙ†r4”ëz¶õ׶ʹOYÂlVÏ~(æà÷aUöó`14ÉrHea®ûP S¤h9î¿;üJ´ü¡ézmC3ýaÙœeK9›™Ûtð;½î(†[nû¤éé…sr´Å°Á:üv“,ù{±—<žh'Cùw"\¸h <Rb/%¨eÏNç$Ϫ–™›ÍÜm®·EÉo‘«4²ßw)²¤J4\V?ÎsŠ%weÑóž}Yöfiš_®‰mÑÍæí4Vܲôùk†_2|’㢅¹­§_>3!Ï ^Yq›rÕúÛL/›Ä&#R¯¢¦—-Ñm݆ÑoI.›ÚtåÃã8ù{{Q¬9cò‘òºqÂÚqÒrùô¶ææÈŸ¥ ~!ÔŒ!…Jtæ×uX‰Õ0q¥dW·+Ø…»­GË‘ÇQÒr2#úInºGSû<K²r[-KëùšÝ½¶KÝh‘Tþ]þ<ÿ,u¨˜üV^Ãt:¥ÏEnšÅ„r[øÓ^Hü™“%-ÝƤB>Ä á¡…Œ ˜
+Æ$2Íq-~°úõ¡»aò‚ù+ >fÓ§÷5ÑójÏ÷mCGÇ”þ†Qò¼øx+~Gëz‡âGEK²«›$ÊUEd–‡[xû„·GòÚcš¾Y‚RÓ䲡5õœ$j~[yªg¹ó|5Áoë¶ãäË’Åû|üЉ®λmô÷½ÜÆ"Öj)‘^®S3¤®¯Y†ý¯ùýœãÉ–y÷%!‘j¼°€`XEýDU´#úGjùÔ®¸(æèéêû]eÙm³C0õ¶,½Ekd€1x´˜èäØ"±ˆ@ŸÔÜAÎsŽ¢6-½k©U?lzjZëu'¿'%ÇrÇá!øŸ¥^Ž»IάT2LN`DÁkKfSôZ¯úÚ"(ƒÞˆ‡gs}T•Í:+R¥° TɤŽQ뙇߫e¥×ñû·Ûæ;µ©‹Ÿ«yPlÊ“ Ž–´Iž˜D3¯‚õç}Ts†ä™é,ýM‚ÏñÇ­ð»ÄòUöCÑZžÜs”–öç½_v:c§ÛõUÊ“žöè}R²„¯GöúUǧ·e·Œô¶'¼ž¢ã­ùí͑Ħ¥—ù}˜+ǦVõÁE¤Æ ÅâÑ_4JËÞw2D½¬—“P?cÅ„0XþÕ¦!s´A/´š,V¡ÑŸ—cËïAJxБŽ®\ö°¦¿šö¬a[ñj™jyeÇ*~ä¦[<:”,¯wÔüÙuþ<†ÖòuÇ5%ЊˆÅIôR
+ÑãÈšã–ìŽPÅž-¹…ᱨUIñ+šá<=Kó«BÄÃõ:^(_幞ß#‰v²rrÒë2¥(EámÓIÒ¤×m”¸fL ÖüΧ†lNn|bßÚëÚ¬ƒƒž‹˜JÇ‹‰ŠWêd2ÝõÇ4$&:dhìpÄC°Äª"5±é  ”Ø)ž=çI’Û1š¡çv#÷k²Ó§6máB½ L¹Yúæ˜âë-¡Ÿ$·MmÚ£¥¾¢?즔D¨š&Ùí˜HÏMpìIQ;o1”¿®%»7ØÁÙ¦¿}p9áÁ%…dÇ?©y‡áÉáü* $®A/å¦+ü½z]=Aiš£'z( ÞKõ“£z'µ,éo“Ð/âÙUJ!ÔmÓ!ÈšãÜvÙ²J^Yµ{šÝ=-o¼´°8iáÙ˜ôº1£>…·GhéjÙûq,èµá'âéG|ßÓªWõUÓôª&¾=’Óô”Á¯uÛ©zîͱ…ëC
+ðùNÇp¡h˜ÄLrÜŸ#˜?WÉ­ˆEKv[$·K­:É•Ì‚Ö2&5ZsyÔÓƒŽ¤õ„Í/Ên—ܲK†q\í.|µèÉ¿¬B+¤žEÄÔ<5J\¬Ü.Éî‹× Æ$BÅ/oŽ¤7áeQ{ŠÚT5Çn·Åô™ÇŠH-$=HZ+ü=‚Ë'ÝWÏR ã$‰“¡Š'ÇKý@¡øù‰>Ë`™Jv\eô›ÜõÔ¦-ÙuÍïßƶX®wOŽ3R£“ýž»Ķ$¢Ñ‹×Eö[Ä¢wøí$iŸ(]G*›‹a_’&Vý¡¦%‡™””ÏþaÑ»$÷eQr¢#fÆOS—‡IìEô‰Í/‹“ätÊmEîJC¥„Ä÷Ôž÷)C«‘3*3`©Ûm5gT©%$&ØõOR´š1¥J Æ+EÂç¿ËÖX!9Ç –ÊÉý,÷“ÌÉ0¦Ó5¦-AæHcuÂ!âb¡B­æøGÍЪú陚ß*%ûiÚã73âa~宼¾ÜözÛŽŽ,£åó¡¹ D†1›‰3:¼¤ÄŒ~Q«¾w‹ ÉŽ§âW?ɘÕèÇL
+–˜–©Åç»ûöëšâáMnª‹_¼uàMùû%#$ ?UÇsúêéÊm?E{´\!y\P"’^OÅ0 ·G.û§å1djT(S ëfÈâ¹Kôº»-ÝîA†‹Ò «æV…¯_>;ì–÷2ìG´¢9 ²púõ{zölÙñ
+nQmÊŠ]Ñšú¦Y‡ŸÊȃ²åP‹æP9Á#F2':JV,V!Pÿ³èê–[´L%±)hEOr«b5RÑ.ì–SpëzÊGÿf¥5zÕïÉMO¯ú²ãØN«ˆüRüþê)Sú° ` F±fÁi «bW¤Ïyôô¨çivU~]ôÆ&û®‚D¢‰\îJ2C6ë”›Í:rhÀ 8ñü§ô,¹g*“‘-ËÛÆr8(øÍ!†ç0·Ý —‚[0-9Ĭ°€<¯Û>ű‡MÔ¸Ô¼T)ZÆÁ¯ÁÓj¢Øó$·8`TNB!’Š¶ÞÕŸY<»ˆ§à4ÔžªÙ}âG bȈí:ÊϳX‘pˆ¨\˜@*{­“¢gE'@€!xЂÌPq ­éŒ d£D%V²!bj‘úœÖRÇJÊ
+¯Ãp¡ZvüºãÓ»êf¸"Ú_NŸüaù ¥åƒÄò̘@5,ÒêžñR\á5ÈU?)‰rQ¾ŽÙ±ÉUQ²»—£)n_:MÂÛ%5]Aú˜àµ/†¶·õcx‡¢ož+R§™U*fÓ=`­(IMõÏ“·,Ö:u×ÔÔ¥4ÒÙV¿˜®»ŒB3¨Qש5»ø¾]æÚibȸ8‰…ÚU%»0T¡,Œo¯d·Á·ãJí:ƒífÙ›d‰^·HfÀJ5^$~‚]•QHÅ*…ŠßÔËÒ˜B<¨pÑ¡r²ªß”NÒç#|n#Ö‚Ù³nŠ:ù…Ò2–Ó¯[Nùý0/-§Ñ6`q# i«g“»š^•Ô¦§¸U ù*!Oš3DNÐA³ÃJŒ‡[îw‡û\c¥€øáÆ ˜=Qs"fÁ‚¥FÔÇd7”’¿b
+Õ”@*Hž“ï[ÓoO.¯æø9’Oß‚2™p»uÇ+!þ%%rq©`£Pìÿ=-ËÂgšÕ)eÙq.ÃüügÓÜîh1ùYT¤§u€!€ IÈ ñ¡†%ˆ-:XFHrìÒq&$¦XÆÉϯ(žÛÇ LÇ ŒÄƒ÷ê‰j”ÿf‘µêÔ¦1Ý®a•ZHŸ×]›hZGOÖ§|z™è¤ÇOðú§¥=‚>jžà—7I‘Š’\•½Þ$kHͧ7©åZöÅÖ: ÔŒÕô^’j§ó—%lŽQòZŲ>9ö«Z›æj–u°¨Äpï¶\–ñ*Í9Ù))AÃE:Èðs⧧ë¶ínûÉ%[><Ëä"¦’QÒbá¶M–,*•?zŒà4
+?£ôsMê/B†Ì˜’=ßË-–àcÆ¥b}NüÛ´¯˜D9°˜É&¦$Ã:Z¢€ü”èF‰kÅŠ¤‚[Z¾rz‰8žøA‡U=¯xt#*!\Hz¤tŒ¤\´F-\þÓ²F%‚;•Úô´ž5(P’ÍjôB“"}^Ö·>Ô°œ|öP{‚ÒñDŸKp96·=T´üˆñbÆìK•’ßÃèÖ%³#õ c2,
+‡Óm“èô”— ©Fħä°
+^£ú´ô˜-Jä¨C†<Üð¡âjaògD< U Ä¢+'PðÁÅdrÉî HŸÁ"Áh™LúÛe¿”ô¬ç>vሉ×oœ´Œˆ“
+Õ”~!†ŸIx™µ×#ûMrÙ–ÓHïzâß-V#-Ó .¿j×åÁã3|A¢ñèþIþgHb»=NRбˆ2-T P?b¿IĈÙÁ…$…ŸY|;&Ä·|øZÂøGLŠkÏc·<êEh85¿¢WUÑl‹~Y>»300NJVX¤&€Ô
+*EOùýêøƒ†&tò èˆ5ˆ1y‚ LˆGémjòŒS¨ôå(4
+ó3úéS>hcEÅÌO«¡µü”ÅKº¶xcÚ`C]u–¢§»O%ò¯©uSRÙušBaÌŸÊŠÑß°?±¼&å
+¨æéèj¥ë¶-õû-<å„Jå&Ô­ ,å÷_›®
+Ußh³Me“|¬9íX¢]|%ò8÷9Žt2§?ÃØ”Äa¼×¬òƾPŽ‘Nfôw7EqŒ÷šWžÁ˜/Éa¤ËŒþ ã¦,Žã½æ”7ó%9Œô2£?ÃØÅa¼Óœò Æ|AŽ‘Næôg3qïšQÞÁ˜/Êq¤Ë ý c¦ Žã½f•7÷e9Œt2§?ÃØÄa¼×¼òÆ}IŽ‘Næôg7qïšUžÁ˜/Éa¤“ý c¦$ãf”g0ö9Žt2«¿Ã؈c¼ÓŒòÆ}QŽ‘^æõg3%qïšQžÁ¸/Ëq¤—9ý c¦$ã½f”g0öE9Œt2§?Ø)ˆc¼Óœò Æ|A#]fôw3Eqïš¡¼Á˜/Èq¤—Yý ã¦,ãæ”g0ö9Œô2¯¿Ã¸)‰c¼Óœò Æ}A#]fõg3%qï4£¼Á˜/Éa¤“ýƦ Žãf•w0örŒt2£¿Ã¸)Šc¼×¼ò Æ|I#]fôg7eqï5§¼Á˜/Éa¤—ýƦ(ã=_¥ÈDÏ‹8tï-Ýó‡æ½D {^âн—4Ÿ—8ôï%íó‡î½D {^âнGèž·8tï)Íó‡î½D {^âÐ|/èŸ\’bQ˜É{Äh ¾õÂÜÇIN¢~«yøÔQLÀ—?G}ñ>ö1/󾨇uû€¿Ý·³éSRÿùSòÑ%ÙÍeL+Ñ9ÖÖÄ-óÿ²Dσ r®<Ù³
+!y„wþÑ "ßØï-·©‘àã6Ìøªqë7®ëbëûh*z[½ ;­Ík†·‰Ò¢%l¦ãGðëÖ–qëd+‚~Mí1c^‡·n„åx¬ß|~C ;v
+ï½"2T—p©cÓ¥tè¬^R%êþF_BµóS×6Ö>«7‰kØ'ÜÔJ½
+¨“oÕ¶;‰ˆb˘+\ QGVÅBXå.tJ_€Ÿ}÷¸Dk0¿ªí1ÃQËñ=¼}àËÃóú¼²²r³ûI.:T cg@€£É±ÞÅ`3ЮÚÍüŸ„aü5}ÿFÊÜoNµØ
+zpˆµ¨~TqÙ3äaÜq¥!èdO¤U
+Cc²8äR‰ nDM¸EF‹AfG=°ø¤qŠ‘Q¬½aX
+c/ÀqôEàÁVj•íâWf}˜Þay)½Ýs½–$Îï@†.îþ"¿DG6Ò’Ÿ|Z ¾¸é›:èÄØþ¶£½Û²×†òv‚ñ]“þ_QJx!åÿPžê L®O;ò©AW¬(Ñ“Ó‘vÍ?+è
+âÏQÑ¢
+m™«Ð`‘o¶0Sq jŽ}W)H)Á]vË£€ YMqîH,Õy¼Çf#mŠ‰úMÒãu®û»< %ÞÉŠ·«‰Y¥›u·\ 8
+¬–b‰ƒr 59Q »o4¥4‡fS¤+Ýlxôè€bhîˆ)ºÞE ƒ©i>{²
+GÁò¼n3è[RÍÓn³€|]!Fªkû¯â˜Úô `+dЭ£PdŸ¥Qþm8„JÿPI¡ñKd‰xmå~2$•@s!¬'L½:¦– þóW’. xí +åõ!ã‹\íõîÐu”-þR"ÙµSPÊ؇p‘Y7–¡.åð|ŸrÈT#äc_f+épšî¨%`ükØp!Sf` t™Bg‰ô;F û³ !þ ÈQAêµOZ ’ÏJÝ)¸Á @±üá- ¨)DÖ
+‡³
+1 0 ©ô¶ís¾Æ] ¹`®i
+&Kg%CºˆT6QX*Oÿ8»lw6¶YÄ
+›Â¢Lp¾åÐ`ø æÍÁ¤^ÊX¥ Œ½ñ
+K8*F±ÚmûÔ¤cÚ#ÄÂ
+`a£‹ ý{b\e¾É]²ýܵmDˆè.?—Ñ?‚f_ ŸO‘ hRO ¶jä‚|ïŽ>»ˆ£ª-è—‚Û–•-*‘˜a|Ôe•2r„tä
+Ô]Ž§iÜC¨·éÛ¿y¢ŘWh<çê…ƒEØ%h{ÐEÝ×X­`œIœ®ÉŠê-ÛOÁ9»`^<T.jß°7ÃMF6 '”.ÔöXRP¥%¬é>«Ð¾j˜Æéd@[jŽß±38>6í/4ª}7ÿÑâBMˆ½°lþå”Üb.:Yœ{$dFàúŽµËXŠx¡¤s¢0+Ú9 ¢Œn*8¡GN
+ÐÔ⯤“Õ{ÛJÄýoc»Ô°ZpyNÛTè5Wòòn†ÿgà¡b>ynæaq-Œv ™[p¡·lõ([¡¾È„Wºä6Êe¡‰´ÿ‘¹õx¶Ô ‘'Ÿ¶I1Ì¥¯kÉš/\ÔûÍK¶\
+:ð¼žÎ‚;vÍ^wöÔvã3h¦êD ê;ÐŽJ¬°¤t7æq¼‰ž,K66Æýg¶™ÞmxØ!¼ú8·®94m¾ðx“ã—PYÞ`´à»XäqÕM˜.!!Â5vx_hŠlQmÙ˜XN sÐ%†Éù}áYÇGd>ba PxS
+#3ˆ0q½ äÁYeÖNȧ„ž 7³¯Vb·6Œ:1¹mf÷ƒqϸqÌkË[Uá< ôÄt7ÒHÍ“¸
+F
+ÏóÜp»Äqþ¾žG¶‡Rrø°5W¹a)&š2¾®­ÈëÇZ|èdð±‰Ò;+‚¿5Ñ—Ñ“BÈü¥…X[*I
+µ|¥H3øiÐ8™IP0轎Ækf‹9±ô y05A‡5³;zîƒÿ]Òþp•L2ÕuëZ/õû…»ÜÙ¬r†³t¯N0ñk³Î:Œ°©
+¸Á*È…>@1†ÜOÒEÉ]1õî6'‚t±X¸|³¤> ~׆U„GYr>n¥3'tàŸ°ºr>Ö¨~#G¶()Óþ ëÀy
+pP¾ê¬Ìo¤&*Ï•ÍQP7–:QØá‘þ0áJŽæ÷;WõIvD ÿl¿²©•ƒÚ+0qî€Äµ4¡HkÆ
+]¨"8—eÑ#m´’þñ “6‚²±5”ñ-¦°
+˜Ã2e™ÆJ¾kO;I&pvr²>B« ’w# ô•Z Ä„Ä#XPéçþÖsœøp*ÅÍœ
+¶^΂9~vi%o¢†rM€±f¢¹Ó”Cqb¿;le
+\/0ÑÅ5Ôã»æ­¹CÚ#pÃ;ï¥ÜH¡†Š"†QåF«ú¡´è¬ù1/Æ{åŠãi ÉûËmÓwe
+s,Õ ¼¥-ꤚ I–Ú}ØLxQšÓż1L#4Ã.eñŸ †/pÛü pSé0w{’”’üÈ&˜z+¢¹Ú6N‹ò 3ñ# µ ñŸ“6ùÆAŸÚij0 ãábÝ'ê{ó¡+ ®ªç—F€w°ŒÑS\‚r?½tðö1"hu}À€yÂè &»m:;tãÊU,âăˆ<|¨
+Vƒ½ßú\è6oAH8ßò…ŸßØæÐÄëþ/ ÷ÚE]aoÂ×Z»°Q<ø/Œ‹Ê¢e#;‡62ðkÎ'5±¿µ,9‡ÇÌæˆkí³ü>a47é²D‚‹WÆ¡ÃÇGÇu,GRœb‹VXì¹]¢û’uЧѮ@PWŽFsµ$Í`S´b…Qé/Èñ¡7Tp
+m´
+{¼²Uc;’Ú ¼ôo£"®ÀDUÙÂdŽoÖãÚIc–Ó/ø»—lôãû o êG†8æ÷‹=ýσöàwPáŸ<uH]ߟçM4»,>±þ}Z›>MDš«ÉóŸ Ÿì±Å^È÷o ¤àè–žúDƒËP!u@X†1E–Š»š§¯;‚XéàWN÷Êÿ•S•…éŒ,T_CQ*¤¨‘åÐÿs 4ÝÑÂ6¡‚æü–”H]ƒŸô»Çþ%Q–!8WÈ"
+ÏA
+ýB); }.šðX›ÖŠé)#è#+ü¡æP?N”ìU+B¯㑾ï`¾]#m?uOÂù†ÞÑu›à=ý8îõmmë£NuY¦›ô×ß:Õ„ÈSAÈ÷Ú4˜ÌºÛ^[¨±€ÞÚÙ…œt,…>㯌h«é»>½ÞEţјrI(ùhUË“ì Ô-Ì®EŸ›‚Š`ly›kÅŸ#w@"êG,1š¤õ„Ë ±;Óô#6vezrú5LN|æðòh
+0ßQª$BEÙ
+{œ×“2L½!O‰ÖÚ¬ÌxøEX`’Iµd/xl`U9¦„RÄ_ŒƒàïC
+ŽxĘ37ÄUB³ÿÙ«º·°HȽ7 d¿ÉÊxŸÃd¦0£y³1BÈ’Ç÷ÿJþîÏ•£ªä!5ìŒÍðIç€ÈPÏöD×ÕÌ~"+«Ï „@ÇS‰«Þ‰Ê¢ÑL0»öFG7&ê[öP€z
+€n£´ß™˜–°ðË7Œ6«‘샬&ç`Ð¥cR:uìX‰ðä’–1dÒq©Áfô\BŠÔ
+B(
+êÚT!šô(†¿C1íáŒh@ñÝ£¨rw€‚\Êš”mÍÉ´ÖÙ³ÖŒª“Ì‹§gƒä®b0ȇOs±ˆ1ªxóÓ[ŸøQ/Á¶³c2ÑìW‚±ÈkXZ±þ%)ÔfR§ÜÏõ”F )•Æ‡mœÐF.£Ö‡î6Œ/×’|)aŒÊš¹Ô‚¹j®Kƒ°m=)ÞÎoˆyÕ¥p|¦ÑS»˜%·&kÜîÇy
+XT¨d¿Q] €µŽ4^³y`?Ê©
+`Žmó™^.§=ðUˆÌS
+v!¬°P
+ùÁÃúsÂrž­vÉ2_Ç›ÐP5 œ’öJ1æ½àžh¤u£WRÙ”ì÷ äsÛáƒ"vñÄ2Ã=~¹‚·b–*3€Òö€ÁykYÍ‚³*Ê?úÄÌÍÝYm¦ùÔ´u7ŽL
+›ë+XÌÄv½ã±­¢fû3û>âæ$
+±ç}LCÓà1¯‚ÖcuÍŒŠþÞñÚÞ÷šx×Ä38‹™¸§kðx_×Ä*ð™_¡ÓØ»~ï{ž¯¨W°èû»g±‚fUÐð|Ló,ðxÅŠ|&¦Yô{­¢fEnç4:}ç4Í"ÖY°àã}¦ñ5wÜÛûÌ÷t< Á„AC„O¾=#óÓw¨-ЂO¶à CL BuÁ ¾äPà‡ˆ~%
+ÄP}0œp1¨ÝT€4äK°ÖXPx, `5#Ï™"Ôe±h§ 1eÒ4¤1îo¿Ï†™-w¡P›>bô_MÄPÉ¡ŽC1TŒBo™#Cšü´HU‚Ó~$Ã÷}ðˆ" +~ðGC¾8?ùƒ4˜p1¨í€þ霟Ơî´\O÷ 5è ¾P5…ÞEB >=ê¥o½P²_Wüa
+/XMLNodexmlnode-nodevalu1t/Array;children(turbulenc2attribute(nam; (2result,numOctavesnoStitchsTil0.05baseFrequency,feTSourceGraphicinin2opeComposit100%widtyxxhAI__1ididobjectf/Def ;fractalNois44blurstdDeviGaussianB1ddoffset2yyfeO-10000-5xx2zzfePointLlighting-color:whstyl(specOu(1specularExponenConsta5urfaceSkk33044lit11arithmetMergeNod(14-2xx(AI_BevelShadow1.adiusaidilaorphologbnnb(-db2sb3AyChannelSelecRxDisplacementMapmatri0 0 1b34Mfrom(0begfreezfillalwayrestartotodreadditivnonccumuNlineacalcManimbc-8cccccccc1ccccnnnnnn(1145xxCoolBAI_D_663erErod64_771 1;20 15;200 200; 15 20;1 1 remov1RindefinrepeatDspli7c(AI_PixelPlay50 5;20 20;yellow;green;blue;indigo;violet;red;oran518azimu6elevDidiffu1l1re5001kƒN¨’•43#’$©ÑâÃQŠµ„%Í@`c Žd(ˆc A„@
+b!D)FAÄi5ÍŸ¥-«¹OZ°vàØ`µKLØ+ž1´Bl/f’B *-AÓÁƒîrFÏñ©š½¬jë¢(°ÆÃCl¶º?oçtzä\4Lï‰1J+ê± z[»–‰óhLŽ¶?÷PT¨€ rÅPãÝ'Æ_í
+#«’ ,Üz+úðFüò ˜«Æ,¹P±ë»ÆíÇ ùW^°›<ÃíÊvÐõÚ•ïºòa{ÀXÐ
+"Zp}-0l_bïZRå†d~"F±.cP¦B¯«
+vß–dëƽ‰ÑÜÐ×QÕ×fÇõxø«=¨Y~4³áˆ.ËkG+Á{[^“ÓÓ¨ ŠÄ£¡i@T>B&|›Äε#¿t®+H:¡¨°~<¬…Z+‘µÐYÎÍñf‰í2žgî
+új@8øáGÅ$Š>01ªbgMÀ Ÿoviü¬¶n Êè
+´NŠfË„ÎßTÜ*#!T ‘üXW@ÐL/ˆ—‰Ñ¬j"¤c*­—ÇIc’gòB`T«ù•Öï š-7àÈ°®~xUúÍî¿ êpZós… ­P>XcƒË‹•aäé{¼ªìrYIo̼xðq‡¸Ý7+ìÆè%s²ÜT(UÀç&]k¦µîpÛH<#Ê|Ò`ñ8w@ª%ài%´Çºx˜ˆ”’†É'ÚF>.DÅ,
+¸«Œq]§‚á˜ØÌê†,Qf¼™B žÞÆ–Í3µÙ§d}ëÈãp|a—ç* ­ÃÛú˜éŽõvx½ž(*ûð1L¸BqñV¥ñ‚)ëì‡4k‘0¼r†ÚÉY“9h
+ D€0á‚L€`Á0h°àÀ É)"T 
+âC ýRoÒ\ƒF¾½ô©÷àëFź»tôü8P³¸p hyÓ!¶ˆWH|@Pà€’G‰H*€@áA„ *ÀÀðÀÝ\‰?‘zCƒN “4ÖŽ¼Š|A%­{]ø­áKØuçã#ñš{­ÔÆâÑ5%®“Jœ%nrK Ži4Xà‘4j¨¥(-ŸGx² éùŽl×ð(Õͱ‘’ò¯-* iyoÄL×Ñ~Ó‰A˜y7´áñMdžuȆQ ÒÔ¥y%>f<»oKÉasM©;^1ˆ„¸êv
+wî€hðYÞg¥Fd¬¥-zEÖQQ8&g…Ü
+«I)ʪ\.ŠBq(Çùª(Èx¾QE1ìð€(ˆ¢rìØQ…*É*‘-¤ä\ùÎw@Yø¢áç ̈…-­ˆ†Ä‚ÉçôƒkE…àUX(*ç€(´nÒPl6ybºƒªö y@TȱbA|9J»R³rK*wÚŽˆ±ÕJ¢º¿8¹¡•$Y4ªyHôe³’¦ÑQÙ³ËeaÝ©"¢QASŠ%™D/u%k¼77Tí¡Ç¡W,jÚYA²ÅïNò#ww äsQêOž*ç¡XaqªÊ®ìçªãÞ+
+›#Z(ªl‚8LE…Õ XÞüãRX“£pJÌÔ|õSèr*s”híœó”y©Õž(J¦]*uèãÆ¢Ea˜Ã2‡dI
+*Ëíç59†Ùm/Ñù=¢}t‰UQa¡@U“7dÜØ9cR5xNµê” Cµ²yqcj.ï†ÎA•íqSW̬IDaß «qcG ÏÕŽ…r#]8j<œ²uä…õ 
+ ˆ…ª*¦ª¨ð§e:Òx@Tp…ÄÂiHáø¸çhH,T(;Ïù.eÃî&;Ä[b³ü¥6çõûËDQ?ã°¡ç— 7‡`ƒèóP; "üÄ” ¶3÷ÊÊ™Uu¾±º²ÚO©ûÏ}龤#<ùŠª•´V/¬JýíÁ;Ìßݧ»žˆC^3*õÄP>Zç™i qÒy[ós-'1vgWˆ €@(.``ÀX™
+Lx€
+
+PA  °28 ŠqYƒcˆ¡ß K'®ª8yÒû8¼î¦s¯”y¹
+ZíºµÛê9qéX®ŽA\°€K OÀÀ'`` ¿jæ? v›‰›rÈôžu¶¶¨YKcJ†vm˜®‰Îü¡r:³Z¦×m© ú†ÖpPz—ÙµkòbîþY“¦%<½yÓ™QéÔÅDDi•£ë:“ï9q„¸ ü®QåЈ…‘X¸Â-#Ò:5îîšmÈX·[.% Dx@¡€°
+ k C׫l,³ôJ ˱a‹e®+›nšz_Õ×—˜zzºîé²ÌN³›™ÙYgZv-ÐJK­j‹¡Ôjdëõ†ó»"Ã÷öߧÁ ›kkµ™êÛÕ³^ÚP¦æ55\Ó²1K555µ«™žê½½[[ܱµ]CÛE³5[[[³aJ¹v³Ü³1[[³qeä!:²òQªŸä§57ë ½û<+nTUiMéÍ`v¾ÐŒÆ*¼é!44¬­ô„®U"̲Ý%n]%©”+aftffff6†t5fÏ<Ü+ô÷mWg†J†<þ¹›Ê°ùìû=Ÿ4
+ŸXYYÿå”VÝ.­2§^9lt–‰’F4itÃÄ#Ê"B»O5í\ªT7Õó”铪Æè
+ÉHõðLí
+ñZë:~Lsˆ™ý>Õ©où,ëˆøÜÓ<\[2ý"îu­{…¨ÎCÊéÔs , Î
+¥Z¡ÍB¦w„G=rL?Ër)!’ÄØÖ¸K¯„
+”ÃòK=­ŽŠr…]Ü•$N´OLJ¿ð)ç;´ƒ®©’<\I#,2ãd•ÇÓ%Èv›˜1kì*“¬MµJ5Od5ez;œóòL+îê†&…„ÿ´‘5S9à®»]*VKjëƒí¼ZIöåôÉÓ˜({‚ä’å𢯈Œ%C`%ézl¶ñ–³Ì’Ø‹#¡a£QéZû|Xøzƒ{‡èXþ°öè—~Âñ”pß²_9Iß¿Ò ˆ4u¿ãŬ̂FA·97~¯{Ë»t¤+,7PÔéT5q’Ì#ÃÕ:Ϥ$FÏû99[ö–ªU-FÌHQ
+Š” ’àg-E{ Q‡}Œ’-:ˆ
+öÄsfuä_9¤dTe1ú|(ÅY¶Zô@¦O4³ˆX=ÑBt¹˜³×·¾¸*PW­3™ J0H¤á4¸iSž¹†ûDÖ°bG~5+Ñ©†] 16:•=0²Ž|–w¢ò8w'J¾‰à–kÅþ³Í8V¯$àÒèJ0B3@*d›”ˆb¼®Ávp¶CÑg˜â|8¥›ñºäNTDF Ù‰Ö®öËT¾åùFY×.Cëü×ÆŽ¹°¸®U#Ë­¶°Þ¡¾Â"Éi›±¢FþßDÍ€
+±ì^*B†Cš+Ùò6Q×?k̈}wp&àóZxMDa›îeh«¦j©ôuÈ¥Ý_`ЧÕU;"
+þ"ŸÁÙº2QÃ5b©vDȯE¨Va¢ µvû0ö©m)[tO´›y2u!ÓìŠ7ûº RH‹§é[ö2cÑUk.f z2ƒUkõÝêØØОÛì¯K[oºàC4;?uAVZÊŽŸéЪŒCµ–yÌ–ÎßvšY¿6"IõÍgë¼^».øÜÜRš+Î“É ”Ë‘¸w6W*®p»[­ýàìΫµ%[;¯;°ZJžæþÅ· %õ‹6v‹xã3UTç?ÝÌ
+xCõ>Î`ŒeF€ÌÎ(—Ï_àÞ÷iÁ/Ôˆ~e­5RøÎLƒ úf=ø0¸y¢mƒNlÖ¨g|ôwMM&ú…þ(ïáƒÎX
+:dÝÙ 1f»h–Ĭ \[ qnAïˆ%P$ü
+BäT>&ÅS¶(KÐÎ’*“Éežäþ1³z¼Dâ­×1é
+0r ùxïe"^IÞÃ$àÑ öF2z›…s(iɬkì¼G9"zeÆãAÜC'„õ—è̵‚ûq+ ‰‡PåúÛås!‡eoÙ6_ñtÔƒz¡Ž¸àY«„ úœ¢‡Ñ)AnûBé^¿š€Ìã[ö¶š–ð ¯ ÷"3¢'b]ÈEN×%4Dw:Ή2˜¢‰iØ-ÊÓP0ü¸…µªÎŒÊ§Ï ¡¸8 ðÝ´ FO½Y:0ÃmdââÜúxH ÀÏk¬qÄK^ˆ|‡žû¬ÇÄë{Šf˜Æ–‰³€"³èéiÚ£jâ¿%«»JñyÒñh220 :äœ6»ƒ$X¦AL €$ðóá.K9oó
+ÏU\51}²ë“£GÀ·®#áªýø=HÆy$LƒÏÆÍ09G‚y1°îßü
+ÏÒÜÅksævHÄHVšçÑ—y*-5c]èÐp¨|!^Xa0O¼b:Ábög`£mjqöà;öÙäC¦=¹
+Õ£< Ís佚‹Ãvag%ÏRµa°+"±ìÊ}´}Ô¥1Ç©mÄÏœM/‰¸p
+]W¥ó‡Ëw¬Ç;Rµa®†äÛ/B
+Ún‹©5•vrhÕƒjFýÍ¢•ñ}ý@{}r?E\Ñøù±›BD
+s+Ó¡·’S,³¹ã‡˜jæ 
+‰yM+)þÍ-ô<ç¸Ó†ÇæW˜²ÏôÂr•š© ]ÝŠZ‘C‚OýfêæÎ'À4È€ ièšu¾êô+dB’Nûgí+‹~„è^»EµÕ~‡Ä ŽM©¸öÕÛj\¯‰ ®q¥ò'­.¨|®Xg[nã~Ã4Ê–„Ø(×}ÃŒ›$;‚ß¡<KP½3ãV0>˜o Þ'P'Mº–1O€TL[Ä2ÕÂ8àäÁ¼š¿ÁÀ¥áfÚI`Å —eˬpvB:“GJÔô–âæJxE =ýˆû”µX®O½Wû”ƒõ°Î®½ZuÞÄSyÚC”ƒÜÐY y‘÷È~Cc½œeA È
+zqÙ
+Ö¹¸Õ±"²¸”õ›‚…+äz¹ŠÜYÍ ÝàÎZù7Þ/…”Õd¡aÐ˯ºý^*.•–EæÁÚ¦Pgh¨O×¢™!_䂘{Ä\XsÞ.¤°XýS@ÛÙtXa56)V/Ûc5&É#ËôK¶ ¶7³ƒÍ]>+%Áj:€+(bÌ£j.`Ô°•Ypéä1Ⱥj™z ñnàÝWMþŸn¯:–a4W-˜^™q¢¯ú›Í,È«6g5'Ϋ«fúÄÌÈ«ÆÔ<),ÿ¯/ôÓ¹ñª#ª={Öõîb¾sÕbó:ÐD»‚Øî[à+
+xž|Í¡?ø¬UxxË¥ÿxWϾjE_ «×æq9ŠÕa[?¶y V³g¨eaõ”Áô±:±YÁÓµX¥~™Õ—3e,"«têp
+)agyƒªT¿Ís$DV× Ôíd²ZÿžkÛl¾@ÁN]qn?ðÑB‰½&)ªÕ ¬e!¤ƒ¢ùw…Zýk¥nRP«û©ÂCæ¬çw(É’K‰9¾›4«ÿì<`FHº°:Ž^ó~»¸j–¨<ƒÛÊÖ7^µXo7¯:ãb6‡œµ–²À«FU bV° mÏռꕕñ,r`Y\¬V”÷«'¢:JÇšX½MÑäÕ§&—ÕIô¨ÇÔ¨éáÌzja5 LïˆZ…/‹ÕYݾ ßØX„£,Ñ^y?«­Þ›K¥vªAmªÕ
+…g¤.H¾ F„«Bmíó¨b?@—÷z¬üñ®ÓCGç:#…CbÓMÊ~\b¼€¬ëœž­Ÿñ’m¸ F°*­€4ñ'@-@ÂÄ•”X;°q™šN’ámÁGºt¯K ÌYŽÔ-¹3ÅË2%B_üs€÷˜Gø %m}¥!h¹Yâ8Íkƒ˜™GÆæ]€¢Jge'<ø¾ö-CÕ)¡q¥DÛýN\X7 €ãÆì%µ©?KÍ‘ü½üÖäd4@¸UÊûïDÏàõIî7ë^€ˆs±x8sÎt†€õ¾ÕÉ÷½âûꭘؽ×]pÔëB½\v 3ÝX3ÝêyÀ#-é~BsU¤«á·-êÚ-t%xXNß%
+ÿÛ*˜¬æ‹fb5ÓRV•ýÕµ9è³Ê8
+œÇ*‡A Uø‹#=¬†þAB˜ãi.JK¾3ùÕ† k(Äü†Ûn¾7í¹tH¼y9yúÙ
+o©÷s'ˆê»\Ä„žNË”¬®¬_±ˆÊÑ$û¦TÏ”CVM⢿ºÐãBë3On’4>uš³*¼3>euEbºBÑgIÖP÷#× Ëw°nU#Ÿ%š—N¼QY[Ã.bü¹„x€+ù®¤ó˜úƒ­ôÚŸ'^Dô³5½‚ûšVƒHu‚ƒÄo° ŸÚ—´C°ýOë¡kÕó¡‚´8s¶ˆ¹rà5~ ¢W3öI‹1,$°ˆ(^í"æ„ý•’MéÒ«ý"É9ò@xJ*)ßæs!N>VLÎ퇬ò;à{²Øi¢¥À#OÐt“ý ÷D'<·Œ·ŽŠÑd3Äêf‚¥Q}¨ò"”A]ck©Þ
+„&kßíßpôˆžØï3þüº8{’š§»èBÊ<R3Ÿ¢7ïØÞŠûk‘b²8DÇV¬|rn¶—ük£*—è+‘È?¶LU¬ ÿ<$n:GG
+/½ì2l½R|MÜóç«•0”êZõ¦+àè…ÍÌ®¼r9XÃ+y
+ØÔ¹ƒ_sÔ´-ü þË¡’ÿìû-"î&cçäDÛ†ÓH•ÑÜ”1!/¿±pص9‰’:ØÜÃb•¨häc¶Š`ßˈo”ZÀBŸLJdvé=&9EVžqJÍïÐJ‡£ŽáK  ¶™. ÿÖ…õ‰+šò¿bŠuz‚Êð¹¦Ä"DÆsuû²Ôä‘ôÕJ/‰á“GKwwPÎâthî™Ìø–zëÏTÚX&Ì1÷Œ‹Ê÷Œ4ŠªEìÔ³eDl{ªÿÄ÷æ 16‡ 0¶aW–á7LP‘Fy€åÆ·%I&ȶף‰Ÿ€'R
+)\Œõ)[83QOZ¡î¶[–¡­8š«ÔΛdØf$õB½×°ø™’¦Ó™ÎñDÎáòn|ltÛ*PЊt¶£ÞKèp™¤Õƒ§lEdËF½ÁúTyÂz|ê?…Â֣ƿØHOä±4îì 0–?SÈÐ6€KOº`-·ÇŸºß·×@ÚÏ\M-쇫ù³<Vwfí”q,·`Ș|WN¸â1ŸSØ£bÁhzA|‰8¬×n²“…c–¡$ù6ÞÎH,+)KožfØ´á°ÃbËÜ2C-žvÌËXG‹I€e()ó’8³ ç_JÇ×0Ô‚"NýK¹=ÜТ¶r¾|Ž5“ÕD"ä™DDerW¥•‹>‹¾ëžFWñ•T÷HÔ•¢x<ƒ&¬–êÍsÛ¬ñœÝ‘åÿèã&yyé ^‚œ îr69ô¢<aHšéI´/5mëH©crh*É”w¿"€TŒm4.K¶Æ ´ö¤>ð÷(äv;û~H+9å@&ˆPáÓéÎïTñaLÍ¡;ê$“9ôKnf²òo‰èŸàƒïa…¡{ÔL«/îZÒîÄU0¢WŸl
+Uƒ†­…š[_³pA“€1=GlŠ•ˆ¥»jïð’Âh~]¢I|‹«8H´—g‡K/«Ñæ†ù#BtJsãgÔÌöÄÁÓuŠGIþy\‰B,êàh–ÞªÈ`P;G%
+Ù´BpžÕ¡jÕ-AÜÉOX6Ù‹2û˜ÇAÉ|,b ìFreц ‡£Š9Žê±PêR³ØË¢À×øGɺj£ƒ"A¢ðèˆw@.ÅÏ>žuó±‘dù&»ú/þÿ»ÄýõèO¬˜ÕYÒbÐ.Y÷µAYoÍ~¿.6Ç®wÊ Dõ•ø8Í+ ÕàEqèÒMÖVtyn~•Ï¶Œ:Î^ µ €À«½O8‰nÍ£&n"šÝi*8­”ù|›žÅPŸ%”DjÔNÎâáÄê¡‹Êÿ®”œ–Ê· m6·!žßfr%ƒV`n+ž­–ú:ïmµ}8…˜°ã%<
+YÇDP|,£Bï¼Ì{̓wÖ+KMHˆõý(ñ¯ñfè·lR¤çJtÎCû
+iM}KìGŠ£HöºÎ2¬çZIZn_ú{…ádšoŒ°Ja|¿û'
+ ¼û\Ò˶§ÅbÀ6øÛÆ.žÄŽÛÂ`ÓÎ>kk@#ìy9ÖJ ±¬¯ô—‰Há¼ ¹Æò‚iàÒÊénaæ•|˜'p“kêeùFªõÐÝ÷`§YÏm2z{%ãS¬®?ÁÑÒ&~ u}ÞïGAÒŸ9®n‡=g=Ê¸Ä `AÙ¹a×È
+ÚÕÞ¥%pŸò6ƶ¯YŒŒr¾E•(Ü"M/èý±SŒŽ_àR8§ ‹Q¥H;’ÑΎΰŒÚ•´0§)ãC(ó.üý†>†Ã -,4@’˜ÁEXÑOq}Ï-Eê…£[”Åvdeâ(ú…µ'±2¤\o¨ö5œxgÌ’_mì.‚¶œ»~÷’·g ãLØf^‰54Φç;N(t@XNíŠ}G:(Þ
+63>m]ó'"]. WK^8“·<—)Pô$;سÉ$&«Å›µÕhï>¾yÁÆ?@Ž*úîa±Ï¬d,Èù£•lJë(@‰pVŽ=qn±È3æbà–ZdÂ< ö0IOÐHc±u7]Ð5çjF¬dXW» o#o„£6™ÃXÄ´oq%Y•WÊ| ÇK‘ÆV÷òel™"K²âójðMÌrÒC4N"H¸Ï*KxÐÇ]ƒé¡ÞZÅP‰·žmBúT7‚jÏ>[V@c×F¢$Έüûñšg!žJùzz<#î—eém€!ñl*5ûôW'ŠÁ~cêml£“DŠ–€¢ÓÙ SvûÕ›XáÇZj»*6L™ÆÚæ ð'‰²ž¯‹6ÍÌó!üB†æÕÁ㎻“¤N®DMS/JîŸ9 7mçþd7¦c”Õ—.L¸^É”ÑüOS>Áâ%;t8à\1»mîS˜¢õ°d[Þ!)H,è¯ð{ ¢¸3†$ìN‡S2 ®
+ÞÒ»ô}“ûþîN‚ƒ„f¤J–E~b¡U~6eMâ ”¯´/
+‘Ú¢û°%h‰n0”Åü§YÝMˆÄ²·‡ä4f*@ 8ŠIdò¹eÛò±ñ’ÁЩ¯ò*ÐPF“½ˆ´¦@Þ%’ùRBÀè~£óÐè# {t¢‰7GÈ@“í
+u³
+¥ÕÆŠb}Óu*8Âãi\"Þ£˜Q w5täù_x¢<ÌZi1µ‘F’€¶H’ˆšgöwÀX þ’¯šRþ˜öX6è_¡‘XèµcÉé«ðµµÛñ4„àà£ʸÖ{ªæ×¼KR!žpC½01ÇN8æ!¸[bþG]IUŒµ¢u‘yNƒ¿û(4O7ÁF{ùC\pÃJ¼¯bñ¸a‚C7›è=n˜iÝñÉ‘ÜÁÁÞS4·ôËc_×M/rŒØï¿o6÷«Ô÷¦M³Ñ6 ø¶1Ì¥°Ë-9’éBáøm‚¢hz#ÔKF~.ÀuyQ®2ç"×-ÏG-ê «˜²»pBwÃ" §P!Jý ‡¥Md¦ RÊ–úÐ2hðÑoO€G㙚Ö5ßÁ‰íK€_pøÌ]âHšl&â•8è;1 íg[ŒUA+t]ÜÞ@Ú~Rv£?µ$ÔÎ’*ݼï÷\Ëäˆ:nÅݤ‡ÜBçŸè!Ž´^3Ç\îq€3€7ôLî6ŒƒñÐ SÜ=eºbœ¥äÌùv¶I¯i>¯ª·ÔoÁjTKPõ+q?ÓSœxÃÚˆGF`º›l’áÇq¥6âã*“d%ð~Ì}9/†I°žz‹ánS>w`ÇöϨ*
+&¸“8ý©Y±sÀ…Ç;¨å'uP=Åü) ¨(¾ß{ó¸¦{x«Šw´ A¸ÂÅbƒãeÞ@ˆ #¨0½ m{ÂÀŒé…{ˆÈ)_Â^WŒ•Þ‘DWâ‰w‰[e¼uÆÂ[—¢ö;%^üúNQqÐ¥é&À$¸]eÒ:Ù—<_lñKZ$"¯{¦==^€!öê4"î2›ˆÂ‰0×Ý­"êJªT7—L#p=̈†_˜ûX…ÎØo]œ™´î§"1½³Nmn’öz†—gl>´ðSŸöoM\å\&50ƧPÄ>.“¹T?e@Pß“
+•çFW-åâ@{XsMmÐl\Ñ<‘G4ß;|oXßІ’(ûwtUü—‹Ê7“Oß´–—Ö¡XÌ ,RÿìÐ… EÈ–…¶Ê"$
+ ˆ3(Dx1Æ“h´ÔËÕQ›]æT¦Pº6…Ø°î 0¦¬pÛ¸ï9HšAIÍ‘œFR®ç¨™K%]Iµ—ŽòhÃø'êY†ûåPXlŽMë”tÛ¢¢u´4=Ðy†‰sïÌA£ö¯õS6»¢ªt±bpKâã%1
+[:™’dZÉc^3lgüºN(ê"Ë*9YÐƱÞs¢qËÓõ¬·ä|­IŽ%ÈË»N¾éÀxÛ@ÏnÛp3†ƒ)cÍ´¡uvÚgy5£…¢„ŽŠ9£åäK›¥ÕÍ:°ÑßMÎcéå3‘Éj«Èñ¼1¾kï7Ø„£Å¹¹.]L­Ý¤ßç1êŸ/Þ=ÐÚ€Ê#‚*Ðn€e…¥u‰md7‚
+ø,è*?N6ƒACPþë"™±ˆlq•>¶Äº«ê!Y9'3M·lúR"†ˆìø–m]š¥{¡p)l¶È〮Þª
+¡c½Sq£±1EBgɘÇéÅ4ÅK!JLç#=ѸG
+uó¯{µŒòÄÅ–-êŸä3ƒ?6N†J¯m¢1#Çtã­V£‹ƒ]%;µe“´ÙXHkˆgj¬ÁÉyXqB,86.iô–I£<`œÂ49(ƒ>èúcQ)q…=Y(£gT=Rv}]i§Ÿë[ŠmËåá°ø!¬žø_s˜rtÊ¿ÔÏB{æ|ºC`ÎŽ’éúÄ
+\Ý:ÁÙ¸Å"’µ¢ »àÎÓÎb5å[Ûžã˜G¨ÜØê>š¼ÓY+¶óˆ„î’ë
+/quy¶ÐZ£y^žjs^~¬uJOÃG½§²Ëðó @Â8„dš`@×[è Ô Â+6- &u¥ K›¤!¦ÎÆ,[aP«FTF Tò¦| $µu ,5/Ù¡çÕUûñŽ"e@Õ¨=â«¡éÑ-8QµÐOÒrÁ
+^L.b;½3¼H^aæܱnÆOŽ(@T‹êPˆHÂ86““×wViƒœÚ& ruEHûK‚ïHè³w3Ó…‹TÁ{K+(Ì3UlÚƒ4I¢¿¸Gf©ø‚ë܉†@V‘ØÌ ²56AYtžV¢@8u¤jþ$!ZÈ5$ Äáù„*ä£k|v—!„ç'e¬º‹ÚKn¿ÆMg«Ÿ'É×ÝoÏi<X\ʺd
+Ö.cTp‹[euÇTøûuPü#ù1ý€ø³ÜàÂd9ëäPª ;\¹"KMbIN"')©‰ô¤²ÃdÚýLµ¿qÜ.¥~ñB‚²F›š­ª‘ºÌ†%‚A”°ˆV Ò£sÎs[3¹RáÇJÜF`p Á—ƒç¿óy÷ôkX¡8FLNȧ½ç)üÞ3¹^óCš’‚Æl¯$`’b ©|u®1ZÑ-hyáåÆaŠµ=ÍÙ&2Ñ1§ä<ðGp#†ja‚¾š'QpB˜½lu’ö£þÝ|ï4€çïCW ~ïùv©X'ü šréÖz£µ3ð""­×*²ëñX¯ŠåɬlÌ¢Õ? dVn´‹;üÅ­"éü#Z¶÷’=Äó~qè1ew.PGøÌùp¶Êº%‹7¯^ ¡ü¹Ñ È©êîy
+—Y 2m÷Z”ØDF.ý¬áÈvܵûô"âu±XSº.ÁyÝj6¼¨¦ä×ÚeRM7 03žµ‹ŽqD„$‚Hý„ʃ¸£¡R÷!kŒ M/8®I/ŽÎ³ë¢ä¿Îò­¨¹n
+—½“D$ôý¹×þ_ÿ‚y¥¾Uq¦ËÉÞº™^Êö¼›‹vé¡X kqÒ‹Qdª7óЭtfª˜'*"dA2»^%9ÆLJÀZÛ"vit‡ÇÔ÷k¨—ÕQHä3+ÈIHZ°s\I J›DAм_”ŠMaºqœnÚS!™è÷]j¿<p´”U%„J‚ûiBÃü•\~pxXܱ8×>áòü›ÅêËò°»<ÅGs“ÿiÞÁUTS‹F%« Íyñêß3Pó­×lÛc2€òóÁËç[éÀA†Å6}e]d^Ñð]O¨Á•–aa²CY<ØY„ ±ÐW4¤úÇë_´Mΰ^‘¸Æ‰€w0¬"a]ˆØê„JúA¶ÌÙ˜)˜{CY13±LRDá‰Íé HxR–tœlX­KÜS$L_‹ª! z¼=æ¾\j¸ßLr Úpz[ïnPl*“0V‰N€ Û¢|R?Ð*í‘Qæ0  ¹dQù«.nwK国µɱš(cÓ‰ß×!ȺF³b Ñ ü0¦…¶»¤qX>è²^oN‰´„åFdâðF‡Bbv3"e¬:b˜V‡ST¢wxêdQü¦k¿áiîÊöKä6»
+rú< Ÿž×kí@AdÐü)m
+€…2@—8έÓÜéŸïê"/¯ÏÀõ>Dîf7²€•h Wº:c¥€ ΑäËn'¬÷ÞžÅñ}übh–¤±¥Ÿw ¢ÈyN™û{„û¯_ß].<äíØ’Ê!öÞ…(QâÄûßàÜ8rZ{¸Ú³ Ø­1Ç4ãªÕ’;ÿ%ìÔ-š½ö{ÅžÆÅú¹¤ôȱ!6j&Ä<Æg•¯óqú\nØqÅÌ€d‚ü±®D:îÍb ËHé- :³8WÎá ÓË Ð*Æß*3º>I”Ý×å÷†5ðÊõ“bŸsðR¦ãih^ÖyWjÌ~ôúð¯sgÏV $žv·µ¶ª“^áÇmçp=H#/€Ä‰†È”qć™ZäÚõbte i6Â’©!ƒ–§R³Ò7xìwùê
+î×»žkEáx0Hʈ®©‹@UòÃÁ‚#úîé@æxÎ.£Ü§šo†ƒÍ^7ȸ\î®Ûä$ zO ?Á»äûÞ’yÍùû†« $&ÈÖY÷ÛÉÑ5……”<Ôƒ3îñ€ÿƒ{—F|Òpã1蕯Yzã©P[#ui‰h—p]Е!ׂíh†C ›4CrrX¿dÐeåVQiE|é34‰ÑÑCù+±×Ç
+¨Î›ñÃÄC ÷h952Aë]7îLî
+žq8$vý˜@w—=1cýâm=%Œ÷Á¶—¿ 
+»µÓ=FJ‹‹
+>†(áßo1X!)«–(5{s™èpÁI(O?µÖÄ"C2¤2¼ûÓ«Q£¼¯ =lò(!²`šûJí=sMDãØlî³c ¶Ë«æðt¼[5Ž(’™Ž/vS
+#Ù;šÂ¦Íôw™úàïµ?ØòE4ÇÃ\"¹Š “ñ‚/OØïTá3B|<ÈêÞðïhåèÕ ø¬Èzi¾Ô¥I
+®-VdsÅç‘èClyê/b&O¶²C6`©V­øÚ'ìAå+³</2ú^¥¿zƒ¢²ŒO•5ó(ˆZ˜óÅ øú䘃YX$DÈ!Y~îÒè±ÀâR 6d[!%‹‚Ì©™ÒR_ŠIÄÄY{¤¨Ó¸îˆ- „EU\…ãËÙÉq‡´¬/«Pÿ^ý¥žÌ›*YËx„°ü#jk0±w8çúÉw­ÁcKpìqB Ùl„‰t¦*:í=GÃcpZÐàóœ¿É3¬Ý~³_o AßÙà5c;O%d2Ó´¶àãaŠõÕ#zKEã<‹`v£_q¿0∠€†ÚÍIìdÐâÓÉDº†ìý ]Ê{³LoÀÑK9d2ÎÆ *³XÕÿfôùVf4·*ŒƬI[òºÎ†1%f0‚ ©%¼]‚tV‡HG6¦Ðx¬Oª"ïµÛ‡®k5†uã÷VŠ.‘ÀåDÀ4´î&€À\Wƒ|k<^tTãͬ+´ûÒsÛm‚nçÛ6¢ÐOéi™`9d»¦m‰'âíf]Mgzraöíg4©"mQ bÕ¤BZhÎàSúA&䥆\ªÚÜØI©6‡¢BT±‡H¨æ:líòÅ‘¾E&ŒgsÑl$ËO(+¤×dÉ;–ÁýŠFb‰Ü8GÑ
+ ¿'ô;åË™ìû^?„h‡×èü#.„q¾3]ò¸…ðL—xÖLé^§äÄXÁc¶7Üi;ˆeñÁÅWk/¶ž9R©øìÀr ´»éa²-E™DWZØúà‰1bШ;P†{…Â.šHOxÞ€ø„æ‰4Y•y‹­È»¢µk
+c÷`u_/x¤’Á£Eû“ NEåÙÓ²ZìPÜ +W²[T½/ª^6:5^$F]@éX$ßðæúë¢ÓÇX‚¸K ða•üqpŸKNÀ¨$hÇ,ƒ
+KÇaÝ´ÂfºM´D‹ë¡+ éQì êÀÕ‚$´ÂdÓ‡cpño<fíŒ&;¶‰'ƒl!øÒt¾„H_×"AoD²<!/174ܤ>cÎO© Ôd識&†žD<½dK$d¿æÍ·ˆzxA­ñ¢Þ{VyÍ“nØäë èïâ²Èkžs9eù8'ÚšaŸ2iqîåÜçáƒÞ¶¯X7Ðf¹2Ñ£ÒSŠàIåA·•[ʈíe<>
+Ú.[ñ°Üuc‚¾Ow´Zö
+¹URsÃÃg^,¸P…]Z•=N}L”åѯ¿zêƒÈ´*¨«õ ³†¼3mEt}–¡·
+xõT<ý TX¿âœ fzˆÚ´‚Ñ egêåC{0ft«û#x(äM"&º¢&I¼´•`n/šÀ9ºà“q‡ŒI¨ù kTl¤Á™÷ù!”MË·^ÅrAåºR˜ƒõdvyuáXE¹­çûªˆ£Ä,°¥Fç¼£
+;“¢+D„X
+9ó|?æoŸs·:c͹ÅÔím3ÏWg¬±Õ}ãϹÎÙþ{uæ¾ÿ¿Cûwæ[Î}Ö_ëÌ;Æó}½½Ûnû½ó|·…ÔYm1uV[8Ž­ùÕ÷ëþµµï«õýY[\ÝÆØ^ní¶–çmÆo⮯æ–[¼íÆ{ßì·¾k¼ñÖvûÿï½›ïÌ¿½ß†;g Ça9öÿâ‹ÿÍ~w¼³þýZù·Ûò¼óö™óëÿÏ=wÙ[ou·ysk±Ýýgœ··áõ}oý¿¶zoÝÃ-¤nã|-Ö¡ÅÕm{7öóûñ ­þÛâ|õÍÙûló·o¯ÿß¾wí3×»‡¶ûV{kquûZLõ7Ü–wë¿ÿºç½s¿þsÌ·¶¸:{½åØúý¿Ö×÷moÖv[Îû¿¼ûÞ{hýÏ×f»{Îo̱õ:{þ¯åÙŽ­ßWï{¯Þ}ûß½þw{Ü;·ZãÌuǸ_îwÇ=´ýoÞÿµ½øß¾ñÅ=íûÍœc¿¿-ö:ã›ý·ÝÚn­Å^wìqçS·ÿµ×_cûÃ3Ç—oî|5¾{Œµ¿}s­yÞû[{·ïzgÛûößꜷÖ_ok»·Ö†Þbêì×û~omÇõÿÝB궷¸:û-¦Î†ÖzŽùÍYs}ñ¾×Ûk!uö[\µ×bêöµp¡ýzë›ïý^÷9þýŠ«9ÖDÓ˜+7b
+6®âz¦+
+œ8”C\z~m¯¾©95Y]73ET€§ã›’
+5ª¹œª²[*Τ§mMÇ
+.®ë4¹1 P2›íÀÓ1êØ //\ÿˆuV#9fF¢ÒPFfO×KÄ#n“d¹E柒'Òe˜†Ì-™<JÈBrX2ç'ÑGà“«8MÃ1ûˆÓ€6¤«!#™Qn›þÚ€ ­“á±Ü.ˆBC-t|Ä€Z1ñl˜YJ“:pm@7xeQlzJÔð<ØÉó¼
+%AÞ<ÏS 5</-<oÀw@À"žç
+ÏËéâà¡<<oÀ€|D䂳M6 çÁ.pÅkäˆóLº@¤.kÁRlÊ¢¶y
+Mä; ²BΑ:_¨Aë P1O‚ãDÉ`ÈÆóùÔ†•N6‚)åP“ë|vóØP~ÀXhœnóUfÿ4fÛý=Ï —XÐŽœç‚žç©`*ììt²RIŽÇó<o@ây^¸} TÂó<âyžD6@ß±²<<Ï£1 ¼F áy^Âáyš ¼FF‘¬2'LÅçMe$’-x Ïó ¬Ìö<ϳ1@cfò Ïó>ÏóÊZB׬<ÏóÒ…çyWDf°çyÞ€“§´ýw@$FÏK4 ž×YxxހЈñ±å\'?àeúÍœ(™TH3<kzÞ@%—&šjcI?ñSù¡Ó¤“/—ÛŒP?çñ<±³½HÄQi&¥ÛÔFÕÑ/ÒÜ®Ó7ÛV…ÕL2Ài*I›*š*Χƒrt•»šãLÄ9ø®j®”™"¼ñ½¥'
+ºdy¢p
+95“\É™öé îi:Ǫ’¥îfŠd᪖7“K”\Ëœi ªB
+wrN]§ªcŠ¢9U×m2Òf0–$íLQ³1†ó¨é¸Ž81Q"Í­Ì¡cªcg Ú蹚c¹‘‘±Œ‰«'”¢ì,©Q:®¦HvvV1TŽ*GW}ê:ªªf,câd•‘GW¡¥ÎýFÏÕ栗’Ÿýû¿Ý<e£JÀ83Eã*ÀS–#'{ܹ®çFkßÝ}Í$0’,¯5£Lü¬´±¤Axã¿™ç
+Nè×E¥ßUS‡'ðH$yu~Di`¡*ˆ([¡ƒ^¦„ô¥VÒ´ãbÌìw@” }‰0T¼y“ŠUÍð@—HΗr( Ú„ÔÆS–ÓeÌXëÇ«œ²•žX‘äà•®ÔJš~„ZZüRm<e‰¬¾”C•Â'’ªúÉŒ‰Ì¦ñ<Š‘ü&_NœØm(I&'†™M“6 JwÒ`lòeá„Ñ ‹Ð¿|„æ¥Ü̓ãí”D«úC-—¦èõÜDV3zÉ«-9©É"›y`ü49#RHÖ£ž›D±²¸Ñ¾D@YÓ‘`2¤þ*<°°A°E0²CšÐþt<±r'@­Ï•,Fl:éHN¸.“„†0ò@#Bj:”ìŽ^_†hr&iðf×@®Y@ðtJQGÅÀ=6¡¿˜Š Ó9è|*"PS'™Ô¸1ðX@«…ÿÞŹ/È«Ì :¤&ã ™&® bd”Md@I!vW“†˜äž¦ç\dUÁƒãí“¢\NÃä(M¤ƒÊ&A0On¦èxÖÅTT'i|úJ="ɨèhV-W muÞ:ûÁŒB–q;° â
+€Æ¦ÈÄæÕ†‰LL‘‰ÄX˜H_ؗȱ
+W÷Š4_¥²ÊÈN§ûˆK
+Uœ,F.1ÛiC£s »ä´Ø‰Ì|݆Çl|GÃ}°§Èà.¸V7Þœ«ôRDÔ[×tX·YÉø7‹¨†2à"/ƒH†â ßÑ( ˜$¥€€¬<^šLÓ[icá&([Å$È6ïTo5Cuª‰2εœ¡Ýh¹–Á‰&keZªÂ)ÇÍ5n;Òã…›¥VF0c¨IøL‘NÂUU²ï€`­^–ŠAÒŠ5cÅP¨T%F€åD*s °2ñ-DÙŠ °p»&g!'Ò…ˆ­%ƒõëÁ<!¬
+3…Z&!@TÚE(=<¸ƒ“"Y`J6ʽ\±‚„Ž«/".™éVkU‘S‹!!Ä[ž -5H|þÀ €VÓ|„Vm eºØL¬kQج[e•dT¦@Á’A
+b´µ¶:9(¨•
+ÝRT'Ä!}J‘Iö%€¾"2ð<¸A–Å©ë)ö)¸Ÿo›T>*ƒAããÂAHÆ{x¿ñÆÄ£2- Í ¡†§QG8õÎ,×ZtX 3ZWçRÂN† „œï€ˆÈRt£
+ž½áfïs¨2Ü5™ è4É|M1ÉGÄ Jè “\07b(‰^¾¢›`„4pIˆu,—”’\ò¡Ò‚³Èlâ€å1ƒ\,´,Ð//
+ˆd6”
+Xq­4F³J)¡³ª¨N§òqtå,I‹Êw@t,+’‚ÚF„IŒ(^âC¡¸HE"àÑŸ°¾€ÍCðQcóQ¤œ9)>C†DúÊåÀë ‡äÑPHÀœL ×Äfj‡œé¬ßð…Õ[$PÿˆÏìñ;×/Hg¸~/eŒÃT©WM6Z¸žc6ïIö5P¶§IÁþ8ÐNtƒ¿WÈ-²ÕEVµºpz]@Z:q4£ Xt„³Nôw@Ø“ABûZ‰!g?!6• 6'/<4ϦªóÈ E©wÕ8àÒÂ. —žó‚# ƲáÃ,Tȼ'çÆ.îgÁ5YQtm•åN÷~DÉ!úî 4™vg‚˜5èÔXVZGö#nŒ½)
+²Ð86¦€j˜ìÆ‚6QLòJ9MB9Îd+f~K¥À‰@%&¥9*ÃPI %Vhà`‘Âï€è¨-–1M›Hhj9Ж
+o Éàµ
+¼½` UìÂRˆ,jIcAÃœ‹š†¡*lœ”©øfPàTµQ‘Š(Q‘@‚¢ñP¸ …a¥Ð/”Âr3()>VRœFÅC*£ UªˆóŠHfl…wå«q¨0ˆ‘… £ŠEªJ(xAcM|„È£ÐDªÀ%\d‹*´Dãb ˜§‚%¥æ•¨ˆÖ•X±Ö‚„IP¦Ú4;%N¡W)QŠD MRø$T)'ÑàJ3 ±qI¼LJ%áh øĈ°ß3Ýœ¨ˆ&›XøT0a1œH¢ es$Š #aÚ ŠDNy‰–¬^ƒ¬Á…§ÓkpQp^È2nÞ*ž6¯t™Ñ¼Úšyª+æIöjÁ{•
+^/h½¼ï€°Y`žGr¶¼CŽå´l§M"•Gjbžˆ3PÞ#‚ð`ŸXò49ä)ГÈ{¡_ÈË4 >Þ"ex<˜ãu ”¹fÆ«YXï4º\¼nÀ°xß±ÊdOÁ+ x!ýù^6e¼—Ân÷îÛã¹ì-VEöÆ®^
+,‰·IDÿò£Bº˜ÈȦŠ´ˆáa‰ÖŽ£RÖ1„Õ˪ἀ¯^ÈÍZò²c|2POí¨çæC-N«¡]ÏzR"
+dÁròÄÉ¥”¥øSàñXiÀ§Bþ@A
+n‚ؽ”…\Úzh‡r y”‹¥PS˜ßI\xöyµP ¦Ã# §å; Vª¦£ŠŽÁh²áÜ[ge íõDÙd$õÀ!°?pGÀ³€WQ,r=™Ó40DP¶Rr¬©>n¶eRpE-½<w£Æ@jÌÅá(RX
+kxÀÄL܆—A+•/§ázX‹Åz½<774h šmÐ\ F£@¥Ü›:ÞÔÑNnêhóÉj<BÇ&Ž¶GKB÷W€ÉÙ# PÒƒÔè˜L@Á2 äFw„冿ÈîçДÅ7Z8(‹nt1«å¥K4+b€µ68TË“PhÑÐÚÜŠœÜ
+³z˜Üe½€fÿv‰8Äü!—çÚÎËsQ‰ëâQðD:/Ïõ(€®KÄ£à\^ž[é @*H’ƒuO§ƒàÈ}pŽï€°íSˣРà&.§ðGÜì€óøã
+LÈx
+ž¥O"´uÇØPpÖ‚¤ÕOBMp)`\j¡àزK]Ü¢áòªÍ€Ÿ¹d°Ñe(8¾ ŸÄK+8.äRO‚‘À›”bpR‰7
+e/Ã`“x+›Äsýèyþô²ô<
+,‰#0A¯¦â…òˆe°F55#{NÐ-)\&«‚BìZ®äš 7›¦S[<ðbA²DøB/áIÈ¡MBͪ1WK’ÕXÈ¡¹ZV
+kmE;(ØAÉ)X˜Óºö&n$qQêB]¨+ázy.çôò\NØРYV¤À•¸' t²@—ÎWÒ@.Gýh —ÞÔÑ´ Ô*bª@*Ð%¢R¨Z¹ÌBÈÂÄÑÂÄ„‰£Å¡@ƒxL.Óð9çF3jø‹ü¹ÑÅŒ¤‹É#Ü(Aºô/²:º˜mS65Á¦&`ãÁp¬ Œˆ!á…15‡ÔËR/ë·¶6xY/w% Õp¨¦^Ö´ƒLô™‹àÙ³^NHõ7Ð,òùˆŸ\+!z<Ð%òɨģÀðt^ž‹J\ ÐåQ
+%|¬­}.—F!ˆm«“p  ¬DRŸà „|v¥ Ðö@ …߀
+Ÿ„úzÜR‹˜= 5‘ÂBv‚òOàvö[kb7ŒzßÁ¹]"j“x¹‚Ã[Í`O¥À’x©ŽÄ»ăôJ·KÄS1Ø$^_¤Þjœu *—ðàYÀ¥“±Éé…’É| [ú¢Éw@¬ª/¾ÉÂ>QcŒ„krh6ääckm°«ºZ̲40§Å²40wPRXNa …fWK‡ŠÄA¡Ù˜¸4{­)!†¦„(1%ÄÄ¥5¼^ ËË ÕPJˆ7Jˆ·³¤¡AC] ƃÕbµX-пˆ^žËihÐ,¤¡A³ßáØH"7u´–Èi›LŠrÁŠr9ù¦ž,Ðw@œ,‚‚ä±5‹3ËùV­UkÕz< $òXYÇc!taâhq(Rµ !ËQÏMC±²ìP¾‚„’ýy }X
+0$”t 0™„‚þò`òw@¬(
+&2Ú¹ÑÅœ,Ð%2RÏM„¢Æ†¿È¦ “—²Èœ‹É#è_‰e‘ó®¦áºDD656»v˜Fd1ˆ«ˆôðL5Á#6fVÄ
+­ n¸¬—ȃBk54h¬‘a´0q´ õÜDÄfqSŒþÁƒfŽ˜…œxÿ†C…xA3PFmYø; nÔß´„à„rhö#FÉ%B`Šäã|ž i“KÀÑ!1Y¸‘ƒ‰þíù4àz¿B.hMˆ“‚u´™6é¼<w“$PF%7u´ºD<ê¹x Fˆ`FŘ•Hñ°^ EANV‰‹ñX
+—NSX%› €§E0àÇ™m,1V«óc1î“P]ú•ã
+Oá*‘ð.äÒ…J;%¨R²µtÈg5BQSƒ1³%Dyúœ`³úØɬåÒ§'ðAlÛt’U" ÊÒÀ‰E!ðª'¡ÜGÌBN&Z)‡ž@šæZ–‰m˜Þ4A(ÑEÈ«“°‘—¬Ò‹ªö%!M6Ž,Üp>vdBÏAgåút\³úpë$ó]Lš®T%‰FBeD#ã
+L“g@“?Ó‘=\`Ä‚T˜4.5UfJ*Á†}&}p¼]~ „ÆÌr G-¬B[ ä`T‚U$´+ŠÄⱡMy,°§¹ 1ƘIãa@^"†Ç(©b¦ìk]4}»Bp‰|
+—Ï G°Á¬[E­ ‹®ð-X(—7‰}é-ÒaQ—fªF±JâÒ,)ø šZ b„|(&Kœ*´S cÌHIvÀÀo(ˆ6¿‚`ÿ¢ˆP½•°™þ¦®`×ó>2p" OÁÕIÈèh 7÷-„¬…».Ê©À(mG»ZÏDȇ̆°UotŽš4µfLLÓ…'@âPya@£ÛIáJd倢OBýˆÐ»¨©Ä]×?Ä—§àÌjaœjú©…1³ °\*ÑP ¶ô
+9. KGŽÁ:,—~D…1³0!ã%À?‘[àò"ª/UpÁ²ÿ'Ê0
+5ÁHSµ™Øï€xUyJ-`îâÓ¹˜1m ) …áön4P[:5cf·DÈ.è; ¥Kî•xbÈ]X]ð
+‡@v]†€R Ð>VÁey¤æBÁ3œŠ¨”ó(„IÑA†h
+"òä;ÿ±[ô €s÷±VÙ:,]¦:l¢Míó›é¦¤\,ÃIÛ6©"_¾Æ<F¼ ûNšÃ¾ßx}p"÷ò®ÿ´ÓÅ%ãP͹éegÿLÇØ:èƒOÜ\(‹>* õ~1¡Ä±aa„!ô‚’œ–ò¤)¸ ’¹tÿ.·vpDÖ YäþQ£Ç¾Äš ëŒ*ìÆÑKàÇŠ"<:õt0Ú¶Z’k)VÐlû}z4Dø‰N“
+:cFÕ}2ù}[Kºï†;ñ)y SŽéýÌk©aš²×ÑNI§µPzþ±âsîíEl¾
+Õ% »X4ʉy~µNFõk奱 áòôžse´å"ó|§½‡†=uÝ/ØÔ¦6úé•ðqÆQÓ¬íÎÜÖQ55~
+ºM‘ÆãñÁð?•³2´à¨€0jâœs¬ÿÓÚìÌâ:PÃt¶øÞa{Ý6©§{z³ärDî*|èÈÞIöç]ÆmŠY¶×€4‚:¹Ä÷ßÊÕ¡ÚÔŸêÚz¡{ 29_\8˜;~Ì™¥|×¼ßÑ”sT@*¬Ò„Å×Õ«š1¤ƒ&1ÈpÄ6ukò·ô8ÄT!^”êêP.jŠ=´(YÛ"ª;ÝŠùF9îÔxiAâäjï|âcpE˜|)5B >oΆ†TÉÆú— E#Äø f¶c{VƒAîìšOEW—4üH „¦•˜À¹‹œÁ
+  voÛª&ùAeYûêÍæÉc×oK›¿e~t¯¨;dù\¯dvq¨ôÎ3þýŒlø±cKoòûäÌ‘å`LP©m`\»VøìlfÔ†
+ÅÜMY#™MŒóÆ„_š¡1›ìGeÄ
+Òª§JB_IÁÐÂñŠ[úšµˆžëç©3l\ö> 0;(›Æ¸²Î±ù”æ«!Ǿ–Œ òÛ³qà§Õ¬¼
+`õ@„ù _îD€m€Íò$¼ãâ0TW”KLH‡YQ
+`A7^§àHl0 "Ù÷
+4~`OàüŸðàD©úÑÐËÝššʵK/cÜgŠg¦û2”T¹`­øÎ
+UIæ@ÀÄYªè8µ]¹‹œóW{oðmýÞûio6KºŽá‹ÒWTÍfÞý §›UíkÇœ„hNü„‰¿­‚õ÷íEIaÏe~.˜ ¦¯™pr+×´&a™èÆÊ.æZ˜]çmäø¬Æ‘Å ?÷\gp¥m6ʆ½Þ6E=8õ•` znp¤ÜãSCŠKû‹²´Šº¬ŸU”eE…n|ƒõ#–âÒp…˜ü´$µ~!Ltœ„›<uéÛ”]ì}¾’'7ÚJ_s…¬Ôh™=TE-„¦HÂ`OFE*ÿ¡žø|ÓaË…†”¤oØQ“Nõöù©¬S”¾LÍ\ä€ÀçÛ&É^úÊìˆ3¹/ÝEcé{,0æVmì“¿&N-ÇçK§z_úk^!Ê!&Ñ^'ÄãýôdJËkèà¸!ójÿR×5€Q  i˜gr››Òäb2ufå÷3?[™®tØ
+&—ý¤÷kEæäÔ.ù¾-qaàrd@üd,79JƒañaÞ0E¶(.Á_Gî…ù/DMpwr)8óT·‰;™ãJ[ª˜f—'ÿB’/¥-/±£fO_Äé%C"fGgºaÍt4™VÄZĺ _õÀÕöÈ)X†-i( ‡=¾ ÛØ;ªó¤‡S¢€Ð>Ã*p#‹ ·‹aEÛ¢r¯`†<ål(Õ%Dh]ÈôV:’
+JOÛÝ{”×—¹a‘@Ç{𽺾šî€I2®/*Jm‘ðQ'ôõE«âšIaxצt¡ºrܤ×W|DQ^à‹(gÅõ½ŽÆW¨îÝ$ŒúÊ–åú&OßIÅîTÖÎÌ×’§1ÃÄ}Y.†i
+óÃfv)Èö*ªÛ|û$ÁáÌÐY˜cË«A ÎØÚÝÄE…¼®T>E«rȬ!
+á)þΕppäí¨MÐNl=ÝÌâXó6¿NýbnÀzWÁaa;‘ŠSƒcÁk.ŸÁ}éÌ2måYɦªxë/E^Tx‡8†Kœ ß›‹šr€ìÒÊ€.OºéA*V³X’¸´æ8¯á‘„ŽVó«ü_J@Í X…M»ÁHž Τž¯­ùO>$í˜_¢à˜7æèóÊD<ŽÕºŽ˜)la‚Wf>R·1Þ© Y¸wnýÖõÏ“IØ: Xw« Â0Ø¢9eçÙ¤î/¡XHC"# MßeþÖå-uyBÑfÌckO­ç»_¯ùùØ6.X‚2ðpŸ§ƒ?²$ dðP~0?Ú«±~‡WÏf3×JâÕŸ’ˆú¢:"š5OYúŒ† `ÁéJ²À>«Ê‚ŒZ0Í#0tÌ?X óˆ­0^#òW
+L£¬ƒÿ>L
+¢@K,¾üpcÑÓRƒšÊ_É^ ö6=K £¡…LKR›
+g±»& qiŒ‘çE¼€öpPHV/Ú#Ëf­x&R³g”iù>hVh`Ë£hE0›X„ÄI­}µQG*)jy
+‡q‰Üy—Ä»úž\¹q %¹n}EwŽÇ|R0)L ÎnR¥BmÛ*)³_ªïËÇÁ„·^‡*tt?9XÑϹ\W>Ž…­ÃÿûâUfU$
+wÒIR"‚ʭΙeç9Y&¹ƒŒ³®+ò•³d˜ýb™ïT´×±Æ%(°‹c3|]zˆUâøøÄdoþy!ŠY8_@ðÍÉnÂ…Džm^—‚àÆ&?? GjÊœÎm8êu¨úiá¨ÑÉÃɵÏ.åµá<éZåèN m*™±ö/T8·?µ÷OÏÔ¬IšÎTˆ€UJ
+9‚ü9ålͧ”6¥ÈëSm²¢HÿõL°T7ø€Q§€X²DÑÃ?­€ôÏ9}' ʇXùcÈs‡“t
+:¬­ÚraDÙ
+µD‡$ž™˜Uÿ^¤>×,†¸Wš–ßäo!±ÏRëÇwR7-Æi³¾ÀúôèrlãG\÷¦1ë>¦\ÁET]K€ ¤6ÑÖÆû_{äé÷¿à%ÍgL=6ñp Ag‹˜cfî<¨8ȯ^¾ÝÊÁRë0sEa!PŒd°“y6VzÄØRu-T6<æ"³q1JX)GÊ%:c§K©¶X÷¶,µå‘³V­ÚÂü^°›²x¬g°SQÿ™¿„qìØÇ»š]? oö¢ÈÜ=ª¬ &íxÙ*ü‹7À@þ¶LnÓØ#ÿê=Ì-ÿð&÷’+%}¶À0%âeP`æš ¯V:»nLW Æwƒ¾„ù5x[ƒ=o<\O§½œ!xíô;u]XG]® þÅÀ×ÅA·—Çæ
+Ÿ tñ # ÎC­D}…o70Ӭİeø>¢
+Æ'Û4Dgïžo5ËÈŒKu{0·õã¨Yý'K<n¢†l±=‘ ÙÇü]ÂœÙY< -Š’.ÚÖ‚­P«ª½`ågQuØóH
+Pž-sD²Ã¯fŠÌ€p 6ï LÍ“]5.¦üG…)ÿõ¯‡ÐøZ|DþòN’ƆcíÅö§1–>è«Þ¢O\™bÛ()ma±§–”ÍÀ·3lnù#z"{.¤h –Áï’‘òÑð‰Êv2—¬¹ I;1$,ËÙ;Ý%ÏïëÅØO]3´NñX÷1ß^d ¯ñ«™RôEñ1q~ÀbŒÌ¹7Y©h:NCfcµÎhýUfVI8¶ÿŒåÚ¥[*,o©Ì*—B<ƪ©®ŒR¬Ð½4×¾¹è¸,\y'ðá;’ÚÌÚaæ…KñÞ4¢>G€G3'ɳÈ{MbP¼çiß¹h!x'n²"=Qà B ¤·Éz¦W¡·l,³KÓœ‡Áþ@˱HÒ ¾rÍ!ð„LåW•yÀÇN:¬)…‘üœâD~|÷­—v&äIô*çXˆlë¼»t~zôFžÍêƒò»mä}š9m£‚´¦Ç5Žò™HåvAV +'’üÞ¯ üª££M~ ŸˆrÐ(Ú›h›ZwÌÑœ§É‘ï+èÒ`–¿°œš"45§*QÏè™G ‹\T;c*8!R·ßÜ~ù»Ü…·¿
+±'ŠÙ¨åpú“9P×ÈÁ°>q/›m «:-×¥«E€—E7/ÏHž”㟹C„;ªž„ ǯŸ¾BΆø›8Üd6r”þ¡pR¥„GÞtq8Ï,Ú¾£^MDòâhñšÁHå&LÀgœæÿ߃pÎi
+š¾˜ •sã»GÀÓàá@k2hªñ¤Û'Áb~;ß|à(Új{5|¥ ÒnÚ.9•ÀBØý–'Èüz~ãQÛ$rk|=ø¾éã¿ ×¾ Ö;V)­¯2€h³×C¾ÌFö•1‡ä9–"ŽÒ”xzó„GVìÊ£]å#_Fµº¤ù=T˜Ä-®¥Ex-ýùr
+š´A0WFr`¸Ÿ#i§jëÃ)€ÐYXˆ‹Õ‘……a±Šu±…8¬Ÿ½ ¶6Èß_SŽÚà?$íèå]¬³YÕUû.ò¤æ*à#êzÄ-¸ü>d{)é“ü»Ä*pKéàá}|÷ äÿ-‰*ßX3c®¬¨Kÿ¼yÕŠ†JßÒ1îõÛè?ŽñÛ‡rŠ¥ $*£äs4iÆ<€&±!­­G4ò>E(¸
+O4,sgÔ0¾M4Ì#
+Þ=9& ú_Ûݪ*%„í?¢iùyb ½9Hh^ã ÒÓÓ¢¨ ¤ 3]k^õúl“ 4}!åh|3ë¡€¡WiÏùœ“½«ñÙÏÔÉ›t |ã®›¥ ºjˆÆ¡tw“edô9”¾ëÃÆ*®Ô M¬¨‚t…Ù¶[ðRœàðN=è+*>½Ÿ'Ž}M\6wŽt˜¨ð=æ&Ù¸‘½E2›b0Dˆ7sÛ|¨
+™ ‘„&„¯¨Ð »éMhî@5å3g¹§O7À´_ÆÎÖ¼F ç XˆÁ½6åѲ:°$¶pÓs5Þ~Ê0¨Ð{éÏ€vï‚_³1 ÝI†•1Žöλ#7’ö‡[Hû;»÷a0”xåòWevçÂÅ–×é.x{î"<é#OW-»ó-‰ •|7áì`{*¿`?ˆ‰Únj}èhŸ<·pNÃîï²U2
+Š}Ôã¢Ò_ÓC‰þ
+ÙÝ—%Oø›¡…f:œÏ•¹K¯¯ÿ‹oXž¢”]cúþŽkzŸ…
+ZÜ.»ðíà 67jÏ"
+Xàp±½6.A˜³m‘†*’N%‘¨M…96Ez@-âø³Ãh'èr«hy)Rd>ÑÈ> ÒM(á? ²ac×vò 9"” æÀu¤Ò´†…‡´Êµ2»õimBnÐYðìKµÅDû%):#]î¶õ) .b%·¯ð5Y(©Ÿ^µ'«Fa@u
+ÿ1
+h`8òÐ5 ç¡,™ï,næp¡KMeÛÝ!³qD[´c_âÆ!ù š|6T1ôýÈp6ø6‚žf’3È0uØ3s°
+4Ž‹nÎNBg¾¨² ¡
+䎽ٓ›Œ‚Ï»þhSÌÑ~€7úT 7qjòp¥äøã“iê½¹1•¬-Œ4øŠJ´©ÈÀ_ãTÎrõ²˜žrâHJ¼Ö°ÔvëçJå†Ûof×ß°o)–¬K ™Yšâåx âïd–|Kõ 6°(„ D||VÃÝx8•Î^&Ë©îAtDÚ-­˜D{ÎçÅ¡Åו^SuׄÅÕ •8°;¹YÚ LîÅ)a2¾aÑÉ!z¼¤Ò˜UòEj»æŽóHZ̯¢<¬tVÆM†.ÖÎ×C—P.ˆÝ0þ&8nÜÙÂòßp^ HlÆj\žö‡g¾°–ø] `ÈF º§…DÁÉ™«§Ï0¡X(³/’š8 "6!~Úé4Î # i
+l´×n$w´„çÁ0¼Ô>JUµ8gz7iáuÔt£¬hóŦ«˜ôg0(Š6]™†Y;ƪŽXü"à¬R¯7ª Ò¤¡¯,m‡Tgþ’ŒL²üæT]p˜b±ƒV´aBOù”ïÁF@r0 ÿ·¤•0†Ÿbz¨{â\~ð1†~ñì”6 ò„|ÊrÑ¢„ÀÒä`“ETÁw¸´²¹¢‚ni¥ ùS|èð(HK7—ŠŸeæ’(Ó«ÆÍ©Ú„I¶Eá}£‰^
+Ê‚‹HuXd6Š’ü×5·Ê'GjºÜÄ‹åü¦1‡LdƒEJ>Ê,”&ÊU_ð³ÖÉMX®bŽ‚¥ñ~•90Cƒã¸Þ-=r*ùqFn{ZO$¥ÃS¡âVmðcóN´ƒm¾jz6€Q¥^æ¡R—ìcåçìó´Ì¥K”›¿)
+©5Ù“¤SŠý°+œ3wZ®r¹â룭ŽN*9r/Â`¾é n€èšæðe%)>릃 îa©ì¦€}fÒX‰sûå²[žu=A±}¥Q…©È÷§mXÃ@\ᣗ°´ÎR]¯ Ÿ QÞ w§€$W8²§ÂÁá?P>u7]_C9þ舂O›>!IJ’‰±òŸ †RÿAãÇ5BZ’Œ*û€P#…D·•@ª&ÿ0ꔣÉ3HË:7P5ˆ'4šK€ªh4 ÃÒò÷î·qzH {ÜuÎk{$j…xƒ'O.!±+a€Ò3’?«\Y3RÅèáìÚcùà¹Ö6Ý^ãŒÛÚ„í~÷]Ç‹RDà»xU?¡
+•°ñ¸:Ýš£¹â|ªbI§"c9†JÇUû‘AB!RÈí6†Ûq¿<¯éå8çÐHƒÛ©|å™´©îÒù’æ‡ÞÓ,?ýŽØ¶•ÛÀ‚¥ÔWlÏî…Í^´Št
+fBƒŒcì@‚öϹ;>¯ÿCÌÜ7mÌ~ËJ4³@*¸X<óÖ’&•`¤^M¢VõîU,
+á}&•{çïG6ô²bŽ‚rxéjôz· fÃ2ŸS—© é›1“´§ôÏC¦zržFc©Àg ˜–ˆÇl¾ü@D ¾ÿâ¬T¾>áˆ4,®¾²R†')@‹™±ÛZœöÛ*0Ó Ýç-cßa¸ŒÆä ´™æERg‹fRÔžzóäýŠf@ÚÏ‘c?`ç÷$Á“šªÌ¢Íát{Ý×µË d’–Åä—¤
+R¹ª?`Àl×W~Ù'âxN ~ìs¡ñF´¤Î¢©:ÇE6OÚÙÓ+Nxí€ XMz{,¡À®È#}-P*…sêÃÛÿ”£8
+d³••ïëó²Y¹çÇÞ)ÌòB£.8­ õC«ÊbO©å¶uP"j–ºè½l)ÐHêe¶\Ü
+é,r˜;¹æ´ ÿ}¦• ”X6Ž+ yyEâÓ#¢¯Ø_§<¦úÑö8äÇå"~©Z…•ùèÓÚg CÜ--–€q\‹2›ÊO³âöevHçèké C%Îb^ÿÜõÌâ)NéÖ‡}¬·:3HF¯7Ѹ«…‡£š–
+löAÂ7\¶ˆœÀ^°Ï¢‡Íõ¨|úÉM’¼àk*Êâ¥&Xi
+endstream endobj 15 0 obj <</Length 65536>>stream
+ºòîSS¥ãCdÆ쨪¢´Kk|•kä–ÂÒ’{\jêDõUÄý–0ÛðÞâ¦7­˜,ÓÑFÓ#û¿‹ÍNˆ•Å |òi<; ýZÉ´ÂÝ<£Ê× Wmv[¤QÊ«˜6¥ûÀô.*Bàk–Ãr£¬*x– %›q·ÝÊN}â{ÓE^A úÈóúλiÖAIøB€Žýº-±…ݹÆÝæ†á¤d£6?o³ùn.Ÿi
+ùàö$,óU1¼½s:?Ô‰òµ¢a‹ÓwF÷ ‰½2/z×`ùÁúâHa <¯‰ÈÒné><«£º@mG‹ÄÝìÉ$† ê ÿof'2Å*Çp!äTŽç&d¢5T] ‰¢«§D«ÒèiÀ
+jŒÖàNèi
+|ªÀ-ˆé
+ýN1º•0yY¾r4…êÕwX¡Öû’Ž2àÈ{Wƒ7ˆ)Vj`0L¼~_ –¡wÈm­…5àùµp:—ëH ì&À `“õ²!užÇ¿ £.îW¬Á R±Ú•—ç_Sb¼píåi(4a4Þ‡È/ 
+õÄ‚;@
+ ‹š4S¯B‡<à‰},„5¿ÿ—]àrúù¿\øŸ°x°h á<<pé² ë­ ÷ø‰[±8‹BärõÏ ÐpB‘¿´?[ÖŽú}™­—î:‰ÕßFÄ­Ý=ÿvÑXù!Ý×Øp¢æÈ9¿¹+HƒpÇ‹4 \LW²0&:1ù[d?
+šw+¬÷>ža‘\Ådø#SÇ{ˆ Š”Wq!!©¯çÐ NJ$¢§ôÌ6ÜçïD¸äu1KŘ=‡zñ=xØÙvB„¢Y§†(!ŠqÄõÓ8 <—}ÿ‡¢ÔQŸ²"EÀ0œw¹ ­–Þa5>òÜùÕ¦…œSw´.Ü&YÙ²Šß}Ïÿ)?¤ç°‚Hïæ„Ñ%S
+ôKhLÉ:§­I:jήּDÄ".ÿ¬o¢vJ(()òOõÃcœ/Q;hˆõMþË
+&Љ¨•µ'Æ1`Ó ëÛ”èõqrÓYÁ{€ØK=)Â`±9±â¡“&°V>è?Ò‹‡ÌõRQ©ã™ß"‰IÚ2—Ãt¾Ò Á³UQv£“Ô°Ü$æœ]ÐÉ{B0NvÙNÊ€l þ…Y[©ÊÀØc*`B“°À
+`=%ù êÌq7ó®óuf7¢¨0W©S$µ‚*óá}êzP*Ã7C†R†¢X#©´¸ûñ 7´ÆwÊ’ûÚQ°½4]2~TB@e+Óƒgª•ä˜DxÅÚ°í²{ þÀÀ°L¨Ø“[?Ú¨ÊX÷¡J‚ÎÓ:QÌ(mÔ
+>\%iõïE»_!8Ì©{ÑP‰%Å„R³/.'× ^m.ãL¨ƒßJŸ VéQ‹Z·—²õOa$7Ç¡u¿\EÙ‚-5[ËwnIs¬CÉ)Q½G—~
+ÊÇ}Rú#•Yðò¹.è ¡
+lWÖêŠ)à<‡Hyr%…BòŸêóqtò4Õ’
+Ò6ÛèÑW*–TàÓ$N~
+¡b²’÷:§‰”ò–¶ Šþ„:ˆsýꇭiµ“à%‘ùådH%G¯ÙLŒÝ嬱3¦ÊK¤áƒšljN­é¼||-šÈQ¨ô¦Y0±lNU<NÅÿª°?MØ/PØ|túሠžïÏöÚiô
+Íò(ÃKˆg,¯q³é¤G.è„–Sd3H‰ŠŽ¹ª¨&´!ULÁܯL=}%16oŠ[ÙP€eýKN'vøçÔíiŒr8÷ síK‹À0“RÓå«N|è¾äî:z€ô1²µÆ Cy)Ìn©**'ËõT“ñ¨Íà]âóµ* >‡’YÒºIË%0–®Ep‘ŠzùB Qaú¦êÈ B~™€T84Ú ¦"uB‰êY̨ýѹ !P3Òd¼›Áœò¤ú#ë”Íy;æ(.>Òª´ÜM>v§ÝË='J:ø0ãjÇÏ8»7þéTÆZI
+­tg b1`SÁ“4[©‹ÁâÞ}—>¬{²{¬æ;à–¸P2‡
+ÓõÆsâa¥à”íFVº©¹ÇÌÁÏÐ1%N_(½Ô‡û§á”E² ï )¾H£Ù,~‘ëMVñ¿S91ïe8 •Æa;é1×£»}¶”IŸi˜Ù~Ž‘Ø·/Aîu¥¢ rËðPè,zµdÃЇР†4bF‰KŸ±I&{ÒRiöá·cÿ3q‚jÿb•¬l}Ò$κÏ=xyVÙ«kÝùDIˆ*¡Â
+ž¨fídƤ¶IŠ5‹8cKZuñv†ªÛC,*BI†Œ­mA
+wΉ¦´KIcãMvuÃ
+ØÆl€|3ûÐ \%cëEÔÑgí›ÿ.¡æ·º•0›•qƘRt^
+cסüÔK÷Ôe"Á©ÕÍ„{ÚYÄgô X©Z„AÊvs{HÌ.<بY
+B1
+ºoe©í‰dL)7ÓQņòœ+:|}N~*K0“AnŒERk’JÇý­_[Ê~"KÛÚÔ¶éÎù Hçk°ß \iR8~ ¾\šÄCv8;ˆ¼WؗÙƒÅeâæÖZ-½ÓÏV´"¹Poä- {fFšë1òÚ;È(}(8TòRµÑæ—…-ô•r§ù+¬¿QÚ.R'¢Gv+‰´úéÝ ßåƒQ:Fle‚çbåPù™BXI´K=ÈF–RŇȉԗ’,+¿Í¦Ç5¦nç•}[~›\?Û¢¡ä Pt@›(ÜFXå–ƒcŠ"(qs ¨º}Íl £–\[ÚU×méE¢nÄ'¡þó#òÝí
+€7éÏwc×›*Ñî'ˆ¿Õ >D˜RÆs²k|BxÉ‘©•÷u–_^IÓ‹7dÞC‘¿IõP#F)
+=
+¾Ÿ”½‰Pø JYB‘úÓ=g4˜&Lω¯}}`ŽxŠúZ,ß(#GòX£Ù¬U¢Ü¶H<A¹zïå@ª¶Á8¦œ:ñi0ó~·R»ç*–â­}—ø}Íõš¨D –õ¶‹UT0…BŠ½VvÍ è@<„g«L@
+ÃlÁ«Â´—·ºh8
+>-»põî„õ¤Íé·'Q€•Í9a¶®Íø—Ô¨:”ÍNPàþ÷+âñÑÑmÞ
+çÍ”ÌfdiEO&G#ÛÅëVV´…ÎËE.8‚“Ó°cèþÓ5¾Í9¦}öȸXåÌÙù¬ –’8gž$ƒŽ4šÄõ}q[—²`NòçdˆÞ,:‚­ ±G.X¼€h]JNÂ Ø §Al&AAÍ‚.ß:ß«šBà<Z AÈð$HÝÒpj¤++ «!h7’zz.hÿe¾PöêÐP¸6‡›{T­2 2˜˜Í (aÞkAŽ,ˆ ûŸ>Y„Š4äHC³@(»ÀÂ7l èÃp'G
+Üu^ÏÏ I
+-Eb`ËH[HnÊ ‡¢ãð` 8Æ"D…FBó…$†B HÊ‹hÃ)\F@£d :7²ˆ mX9ùø
+‡:FB L.”ˆHá°0.Ò'¢‚ņ ¨Ä‚‚Œpˆ„¢8MÀ ƒÉh‚g
+:"€8¦QAaŸ F+-6B"à¥fJF)Ña€$4
+d:hX`AÔqx@ãñ€CÂ"ÂbÁŽF8Œ†“@žÁDäsŒã@p*&sI„¼D\„‘ B7Ñqxpé8<„@¼`x84løp N4H4аáéƒh±YqRŒb±Š—
+!ÑxXÁ M˜´ ¨.”ˆf&` pb¡ãð
+8p4ñtÔhÈ´p ¨P wN\`
+_¡ Õ„è¯ 4, …F3¼t6Œ¨XÊq&…«€!’!a&¼@xAy Èä+Ü
+…PŘ›@…B¡
+P
+•"* U ç$ã&¡ª`F‚QD@Ñ°ŠOD…J>&4hp`ÔqxØø¨¸ø8 ˆg…¦?&#B(¤ ’ž€[¨
+ÄðT¼Ì´LH€„¨€ÔL„Ä
+¡°ˆLdˆ$Àð
+"¿Eè
+ ”3ÈáÔÀŒ¨€:0/!TáèœH¿tLø@+
+3lb#T X¹„ ‹Z:©0¡ã
+¬„8@(äÀKÆÉ £`+/+ŸEè8ˆGÿ‡€•§“—Pa言(@Á@ r4*\HØg€
+HXù‡#BÇ?&B•!ET¨•• Œ@Ptxn
+ …M¨€ a-b3Ø
+3ìf$X™”„*‰”„ªd $TŸUܳQª¦Y);ùyrTñBðù•ÌÉ…„<
+XLèøÈbb¢BQX|hh`q"
+N„'Ã
+0¢ A\òŽÃCHU|<
+
+Hà0Ÿð9G&ˆD0å£ 32ñÜ DÇ¿ ¥
+(#ÃÁÐ
+Ž(bd¢ãð€’òa£“–
+˜Q@%âø<ç0@ZÂr 4l¤áåÃF @è2ZÐ"ráQ•´[ÇáaETjd:‡‡ˆy8± 2 ‘¨hœ`ø€@³RÃAQÚ ˆJ>80£“u\<FB0*¢Ž0j£¢BÅ6*d¤ØIKƒ† bìAIÇáÅÃ;>>ªA‚JFub\pf<<#3¡Â$£Êȇ“ @LpL6XЉT*&€Y.Œ:(l€‰OHÍœL0 mP CáR4JD>”ˆTt@>¡¶ ÃA±èH™ ƒ)ˆX
+ /)£šÐ
+¤Äa 8R¬ãð°á8Œ^.>>8"p@ €‘b`¸|`\Ð0„E@a€áFä d:>,ÜÑ"ëÐPÀ"a£ãð ÃIÑ šHDFƒ¯É«áŽ&@¢¢¡Q@AF"UQ”H°X
+¡
+NÇáAi@ ,,D 
+F$*¤ØˇƋˆã
+ªãð€1áJb@¡͈ÄdÉ8HŒPŠG¨:*RN0:ž‡V\6Dj$˜F†&Ô8ŽÃÃŒK£Ðhƒè.% &l,¥plœE„ "Åð°ÊK̆‡b€
+HÀ#¡ j¢#¤°¨¬tPàØ8‹",p$øààȌΠHŒhŠQÇáA&ÂÃeC#ŠÐ*0
+(
+Èlǯ!ÑaùÆ–o Ø ð)$!˜’J
+JD ''ƒ@ÀÊMB\d<<:>@„È‚:"´€H\H‰h¼0 @E"©ÔÈpÀt&F<íaÀ"ò€‚Àˆêx4à(ŒH¦ãð°âä‚<† 0/Ž @XèB&#ŽÉ Š ,£ ‘‘Çš šìM“Œ®ã,”È
+J:0D J'¢‚€ˆ,4 
+. “‘¡1ÁPMpJ ˜×Ȉ‰0#ê‘‘I01¢R(§R4g’RP&L¨ŽÃ$ÅÉçAM0
+M1( fNŠ‹Õ7¢ .´`@P
+®q‡-É
+hC„ 6Dh0€‚€•§Xù†€[(”ÅG„ ¥ÁÁ‡Tl°°ÃÀó2ÓBñ2ÓR-´”ET¨ÍFD…:Š”•–£HYQ€¢
+ˆ…EK
+0ŽÃC˜Ž”FbDl¢ãðP¤ãðÐqxè8<t"$
+¯¯Ï»]ÜïL¸¹<¾u÷ÙN5ÿÛv&Üô-aée¥-nq‘¹ízõ²¯ù³„ï:\lË_j&¼®rZ¼dË[Í#g^ÚZ&|÷b^·u_k—‡økFÌäKøîº÷ó¾}©–sÿ]ÔæRûË0ai­zS‡lÖyXÊXXùëL¸—ÊéžÌµi[Øœn»Ò´¿mÖW2¿¶Qõ” ÿe㳺Ä|§KV´>¼~ÿB=ì6ã²Òþëº0á»j+ïPO9݆oªŠ{ZºIÅëÌÊJ³CN—¬Z˜ŠéLëÔŽÚ˜°ëyGÞ;›á¹MÍ®TõØ6)wÂëÔÇL¨˜Z›®WñŒ¸–›|\·¼üîîíªÛ6Ënó–×f>ÜtWÔc"Ûi¨†œnÃgÅV™P뺒ïvz}ùägŸ®5f½Œïu·¤Éf©{†›jEÔªLìïZY©qgÖ-¿ü6Ô;kÖt´êJ}e]wéϪ[øCíÔ»t©Œ¸›ê¶¿yÉÕªµ{Ül­)¶éþðyé6ûëî×¢^žV!£íb7¿÷Ò)ÖÍÒUõržr¦Þ5¾ï 3ÛëÛÜÞRW•k•YÓÙ·ÖP¯–ì¦x¼ö¿ÚØ©Š¢zÞ{Ÿ¯õ®µz¸hû®Eν,Î+,í>ﶪ-vïr•Væv¾3îÙ²b±ÿVµrñ›Mí*ÙÙqm¢¦ƒ+ŠõÙ+½kU1¹­.¯wÓ­{ÛÔníïNUµ]Ål<,̽™îŸ—l¯ûŽe?\«Íþ‹;¯§ïijUju¿nË˸im;»Ó¾;Ëôp»•[ß¹¿ì´ßëÙžöÓn2φ˜uûçÕþ8õ©R•«ºòøxVe5î4ÓÖ¤†yÓ£-gU³UÝxÙ¸ïòÚsñÍUÉxhZÉ}œªvm¾ÅkdÐýÖ´®ñšüÖõO‘º™]Ì‹jµÊ¨¶»·ÙÍ•VkËÙO¼ŠYli×ùÝ<±± Ín9¹òŒ×Ðît×z»zwÓ>{«Wõ\Z‡meºÏwÓnšS­0™·ruëŠUËÚ]Š•î–¨Ülõ+ÖÔÜJl›gÞ润&ÜßSunwªñåÞñºToÑ´ðžöPs¹4U¿™Ô^g¼¸/¾¾TDÛÞ<bŸÓÙ¬Õz39 ÏéL¹-+ÙÓâf÷ÓÒ¦jïÚÙÕª·Ë2í­ Ón81ËÎ}ÃïWÏýÔ«²òÚûiÓuÿâ·">[ï˜þ´›mdíKåu›f'jªUöµõþËëgVZÚB-«zÝmW¥+nòõ6»ø’·SéÝiÝŒ“¹6õO‡ihœ¹ªªûi6ööwz¯¥ë–ÖæíõûS6Öªû¥E붲í®UíÅå_/^ñù–¶“í|Ç=ÿ˜•¿G,KÛu{é½¼ÚUn¬jîK×ß5ž9­¸Žw·˜Xª­¹g¶ÌÚlxÉœuæ«ÌâÖÛbº^™ö[™ÓJ“æñmKÍØÖÒžI¯ÝxÖÓ]F|é½þ}2fRüo5ëmyZ|µW~7¦_×]ªfæq3é&™ûnɆ}‰–j}çß•).—õª²ué"öµ.ß®VÅÖYˆ¦†jÙËœ´qŸ›7b7›oªU²­ê+U>=´Jm|U½ÉXf•W÷UyÕÏ|®Ö›—åå½õU‰/oóñ·œ¸ÚòyuŒÉnʧ‰oÇ[‰u¯LÅ]]Óßó‰7Ú˜ÇÔþ;<°\Zö–ž^k¥aoëý—G^ÃlÎb³Ã'¾V;ã½5Ö÷nZßýf6Ö½o¢¢&›&·Unâ¬Ü<ëãÞ½Roî¦m¥«ÿæV]éž}ûý^¿ãÄ;Ü<Ö>ÖU³ýV-÷ÆŽ/ÏgW|ÕÙçE-]Ü6n»qz#[Ùõm¦§wÞV®Î´l3]ì»T-í<"š%]MÕ¼Äk3¬î»nT¾®>µÅ}ÒŸ]1ëÙ6éÕå±U©b&-®s¶!Z!fÙ"â]¥%ý¦¶)*g?éfãM3­®•S¯”íú:•›-±´4{õLÏùzæÕoQ1³*Ÿö«¾ôײֻ óîFµº¤¥f†Ø܆›Æ]Y©i«Ê_[ÝnÜœŸÜµµ\…\‹}ü¼ïÙuÒ_"vRÎÚib¾[ûu䤻}ºfœ\j{Ënåæ¾,æTºÈI›ÜéoDmÖóëwÜéßÎï¥ÖÞ^i—eáæž%;ŸêáSÿNŸŒw‘íÉnUڇ׬eV|îÂ4ÜƵ¯éi/ªu"2wójÒ=^žWÓúÖl›·Ë>ëÕ+®ÖzŸâå³+mU¶v^íM“¾ì^ejx[ÕÞæÕ[®zý©›OXø¤µºkz÷¤]œ¯½žÙp±²u9/_¼y˜{ã3Õ¼ú«ò¿”ôŒo-Ÿe¸§gVÍõÔÒãKšZU§ÍȧjϽ»­Tÿâër“vñ^§Î£^=mã9¯Ì´Õ 3)gíÞâãZbáûôlÙp›÷¬­8·0i1sfçˆÅÙ˜ôõ;M¼ÚR|)šÞò&[b¦jîñJY7ÕÉÈwËúîî¬Êó¥]ûñRó©%^kjÃïÚÞX;éY)³‚6 UOõIÑ’«öY¨ µ2ÍÍcâû'B †–™Ük­ô‰«æ:üuߥ!Í
+¿·x6f´ÆÔ§4ÛUüƒö´nûÚ‰Ü×úçÙy±™‘ñ«Ê½ºeüÞÒ\ÕÃ㧪+íãSl~bÆ:®î:fîµ´P Ín‰Af?>/µÌt×r«+·›íÌrѸoœgÃ^m®©/óÊnÛ–ìZˆÍÅl‰Ïj¨kgÅü‹·Ö8‘«åZêl¾Ú¾Vw£•c¡%ÞT·ižÓŽ¸¶Ü‡yÝ܆XŸYªU…Üyu<.ݽڢµ¦š²j·ñmâ¾iâqc—ʼng{·œhš–IsyM··JÓ~º¬xwÿnfac÷ñòêbb¿PÈ»ÉÜ]î­Ô í²ÍÍ®k½Ôgö½úýX¹¹ìJ}˹)+»5˵+7í›ñ¹ª7ñišrc¼ˆ×¬3]ËgÊÖ>eübÊu~-릘r“–á¾0K­‘[ûTóp/qùwénÕ^·ºYrêÞæámõ6§êuJŸé&Å«,Þº;û&=ÎÍC¾Mêð°Ñ‹ßݳ;+M¬^ý&Ý´éíwÌìÖÃÕ#¾õ¬,ËR×ÊŽó˜{/µñÚV‚6ÿ+îRd«—Þv7Ϻ÷³r³÷nk³ºÛ®hVZ¥›œ©ÕõµÜEÐö_•ßÔ³?mi6Zõ=ó@›§î5÷êpóZ‡][Xy•z ûëX ¯Õ4³î_»©u¯úºyƒ¿®Ô5NNÄã§ÎTë;~u®%7¦²|º,»þ¾´SYâ¯tÙÔ¬~µt½Yc-¦ºÃËe"–=᩾Œ‘‹yQmj[½††õÛ¸>kª6¬½3ÜãLº.ÝÚˆ¦Ç†Öú+³f–•vžÖ©Þ%üuµ-×jéj®nÖ¯Œš÷eÎ+ëÎÂt+;˲­¬Þð–æ1}“Æ}l¼µã¶óbkŸÜ›Œ·I¿á¼¡Y­Zkï±î»«j—zÜ]I¿ù>;Tæ®ÊäßZ5Ÿ^ªëõ¦lUµÛ-]q¢éíÍ^ÙÊ•ÛgeÌ™|Œ¶ùʬe­µ2+æVÚ¥}ž¯{¶#òñ?ãíÖj÷T;Õùuä7ò=•á¶.Ÿ›-Ê^=® ¢'¨„ïeâ`Т¨dŠF
+ã1©d<¡-΀û†0†AƒP Ä0ABÆ ÆQªˆ
+i<ÏOë·‰H@­£(©à4 ©ØHÌgÇk c0 G«C˜¶hâÞruÞXšj‡V
+ÎLe®±Pþ³ùå$Ï…¸P+ÎÄ9"`ÓÅÑ”ÞáärÙ°3»ÛXû1ì¥ÿÃû­‰>˜'o$D2JŸîççýt`š:p~ο]2 3‘vÂÅ·—£•œÆVí°/—ý@3$,+JÔ„Õñ)Ô@5éÏ!ð'niuGpxÈŒ#Ç“ÃO–̆•q,D¨óÿ5'
+Õ–rÇ»¥ã÷‡l˜"îÐ3
+Ô˜³þ %[KµaCbyû–9ñmë Õ&YFY=83Y-OúXu÷`ýÐ}¤{6:hÖÑ
+5¶& å1ºhñÌ–ÁÂo»Božö—ÏÄÀP¶–æH¢28¥”J ø’„¸¿¡J1HeéÍ:²c®³ÇÑfÌ Â"YH}™‰mfÔ³ÒÏehú¬Üè\ªÕÉ`ÄsáÆR$™°jÅyž£8 »Â¨Bφj›ÁdÍÿrû_OI®$ûv[¬W_u89´¨×`¾*—n˜
+Ýlžôw mdY0âe!N©„B27÷J=…ˆü£üCº<Z8Lh¾N炵ü$)ã@ úmE÷íØD[‚. ®€îïå L3@hö§;ˆxJkYõ®þ®'ñg/{*a…/9ç‡Ûcˆd# … þK€,ïͯ
+0nû;QZPS/AÞ¸ÂGØèþfÕևÀ×ÌC•øã¹Þêlæ;à‘|ºžB³kì8·l_ž5u[,¢ÈÇð}€-Y| &L\4ÒÁÝ¥,ãÐéÊ„¡J€eOê*-ŠIR³6Bð»ä˜¬(u®zÄv¥ç`±‡\U|>A!Z´ø¼{ç8œ8ý;ßSŸŠm]·ÀÁã^
+0ý90)
+ºRÞúIè2¥í™æÅvm pZÔ€ô»õÓ>WLº¤U$åíØ Äþ‚}¤
+?‚Z³ÐÓt2Û{Ñ5Ú¢¢¸ #j»ŒccÅETÅ?Õ÷;¼„÷¶¿)
+_-Éξ=FVz½! !Rd!/'ÕÐ"
+G'aYïþa×Kt’§ü 3ðËf…ág‡ØÄp}ä {ä ªb´â•o °&š^Ô6i a|bEÅ›ØÝ"¸.ð“áÖ_`á¦ðJÕQé8{ŒÉqÔxÛj8ŽÈˆÅiçž]!(>Á£./mÏ–vË-&yºæ
+ÃfÕ?ÙTt–ÛlVº=)Úid@ÈW*èÕ~XÖiÉr¤§£Œ}»ðñSËŠMEžÃIXAS<MDZ Èœ¹Ï=+E|u1H"¼ÖʃèÉÁ7¯÷¿9áF èÙÍtòaû„@ÉÁº¿ÃÖ¡O+Œþ&1)]¡×ç0ijöpuQ€vÔ`rl;…ä ¯6!aŸ0±k1PFÈÉ`‘D
+l‰ [SB*å}<çâ°!W–ã2ÿx'œ+†ä³,®qW¤ÈºÁä|+  ‚^F5s—l›ƒzèjñ¦¹IkÕŸû C‚mdЦ,…s ¾_¾;¦¼&!‰ð¾*gÀÈ^ý®°Hßâ]1jÎ'Šv90}@D<Š1±Ôäö¶ª­ìÚÕ
+\à·p­® Œ\“.^ƒ‰ƒ7/Mt%‹ ëÀ‚Òa9$tÌø—¡—û½7N7v!Ö¹;x‚›åÿ—Bë•œ¸ø
+ÓbO^æóãøz[¿£f–¸7ÂQBôÎC²ß‘ah1*ªw›õ»êÑ8ìo•qÞ,žÆm;Ê6a.{à2tTâÓ“äÚÆÐAé=ÙŽ€ÍF [ËËjÊkó”ø¿”U㢮ržù*zk¶U¤S®g–\¡›Ø¥4 R)¶ç-‰
+«ãÁкóó¸+þñÓÒsßbHFgÞÖ°“ÂÀ)â¬'1ÿîCH‹e£ìóy§ªÏüã’4d̲†hñå°/ø†IáD»–, xüJC–Jü`0Õ]Ó–O„nˆ”q&§^ ¨<;¼u’ó|‘;Á$|2ÎÙÂ`×¼Hn£}çcƒÿ}iîÖ©˜·—é+×$rs†„Ôi­‡
+uø&¿Ä¦ÄrrØ`½¯ |¿„~—»öèödᘃ0‹/.¹~=G*(§ „\q‰L±èw1„€u‡ ê6þ­J:µÏê3hML†È›T3 æÌFl×+–Omb¤ŽÍÔLESoI¡Ý|@pÌŒèÇÛä)Ê›Œ‚¼TGúOmÿ.HÓUW†¯Þ„Ê
+¬µk¿ÎÉè¯6!;Ê4Û9ÅšÖx&¥Ü·-gõBô;.›ˆï'žóÝ„²Uœ‚Q¤ñéYWVî@-ý6ÆÖ¨Ùë.wE¤Â¼.ñhí¹®œaÿ´‡w¤©»¬'½#W ‰`ÜÊÉr¹5Èé¢ ôäI,©Ã[‹‰¯Â'–}1` ®Sìe —½ÒApÐö³ÎJ+" P7#d„RIö0A­ÿvŽ]ÇžÕ[BAGí…¶ó–‡ÿ‰o l
+lËPÊåðÕĘ/ƼÄOî‹kOa{ž¥²
+æe˜¬<§^™yªŠÆë (ÈcBÁZX w™¿|QÂúRÂ
+É(†Á»Ã-ßóM¢ç_¡»Ì’€ÿæ?´vå³¥Ÿ9ç=ŠÏËq¶Û³ŽœAtøÄsíPÚí‘`ÙöGJnŠø²ùbåo¦Ý5‘žmOÃ+â[èžV–._R¼E#.…
+ !×/¶F
+Ù‰,½íBûu>;á˜s 
+
+ÎÃÏúØõ¹°vÂO<]#¦©ÜDœÀCÇzÙ©LÑ•Àë"5gÿ}Ÿq8.Æ0¡W]yúÔ hÔv0ú1ÓàÏehy’5oÿ[e=[ä’Ìá*ŒŽ ò;ü©7ê,ÇžÇÀV.XàÄYÍv.ȶ:µŒâ-œƒ¤%dÐBz_[±‘u-òÞçJ˜4¹ŒgÕ€%ø.©7•ÐÇVa›ŽjEÍì?sû~
+éhñÁ2Ça$[÷lûŽ­ê¹™2¾ú™ ût
+éH`’Òm'?æ+Û Šo- úÃÌ£@)ü‡–3ß) ¯m ±= ±6lÇ
+\¸úcªûº‚Õ9½æ‚)eVî}Ø·œUHtaZ¸š®÷kXBÌTÐÒDhÔ©C Eúb†p¥îÂÃèZû dœ—!Œžµ€"©£n»Þ°u¯J¨®ÅL[–pg@c%ë´‰x:„n=rL·z QÌjë^#ú`Ž½«e÷.ºÒ@xì-Áç@/õx¨„.†ð‡ 0eÜÕ2hˆÈ®¤t¤0ç íÑÁè˜1V9}FÈI.ŠÁ ±d°©(Õì6ïØîP­ZvS….¹ˆ(<'‘ \/”×£À,W‰DÕž YÍÌ#µ
+ê+¤äŒíù;äÄ Ž4EQ8ê» &Ês¶MÛX ^O¸‚`%Æ ¡ƒ%4︼^Q%¿&Ëüáªzc¯ë ŽI8ºúV>ýö*®å‰…¹D”! L`n-LìX‘OÂW³ãu×JÑU콪H^*‚úˉ` ´õ9±4yA
+=W:Ê:Ã>4oƬô qˆû…Lã
+ûYlâÃU¿ 0JFÃóƒ§jÂ6»qUÓ í@Ã8HœHgwä—ò+mòAµi€ÆjÆÈ”„òzøC~Qgœw×eÙÖy!µf´ ˜÷JùæÛ. ‹k‰â[€ Hƒ+ïîW@-‡îF˜à(hY¢ˆœƒƒÃK ÁÏV"5´ÎiY~_Ô5‰…XŠ2½qm“Í”("öát‚‹¨øv#ÎÈÌsDù ãÚáJŽhåÇ©<3:D Æ{K¼¦ôÆý?£Ý7£Jý ãQ…s
+â?'4Õ<þ]˜ÃÀÓs‘VÀÀçíý~n<"bÕwrúõù2AU×?Àl:  Ó¬'5³¬yþTÈ@q1Ló|4!ƒ×’¢X˘làjªt:ä ¹ä&„A*'hZ/õX³‡ÜS_NŠò­!vÔù4œæû?KÑ
+)Óå—§e)çºUYÍ‚¼`ïÙœ†5
+´C;)–iXs˜/CÂX×éØ.º‚,E½K=TLþÒ 
+›îÔ$Í«sfJq1}̪þ€ypŸîv¡š®õxË4.Íp¸¨°o_~†q"¥ú|ØÈ Ì;üCÙEûX˜åb
+Norü©[uøÔF„AX›Z 1éªNÎ` `ªXàÿú¼ýú;×+°Ü<ðƒ˜Â÷ŃMÄ#ÊÕš&Ã
+z®½ú0 σÚrÒ5€…¦Ç·Ê2ï,|Ü „Üä?g%•
+F_ld–²¯Jt7H̘ ¶\8F‚…³øÕÞÜ<“5pm8(mÏæïòÚK§âZ*\µÜ ¨ÙׇŽh[Qz£Ô#ç!l4tþI[…¿]««vÏ:¦ ¸äu Q
+Æ…'ŠîwtõÐPN‘€û¾ò¥^¹†³0«3fо"ØÞØvß{–qää¡D‚Ï"
+†›3ü€têøhIòN^„%Ûé¶ABÄ
+J<®ÊL%íT³-Šiáù̦2Ü3yžõ½R›¬›5hãÌxêlGÀ ‡²Ñ0"©hò°L¡Òm
+Ù JäVæg¾ÕÀÈ®èŽÔøÙ¹xh¢c¼©Àá¿àd‚|×Þº›>š½SU@L;Õ‚1¡âÀ„½ð­»b½ÕM--n*ýj†÷Íñ
+ÊD¨zz™tÿólµ0Oâ€"XiBèbFJËéÃó¥S/Rhž‘
+!!ûÖ
+±Å‚½Ì‚§3äf\NÆ¡üÓ™fQ@Ú€Ò8Æh‚ž
+žøý‡ãç©C¨!~ÑhŒ,c4^àÒzòý‡ÃÓoC†´·°!ÑÉ’á!Çúë,CRÐ‰Ý sµ”ÙŒ¸‘ùFñaª†›ÄȺG§VÆN}jÅ„ô{ å#ªÛHÆ,Íée‘Ü ÐôXnMìšÓ¢øzé_Ú7JÈÏ# r¦>[d~Jå]) Árl(á-pGL\kV¯5´¦pð+x·©ã<¶ÐQo¦Ï;ƒæÆŽå®~1ïbBÒoÒ®ƒunA‚ß4¸„z0¡úšªºÆGH~72ÄF¿ä»™a[B¯D*ü«v•%·¥„ko­Zäà¼ÃžTe ÅÊ/½×‚ìCú-#åÄi~r÷™A€Hm3m'¿ÀÚ]pÄ`pËVQ`N;-6 ¼I]fþ”#ù[JÛyŸ?¦9:½\ØcK·šX9—º™%­YE3ì®ã*ôÄ8…ô¤¼ôWãç1üTç¼û/œ32àÿ­ò/Ú‰qvDTUp›i“//_&g¼’=6¿e‹ÙfLvx:ÍÃíšÊ}–¤)î*ìNoÇß·O]yu© M8BàGÕD‡;B]’90ùÅ¿j¢¤§D t¡¹^çn4˜¬F:ýÛü
+ÓÆl’L¯UáÈ<ûþ·Tû¶#Ù)}ýæ¯Ï M*tÁ‚ô£>õ¼a_PÈ| ÃR‡™³*gÑV€$±¬7§›²GVk‡N$Iû±‘ÚmÚKêò-°ß±"zøKéׄ•§$ç©‹%Y€¼Qo† aä¾r±uÕff¦çTmÁ`)‘—ü| êY¯>v¶bg\¼À_Y›ØV6ØF—Y+ÏXjEG±¿âƒÎîÎæZkv¡
+¢[5ØCvCO§æn™—˜ƒM|¼Þq#¤àÒH‡}7’)Tó×ý&È•±L}ʼn¢uÂqþ—FS£Ëš¼¹ðò{Š²åg’%,ì¾0|í3‰@—‡­oÓ@9ZÜœ¡Uû+yã8ÏëÙ¨?ó’A¥úýÒ‹ñ·À AªÞÈ4±®Ò¦r\Ñö88{ÆÿŒ¸EZ$,²óŽ‹,Ô9¯7ˆ(X 3*U3ÄÕμ‰IÇÿJlÒô|k¥[\G23/!ø¨SÒþɆT 8iÏŒ@ÆØ®>Ƕ¤3<ëbóÄ)Ôo¨‹"²‚¶7¬)î'RwÎ
+|tôƒ^IÕg¥‹À¯THU#û•À)VsVÍÙŒ‚¿ ñg,KS6ì ¶…˜ eÅ ]<YôÒOG¥ †¹$n–O–š³73‘ÈÊœ„埞GHt¯Z¬£)Tù_^›»‰T.fþá.K5È:âåÆñÝ…;@ù<ÖÀ2JJží ZE
+EéÚåZpÚ˜]-u y­M”[FQ´MrKŽ«âF>CÜ”‡*1]"M– Gœ/^j•¤àºÈ¾äá݈n·¤mcVæbOª`~õ¾®
+àÔ™2.9_ßõ:ºb•cZ€†‡^‡© ƒÚí†Ó‘R­ã8Z°S|7y`”q v
+“AŒl9ç'G³,¥K0èŽÚ-ž‡ÚȪ D¯µ¶(‘c߀$XÍf,ã‚
+<ù]Mžv¸\4‡Q¶” K"\Ò=  jè€u¡ogñT4–·žUñ°y=-Žáט4Ígó-úÀO²9Ú¯ÉVêS›t,ŠjR‘±AÇ,<‘ªÒ¶(Óò“ZBt9e&ÃÒ$Т'¬(8p¾±¤¹¥@›‘ÙnBò:ymÞS“- …T•­ÀXÕRŒøؤt}•L™ Ì“‡ï…o%
+%¼jzfÀà’=Ö\¬L !4i×2t{dà ˆÑÓ*³
+æÊFbS꤅àägcªÚͬ/cµ$K‘«ÎjùAÜ'Œ®4È&ÿíš¡Ê«È”Æ8y2ˆ ¿BѼÂÔòôÈ“5÷Ü“¸ä±hI{«`¿Þˆ¡rbXL¬†ZÐþ|×#C Z+07ên ñþ±äÀ¾CfW
+s2a^_Œ}ha©GÑN]»¡ÂŸ…æ‹•Z`•§;_ðÜ A©œä°áî2ÅôPð8„ÕoêÊH½óÅ £! + <ýU†g…+­Q¥ŽÍÐVYç’®Ë
+TvÃñ^8N’§r§! xD3wâÖ—4tËIöcZ-Åÿ¢}Q:¥¬:KÈ®ðÆVÏG5ÑIݺ2#Æ)f!Ê…í»CÚ#ù|¯{ ËÈiÿî.4£ËÄá¿"Dшh;í½ƒ>uV³À”s˜ŽJP¼ ÄŒ>³˜ï®ô1-6–SkÅÖ‰²£Ä`Ô!ÏR³R`+IU‘7»ÅDE·}E&±^ü‰á¢«)2ƒ@+ÈPÓ#§ì‘ /¡Ô)v¯ÐMñɺNRh™a8e’̹ë%0-›|…uÜ´mÑ}©CÀ~¡Çïdõ£ªÇª~›kéYÙ¢>Õ2 äÌFBx‘ÌÖ£TIŒ³§h¨ƒ—¢‘@Ãvc­DRxQ%5O["™b&FK{Ÿ;`wÙ¡ÉMqeÙý›-ìÃ0£3Ê–®š´EÀÅIöF9Ûzl\……‚ÉkFÒÒG\À¸+•™2xwÈº× >ÐŒ¶ Ijãä€ÓîÂd|‹
+æö7õ°^†v2›¼e +ÔïNÇdº,œÊ&.SÐÃœ^¾ê9€µÙU&Œ ÷[TÒm‹pWÍJl%]QÄq ÀŽ
+¹Ì T®ì}
+%&Á!'\ûˆ¨€X18†©Cc¢#^baSÀºù¥ž#FîÎÓ£m9É\_Ã(¾´ÄœüM²™X&ê²`3¿¼Å[Z´èPœÆsÞQwœ—–C®ÓŽ=§_5Y=–Þƒ6VB«Hèðôgù–ÁÓ1o
+GèÆ ²’¸Š!9R£Ûhß{$o:ÆZEQÈÊmóCYk®ˆ+çà‡¬­£ÜÓy8W¼Û4Ñî±pÆH€÷3hzÔ
+ÿ™¦™¹{SV‰ž¬r¦ÑX.râïçe$§áó(ÉØGËêæ<e¤à^"ŠúñÿÀ²3_p‡xo”¿Òâi®²÷Ø;Î’¼\Ž³Ð¬ƒïãt½ˆÌ
+çþ¬¬“mÃÖbëÀ¹- ž±ÍÂ^™`Y¿bl7±
+]Tºåxz2Í®º÷Of0'¿‹¢'r£Ø!/~Æ{\Q`Є¹(IÕ^ 40!¤HŒCnž o0Ìi@At±¥L ú„?‡»ëödÑA)ñåˆÇŸZ¯Ö¬ÁK “mTJ"À¦ÈPÖ±òªKäðmÄó>Qñ†x:üLèƒÉÒ9KÁÙ캞²nöú<êÌ:ˆÈ] u-#ç(à€”wȤ$UìöÖiœê@ZÌÇ­AÀª
+°$rdSO ÆL Â5H¥VÌ1ðsíUü ¢Ü˜®˜‹NÛI 魯»€¹‡¡–
+ÄŠ¡L9žÛ›Þ§ê[T…C«{­õ
+M9R$«¬ô¯¡
+óÑ”ºµLsÍŒ2{øjât êqSý!Ï Hȳ”bsv‚Fž‚åð´¥[/ª‡[.0÷·^Üï_'³TÉY÷&¡ô°( DCu·OdGÖ¢¡ 
+e‘/m1£[ÚËÞ}-’¤¯hhGPx„"ôçL¤\¼¬*µA·.<ì w¾²Ú¬ =–3p¼.ø/õ-à® ÕÍé¬HŽ…¨œR6Ö+õ^#ÏKÀ G£=ÿW°CéÆ-èâPt†r’ðìÉ,g÷„)½sÎã#ÎG5™3VŒs\þ:ü?3\N¼Sï™m‡#µžTæN!#ÞgƳ¨×Nœ!9ág=‘@jõ/—Zûèù$ñ­_$ô¹²«²Ý«ÅÎ&z@ZýÕ*;˜Üíþ›§»ºÛÄ:2Z^f\€G¨)-}œöztØI_á'žðùÛ¥¸¢²Ð‡Cµ)Qºú>+=¢˜íA™j;€èÌ7µ' 5‘{9ƒfŽâýªmȱ« ¶á©0iÆó$»ÕÀÞö-ٓ勯Hº–ÛúÞ­ ‡ÏFìGš*ZûŸíµwmësõb8vP º•K%~2^SÒ«\ Š¯¯IA›„ Q2X—*„…V*%Ì¡M9¸Ž_¾oôãèŽEdžYªú ã
++‹ {pÂ)Ô:ô¶s
+@ÄÌRb"×IÞŠ¿Û$owdÎõ¡ÍÃoþ@a”m·¥n…ADÉ |¤QÃVÂÉ÷Ðm—” f$']Q¹ó ½QÛ襖Õø6ÖÏB1äVªn86ö1lñ— KʯÖuÙË!@ˆÞųy³µäûÆA°ÚcÌ ß;»Õ9˜¤‚–Ž¢:&Á-;j;Z.9àÀÍX:ü,Šb{ˆpGI Þã ³½øáËÚ{æŸ!ø)´ZˆÜsIr3Äþ‘™þhT—Æ©«JŸÝÇm¹,üZ¾…m. Z\œ•óË>Žx¥.}¢f­Ô›HKPdH#÷HÚû}ƒ;áÚÑEü”RA•W)¸2%m)M9s[ø6F” Êþœøûù–/þV|ùËc8&àƒÙµëoSö+¡x‰á®Ü·æè±0Ø’úF•1o0÷tÀ9Ã1 ᪌•‡Ou
+~æÙö_‹]÷-
+Œ%Ê^¦ˆu|m2@­Ÿ$+YÀ1Ë-™EŸlá&˜æÖë¢nC66h ·ÍƒRY>ºÂDKÞôóM`5og¨aÐÜcìæ5BŽ¤có`áÃ&ÁkVÁˆ ¤ñÛI1謽šAf›'{ÎßóAKG½ d²ñ&:oõó¡ÒÏl¦%ȽàóZ²AÝ,RÇo²ô]BËZ‰’ K“MˆÉТʯ €* ~£+КXøÀ˨1Px»«%Ö5æO™3{‹›˜L€{u²w`L6+ì´
+Bv·×ï/nk0‰‘X/j\3FNÇi°f°þæuJðïO–ã^šÖr §¡Õ–œÌ€{dÍp½¡mA®ÀöÛ¹œÒvW;õrä,‰ÝauUr­—
+ëÌ/u¤½AðŽÎäLE„Èé¼2åC e¤/Úš(¼€—‡£Ä.*$Æe2>v¹ðÊC)§
+¨MÎÁâ×…Â%ê¹°!þx*Hü€9¿^PÏwuÈ|<S4M^¾¬ÀÅ¢I7ô†‹„í}´e¼OÈ>¬0pæ»â$ðOIlj;¢ŠÊý·¤ÔqÍà×B
+زí
+( ] ëXn0JO@¶iŒ"mÁb-®¨7WsjÆ™wp‡®cM\€Ÿ ~"¦¥¡G{]¶³"Æù¡gåÐŒ?ÛP”:¨u #½Xþ ´ö4•“8W&E¿¶Myþáçé|¬Oƒ»èj¥@aòjýîu¬Ò×›nghßôÝJqÓ»T_çZÁSÿ™ãÅ[v¦ŒVŽ ÂÎÍHÆ Íz¥, Pjàw
+$9;€²W“Ä{ÖEøÍ$#_“DA~Y MqM8ëЕ…ÚÓs+j~I ¹bÀ¶ôQï:zÓ›D¹4ËžìG)gÇÚbãLê ¹’ÏåÓR’Ë¡$¹á…èrí1mT"yó<ÚÅ¥3Â5ŽX¨ß’(·’:à¹^jƹÐK—}œ²<l'û(Y³r"H€¡>¤Ý“ì ÕepÄ.é#CT›èÄ"qOw}uãŒ_"FÎÅäuÙVJ#áZäK …DóCB]÷€Íµþ±èx,Ç ¨0®9H¶,’y‘þÑ5•DpÜÒßq$Jå"þf)jö‘»³Rô,è%·
+VNÈu¡å[ Ú>róKf0<:sUv¡X
+Žv¸”¹&Ї›•¥˜4€/Kê¡ÓWýJׄeIÛëF¬«=qm0wœ“ó«â…¦F8Z»ä­È¬<G8}&¥"áÈKE–bˆ®,Zø­=e &V6äU?j¯q]¸5
+ƒâÄtß@¦è¢–xMsØ€ì°$Ê´§„³Ø&iÑçœÃR\š‚ÜBŒ›ô.½§H.íDàÆE>XìE½ êëôè"B)C<£˜Môi¢rXG>iˆÌû€5J¼5ÿÁ¤÷µæð#cÅ½Ç f¸òý&]Þ¬î¾Ñ’ÐË/ê1!8i.>5:‚m`ðŒex8ãvœXàqýËòÉè–%—ÝD(]ëˆy"uázzâgÈj›wM0(÷qÌÅá±}eXÊß²”PË*£‘bÛ¡ á
+r.ÁDÏ«r'ãoÌUñ¦ÈP˯ n¡²_t±»}XºPZ pÖ¥«]Àc(ÔÂ<éoiÑRÍDW"@ù¨ZkQƒÏX®¨ÛÆ]ë7¾Å¦q_8Å2g¾^J›øŽ5(¹ ›¡W÷QÄÀ)nÚ–”Zγ#…Û˜ËÙ/Ú·a˜$?³uQŸ=úÑÀãœã¬kK£»€¸zã³Hð†‘òþ%lníΣOùÄ%b"NÅÿÁO}BàÜW» ÔĽùèX
+˜Uå¹õX5ÉL:Dèz¿öD|w75KŽ"8œ˜ÄðIw.\Nx7KLiæCfâ?ÚŸHTõ6Ö‡™@z[þ×Ëb<eë¾Ü…(?=å§)àX %dló”b:ݨ!'Y;Y$Àf*œ5ù·6;G6Nñ8mUÐ=Èc¡ÆÇfÒqñWíKQcaKÄkú6?¶™þ*%qƒ˜´8)K®ééÂÙH|Ð în8L+tº<Û¨­0š,PÜÔ£ £!€8öi×Úµ‰dæ‚©o— Ž9ÉûœÿOÆ [V²Dì
+ :…Š¹úb¬©L mn}¯ˆÞÌ»mKЈ„Ž«šéLF²åÍî;F ¢zC¡ÎÇ`Ý´ýÜ·ÜÏa=TxëÅÄü(ýi€†}|Ì+õ=o ;òc–\ÞÞ纽+?œÑŠÏ`'¹˜óÍ‘~@3Ä{Þß8ë*.Aù¯°ëEòU !Ÿ“/›4«ìâÔ­Ìö&•žÅô©ù±`í'^(ôŸ@i8¡Hè•CN5çQð”‡ì\/|¾ìôó‹pC?l˜¿Ñó õ ŠëÈ?þìñ®Bå¿?zJ¶þÕä5!=Óâô ‘C[+Çÿ°|°ªðúÀÄ&fè¬bNè&Öß+!ɲ,áŒx6´WÝžG%(²TåícÅãwE¬vyÂñ«\:û$sNÎ×?ØÁMಡ§+Š|¡¢ÂײJªÿ‘Cå aUÃQvwTþÌeò?”âôùp}5Ô–/vž‡² ·ø@„Ô³^Æ~ˆ¥}¡Ö €¼aÉ|™>zù©é8¬‰/¬R¶§-X4Öä d\
+R
+­¸ƒD€'ÙìÏçbœ±M=$ÿBÑ+x,hÙøþ¥¢;VÐŽŠØ¿èk+Œ»+ȇ™èŽ|}bÿ‚¯±Be߶Pz~Êî_)ºc…$uˆý‹¿îX]?ÿj+F0X¿ RªeùÑëÂ0° ½-qÄî_ÚjQ`[´×@Õ¦ö({ƒ¢½ÐöwÛè²WÒ zhoå¥1š”½»¡½Ëê…‰”½_År4pÙ«$A{÷íÓ¨Vö*9,œNDr‹>/¸Ü¢.{},/Ine¯{(¦xe8Ó²9½ì}]$[ÿA˜q,à8ˆ´ì}®>h¯@:`§e/Ñsº$â]Ç4À²—N/d»ÎíͦL>~ꇺɃØä6>l#ÖÂ^¢½ØiÙû+›ÑqPrÓ²÷ÂÕzË”hoeÜ9èH+¼ruÊÞÑÌæg®0UaC=õÊ^/·Ù*1Rö®×†:Sì¿1Ïé>¹¡Ým…ó>š±hƒyWÊ^ºµÁœ ftRöRímªµ”½»¡½S6Yöh—K"˜Ë;n(;¼êo〮Ë^*]$˜›Ú{Ä@j¥ìUíÅVÙZÊ^º¡½•¯U”½Þ¡½Óm€ÊíBª;e/¦^™î¯–$ß ðÅô”Röú¢½þcÃXÊ^»hoK§ÉÒÆøe¯¢éÃX v
+e¯¯0–5Ö-Nìe
+ž@•T;”acVý—Æ5¥wŠ¦$¼"
+Á"@ú†¡¥(E’ TÜ®W$'NêªÜ« Œ¶%þh/çtίMM3J>ÌÅó
+Ú¦fO\. ø„ÏNóiž‰
+飜
+jm¤N:5aJ|¯]É~@Þ|¢ÓQ‘©sô1AîGK‰€¨±´›û§DÎí;ÎKà÷¾]&–Å°Ð]¤˜æ˜d g}J÷pCKT/¥GŸS7‚.õ›³›U“~®?g äuŠuv"ÙõÞ°ýÒM—׉}ñ
+’¸RಷAºÎ—Èûì#xNA.}€,Ó9×ÜdÛLÀº-…#/'¬Õõ3Ùj šAt@ìäå@ú½\X”f Ú eSáæ}ÎÕõírt·Æc[ÑjøÍUÇ&›waZp˜äR0™u1«)FÈÈøÌÝ`ñØXåú™ÒÚVi¨•„KœTC‰lã+ÙY±"í­’:£ï0Ý©‹¢zö;bÐS5à[-×R8ä—9%[ïãåÔŒùÓÛäpÐko\2W1#1
+pÎpo©J/…&nž+;n-Pòª°)ĹCçÑì¡îò2È«¬¾^`_fZÿ‚¡^v©ÝWS?Øïn­]ÓvGn[µÖF)ÝâKCF€{@[÷*• ý+ÀÜú€² …×IEÓDPr
+¢Ú iæ(24
+¬zXü\’c>SŸn
+„ÉZ ª™ƒ0lìN/>j‰É~”«ÚQèb7ó2j*¶—Ã]cx–° *ýYÙ/ó¶1SÑmùÍrMÉñ=Ðx€ç"驱¾E Â”Â>ýH<â9åOÕFØ®r@ZÖ8©&+fûÁæÎK-M'-ÑxQ,“)H4A*þ¬Î,¡
+)ÛE*‡ÑuT‹!µ_!nÿ oa¨‚请CH@¿äÍB¤Ô¿„D+k.¸ŸZuYwPBñé~"×$£+DoÙ¦¾ç'6¾Â>b`Ö²o*µ¬Õ~†Ä@®«#ù¾
+P#9É(ÐFMßU€Þ0kpPÞ5½TKRoúh>$ uÄ 6^À^ˆhj¨R÷ ~Mÿ»ô[†€„ÁùAëIú?¤ÿ"(·A]ÉþJÙDaS:°7<e(g++£j!ÊŒ½Ôô©j­#‡È¦üÁG×®6@ÔG¼™ÁÂ׋çH 50=Z%¦ðG©˜§“«DiJ¿N
+m"1ÂËw{üK‚ÆÑñìwƒ*”‚ùùËÛp—Ìuñ\Ùj×ùå6„pÄ(»ŠŒ“mÂ4wB¹’C+Ãc‚k ”ÃÒm^#í‚Ñ+â³ù”¶ÁQRÐ&sñ.ŒcF i7ÌP^æ;Ã×nS ¡‚X…”‰ N°‰ê¹5(Ì%ØKx³){À=OŸ4&ÞªQSR4FòH©³Á7³d܉¥#Ÿ‘§õ/³¤ÇQ¥GTH²Õ§!&¼x˜ð&ƒSB%ÿÍDÏ.xä×]Ðÿ|~’;Aà>Ú©!øø‚ÜžHD4·æ$R3„!“&RXÜ%µ>å»W P†uÜ†Ø è`i œ|@ã!o±
+Æ¡œZp‘jø,/rDi‰m¯*}½Jºß­¥Ò¦úzÊÁ<]î–þ`N®÷Nß–ÆN ø¯Ì|û{¯R÷P’¿ælýLäŸO ~Ù†¼ðÑÖ%W±Ø“ëžd<¼“Mš±+ Êï·®yY>‘½´‚h¦ÀP©òE)|ô¾h°F_p×’¢UQ¦†@Aðs®ø€˜ûÌnF
+Ò¶ cA˜ ú‘X´É òhU† ‹A{T<$c#qÁ<\“Grn!&’Ž(ü¶-]p•çÔ­©!Ôe6*ÚEc|ÉÊÇa€ ýö¤Éõ¶³a¼÷¬[’œún±¹»oW‹YèºÂ­‰ éiª
+´Æ 9\°î9›¢ R˜ÑÊILõ•pÃÅ¡L‚“u„Š›ûgÂ\1äkˆ‘}çÒô÷N—ü'ª4…
+Æ,ðnÝå“ n}Yâ9“xâ_ Š«Äuæ¬[qó:
+º–H­®9l. >…-«ä"û€¢˜$‚WŒ£Â7Y\'¿€#Î$ÊÕ8 AX^•´yM¥WÈiaåRÞkÆØ%°þÈñiAú
+
+›Q™>ŇâKYˆdi[{ñøASZÓŠ‹÷­~bÃÉäN~’ؼÔùfé÷ ×#léú
+{Ôå³!=”|Où¿Fze%8îàãã’Ö<Ö$Ù³m¸léÅé³C§¢'PнØ«2QL\ÙGƒ'Ü’\îF´ •CzPmÇG ú]Š·œEê¹V¹E^¹‡8|ÞkÓyxFURLѤLÙw?ùÍé
+ó¥ùË(‘“›Âà+JÆ>1ŸM\Mšõ,/ôç_ƒ€@Ô¼›Â¨
+ê,²0øö>„½”'Nâ¯vzJ`‡8Mœ.+bkeZvŒš‘&tÌ«×TÏÄÏPS|„“Ìî0 É/'N-^ÅœP'sw âî'þ¤ÿ”CKàKS´Ð ‚¨Ë]áÞânË_²X—'^™ïò#U€Þo4§Àùë ½§ú%QVÔ'R.ØðxpâtñVÛ˜z“Ž˜è–KÒ¿…°“FDà±€ÖBùO½~jÃœGï†ê”§–½ý‘Ä9š~Ó2ü›÷g˜ZEò¶ˆIãö#Éé:*˜[øë´á콫H'l®óÖ¿_ì©ÓZÏ&„
+®sª‡B¤ b1Iâå<Ï©¼øoÓþÒnw¿ÞøoÎî'£ ð›n(²d¸÷{‹?£3û³À"›†Ú3(QTØMõ!t³„øéæ#iˆ1‘ç g%Ae3—a CâýHÃq®ë¼Òç $„®Z›&ÏÒy™¾£ìä Y2‰«þ"Å„ëág)>Lžê½PONg­OèXf§ Ì‚Vž7ªÈM"§¬iR¸alº¥l+%¾Ítñ#•uåÊÛ;‘‹Së“.?„„ëNáL÷0‹]Š­ƒ
+‹§F$ÉÓˉÅ|æ–ùÝ•’²Z²ß·pW´‰µÅ“öÔ+¼\mjyYÁ…ÛœŒ(¦UO~ìPh¦3Nj‚u•]GM¬ f ”·b-/ÊåÃüu°•ç¼1–Ó}S E¶~Rœb?ãìïøŽÛL²¯*Û‚Ü&Š8ìFýÁ-¬ ÚŠ½x”$d[­»F)ÎÖ0ái/ÏQûȉŸâ\úùùã6„9+Òµ1B
+5pùx=WS1ÿþ+xð|–äÜŒšéckdÌK5ëÔóB'Ž÷Îò†ˆ)“KúrBü¶ÚB,øm„àÈWkš|Y7b@†9DÖ¾Ùç5x¨
+Ÿ¾ÓrX™ ³W0ÉZêßF»ÙXø
+Öí¦Ffà–<Ü®ì<õ~¾GÚíÉ=œðwŸRã,ÿä&s•øN×ÖË+¯
+8_D©š÷´,zÕ4í¿B«j…z¥Ð*«=1Ÿn8óí¸Ó¨²h'ÏMÍøI%YšjnºÜ7ÄÜÚhÄ݆㆘w,ÞTÒ¬²#§¡}yâôä÷M¾øºkÜÈÍqBOýþ­ˆ  ›œc̺ÎxàÛ#¸m×…4êß?
+¨>ˆ¢ñgÄÅ…L¹ùÇ3­°Æs…W°ªÂ~å(!±,—ªå»[ê%\õ‹ íÉ2NÞôÎxy| ,úmq8ÉG-’‚ù¾ :,ò™™ÏHÃK\gµ¬I¼¢ÜKê92KÊðSo~<¼rƒ£Ï°Æ’24¢BÀ—”Gâ×{tŠóÛ 8/)ÇcTàRGšÄ¨Q6}XƒÑ÷,- ©Îr0j¾ÒÏ’‹j]D>~ðÒxÅýfùóŒqÒ°MK‹vm_Gdè¿=,o>8Ä-üòº‰ˆ•ÈBáÙ
+žö Om[ ÒbìUÅò‘mF3¨­ÈI§ÉO¦)ŠÈùóÏ>~q]ðBHl™Ov•ƒé̓ëXY­”ojd½©W…¤·Äé»f—£0}’Hß©[Š3öˆŠSSŽ˜ˆ@Hp,¸üùÆ´w®¶§§Ð„NõËY@œ<ÕÔX2=BÛBp燔:Æ‘ÛÆÊÂʳxPk¢Ë€R9Å!\&<4âá¤|wëÉœ\‚OB®/±·¨­n/«Ö×5rš”;'ci€ìñüÉTŒ‚D¶wnhRÖ…žŽsÎÊjϱG¥1)³u˜|tµ(›#XMM›2”ôh[ÊŤÌ×éïäĤƠBÛvœ#Ѥâ vk˜>âï÷¤­‘ÅÓ×$"½vO1©å•‚¨\MŠª·°Oí•)R“À¢°-¢{!5ëÉÏNÔ™Ä×]rYYõ‘fI”âÜ´V1U¨ì»ìà;£Øw/;j+*ªÏ JöZ›89ÑÐȳp$V°fÁGYn\Sg’H4Dâ¡"Ü&{®]Š!1ŒJ{ý9±p‘³¨é¤Y8“²‚ îg¥VV’B>Ô£YmRÜpåÓöqÁ?a ÏÌV†Ì¬ÆK°·Ê3¢ìÙ'Nô(ø—óÈaáõî×R–—tNt•xï{f³³òØ,¸rŠÂrÎaZùÅÊE|kŒ¾oáï»t1Ì¢d;dá[:’ìi!‚
+þ¦¯YÍÑZ0À5kßÅ5ïÄûgš† /–»€UN9G¬œ¦šN[€Q€ ·lí¶÷P‰mÁ˜øíb:Üʼ˜ëŠÚv1‡AZ·oç2EïƒãD=›S'qÈÁ§&å ܉ó:¬ƒJÏ É'Täÿ¾ïN“3Ëü·)ôíc: '7|#²Îrfå¨*&÷VÒŠ`¼»U)s^S­pZ²{m W
+…&~wY“ÎÊy;TGÊ”’”r2g>
+èÿÞ¢’üóuE†ÈŠÄB Kb¥RÉ•qM(V@ý¸–ð,šÔ22#öP˜”L 78Ùuš!§2(,PÍ6bŽLM؈4D–ªoÄs ‹xhÖ¹óbøÒÑ=²!¢8yÇÉÅ"Ö(~Asšš÷<ãÂò§Xtq†ÁŠç³ 
+T¡@A rÍF¥
+sj¯(6ØU«+Ú“‡Ó•3áe8BI/2F#e&ÄrE{òBlÅÌê׌O EZ¢:۾҈⩆ƒlKjôÉÇÍe¥ÁDÉå'V!­Í(¦Â8±•Ü¹Ì«E²hËî&Ý4Aˆ&¢4ŠòaÈMo?ç?qñç_C
+×NÏ¥FB›:À@‚êñ/TArŠµe¡àKCÿ;3©dó§W—´Þ0]B7ìb _$¹L¼"ÔÛé×XæsB b¢ uÎÔÖ¢…Š™Xë/˜’q¨p úÌ´ÿL5PíÊÌŒhFkÈ„^Ÿ…r )§‡bE¡PÊFs
+Ejøv(ó¡– ÙbŸPÕú){,P̓,D´+Ð.RæG“P\0ÖÌLM.N+“‘"i†q  @‚Ÿ
+ÛÚ3#Cm·ùK!FçÃNýèbl™pšÂ2ÆmS·Â5©`CåÃ1$Æ7|6Ÿ£*Ý_—'N ÉpF$,C ç7'ej2Ê«ãïR7K郼‡Ôñ_ÔÜ©¢ÇÅ܆!¾Hh„Û**!×û§L#b‹£Y±Áq°Vá}´JË\<³0J¬™'s¬/Ι©1ª{¹ùêƒ =K&–/ŒX<8xzbBªQÄ¿¯rC¿ûÜç¢Óüî–5z¢~’yMÍDqf¬b9”ÙŒmMFWX‘Ä j_{Œ%$,Â2U¼ŠÕ•“ ƒ5*4™ˆØPèβ5"¥ ¯¦’`ú–Š(Sœ†¶Zq½8dìH, ¬ kd)§ç:ftʲBjzRŠåtL®E©:ÀÊ£)IK>‘ü¬qˆ,Q©G"áVÑœbþÔ4.G9tÇ•i+*QH¨oŽÿFdWCýÓüÖh§2á7eçÃŽyN‘–ÉsPLI(khFÉ”Iö•5¡JQhUÃôQCé\©¤ômÕCH#®—/n*/ YEúúTh<¾º‰"Ó¡Mhi±§Ã`ÆE~<¡”Ö°äFŒˆC"˜‹á\=7òP*2ÕÀ¨ÔxRt†¡ÓÁ࢑aÐK‰N(Ê(Mf´¢ˆq@ô•‰¸ˆuóùȈe¦ Kß®%*KQB)!©z)R*4¬0,Û‘žŽ2 z,LeêK*ghÿhA"> ƒyZeuM§1djÕ˜T¿Catã„]!úúL÷éeRf5‘ ·Lkm3Kš~t„©³Wg!3±ŒiRbÏͬåæEá5 …¢,¶DeÖ0Uóˆ…L¬ÒðüA#qTœ eQ¬vDTj˜e"ÉôÊ&¸àqƯ³c\)![(‹·§.¥.Â0¬
+õ†U4d›ÅAJ.]¼&Ád
+ñ GŒà ã—”k•QF¾zÇ.íÕ¸êL ‹‚'>-/&\Õõöù!KŒ©R3!µEMô]QTŽµŸÄ$õ{·FÒ.Ô‰š†LM¦Ô`š:Ú·ÆáÉFdÄ!ÞT®R=,°ÍG"6eyìò6Å Œ%¥„ψÉ21ó™]dè5q‡,ó¡(‘²j/9…ÅêšÎ¦‚[ïÂÜ!&–Уƒ‰„üž•+ík«
+‹AIùgóVÌÙÓàUÌ>HZ6)Á K ‘RÁ¤'ª¢4Ãæ!4ìhê<Õk†ÍZ(kØÛÖ¡ÛQPµeºÎsè'VhvšrÇöMŒG‘„[‡âEH?‹{¨}$ÂGŽÛ4¡ª šŒí¢™j•”ý¡žsZ#Z2ˆh™ÜIú*$L/…TLIhBá#4MHkdºýÁG|Ô!Âh(z¢¢Š$ߤ%H¢\*!
+5šr~øx“b”1ò¨Îk ŽƒCŪ¬‹gFÅI^çP~lÁárÔ¢gÁ²šG©—¸È÷ÀO¹w;dNÑ uFl‘ñ¨(„µ+ÔUwZ(ZhÚœ$ì†øª´fuÐ,j§k(#5$Ѭç"œN™dÄq"4•V–»a|Œ>ÞZøMÒN¼Ø´"+ä¦à‘QTi(Xˆ›7DJC[I §BÁBîyšKÛ¦t$U¬‘Y£Šï¦•Ð«T…ST¹ä3žpäCvfgTIãs
+y¾S‘4µI.¥úCˆÓª Ï0£H¢D(éuÌâQÞL Š°"Z¶Ã[wÐÓ"*‰ŒdL“úƒÑ(2ФAšâ,•!–É^l”0P%î¾Lxb¢MÖH-«²P… ‰Ê.W)âáTG[ŒtEËÄS8­8néÔyyê¥áœa+ˆD†zqP8;Ï­þñÚØŒÚ9‘O”l2äJb-,ç(›ˆG\ªÒ11ɤ×Ð_RQ1²p¶ê.­vˆ”§¼ž>2Å*,çCʪ˜~¡ëÏd(¯³8Rl©sì•ÐªNAâi¸l*Z‡»öRpäɢݰÿ¦F%Ú-òêYE4|ÓÒ¨²g‡sA-_L9¸4áÓ8ø,¯¯RM¤Š³—d‚Cú¡Í‹”T|4¤×÷:óªp
+¡FÕ¿ºø**KòâH,Šthê›éºVÅ”•ê›Š¨ŽÈ‚A§M£ -æá¶_êk8³]J~'ƹg*q_ñÄäÚ‚—·^žL£UOTxaŽOBáqW‡>uGw©9¼Ä—Û=…L8Õ^mPl:M¨¬fxCè¦rJG¯Vª,ñ¦½–©¿6WퟄÃ&–fÂ`x°Â床âsNLˆÒÉOÁ6kqðhí¿(Øõ’ÇëÔÈþÏÊ
+nmB®•Ñ˜‚u}&­‘ª ’(Ãy9%LJxoš¹©ÿ²ÇYÛ)¬ IÒɆžU–0£ëp4+‰[I¸Ï"B\ÒPx”ú ‰óÓµ«A•š>L£¡y9Fñÿ5U4•Mq¸PLíŃææðª²3J^t‹ü‘1rÔnNâ •¢ZÑd©e¬X‘]3_r:gf¤¬p!áx‚# Ê…Dæ&%û1Äp|‚-'§zøR”
+Z'±©sÆ3tNè%£XK D†‡+rwÌcÓª!ù´Z«aÿAs×£ÂkãV¸7Ó ç®UR9ÑQóYƒe*U+|GLጰ[)¼@% ‰T#vø\âö£Rø'J¿R¥ƒ0L›2H7–Q§²™×Oé”2¯š±[àv1%Õ³áUÄÿ"†1C—1l1qÅhMËkhÍØ5Òá_/?’ÐeF´L¬Aú‚Ô±’WÍ Äyk¼)¨4J0„f™Ç¥hj…lRSR2¿9w…d,ÕAFÕ”aѧ,YUyõžvi'P´2?4­
+!AUNæBÛ.Û‚<·(§‚¥zBŽ–åQ(û+Š‚¤êî«ôvŽ®ÅøÚ4†ˆÎ2sþÃð«ºÃɨ!UxGâÕU5œ´-—2pÚT¦}‘ª!H^ý`¨¿¯ðj8j$†Ó%Í.Á š¹pHÅWgÂC”L8Ϫ"
+Ÿp¦ 'iEuvmˆHˆÝ§SŽI+„.ºh¹0Ü¡
+Ý–KBÔÍP„ø#Ê°Œ†ÂÙy\˜ŠÔñ©8úö!NP±B‘(ÆL%Aôw2°hÊ#ÅŸÚ*)‹# :,[?‹/8Rè’jdÜ­,û¯_I44š@;&Ö¯Â,g‡«)lSå‰XhTò‰fF2‚XÒŠ}B]]SÌù2F(ÞT^Zølú€ ‡£ŠêVE±&Ê>jÄ|ñÜ &‰æqL-!|1œÔü2C YÀ 1¼*]d62U<~ß!⑺dPcÄÈþ!Ô¡ºu„¹q-Dk£‘ƒF*Y+>yÓùã&'mcC÷A8&î™aÜÁ|É›™.³}
+1ñÕ0GÆX¾Ï(X¾a šXB³fFÔWå\­ÔªŽáĆA _ð,`QùÌá…Hð‘(*
+šÒþ`±«'²vÁ3ˤ¢ÚB†±É§ :¦D‚AòYà dÏ*ò!ž˜qyÛ˜>¡bÜä€ÜOÅ \³wÎøNÏ‚Hû>Å 1 –DÉ n•¦ Ñ&X:)¨`Rò¡rÄx@# û1r)
+MTæ ‘ ®,ã‰LÂcb@ˆªK 1¨"wÓ¼ã_`—B^·¢føvÆüàm<C–q øBåODKÖâCç]¤B%"xAˆx v•ÂÛešš˜¡û¡nH´6Èàü9§4Ȭ‘ÄÈ,‹OPûioE{S8(Ä FfÔrC„¨†í°ò
+UGd µÏjÂô_OYq’MÆ„Éë¾2Mœ¢öºmJ‰ :Ráó|¢(éE8ýªjˆy:¨ajˆ™ê+ ‚äSB|½,0vB*b@|¢jßñˆÌ{#I*Rü²¶„Þ[ÂN$8Ú§™D묒/ãnR}푪ú#+i‰Ý©4Ö(Œ‚
+$0p)¿½ùÞq(zØð>‰Õ µ ãNc>±x™jìëÀ˜ Ùñ
+T‚"XËŒ„ya‘…VÝ–kæ¾ 6¤ lâ+šštA…ë±.=É_îÓ—á V5
+u&FƒNiBŸFÊGXÔ%áé NìÝâCÄp <Y ‹œÆ‰M¯s<£3t!Åkm€ALJ‚Úª…†äŸ Ò”Ô>T–ü¯v;šd{•Hl†$ÚÚªˆôtvÐb$º¹Óù‰‚ƒd@“}¢S6ˆ{GTk‚ ¾`2¨Mè”´"þæÚBù¯b`ù„¨Sñu7ªê°¨|‡|ò.°ÔQ゘™JK¢Â}hä"³&y EïœÖ4Ar·¬È‘‰
+—nn™êe†®ªO|‚æ³HU£èTË)4H+dˆ+là
+-;*2â‹JD&IñbMÂäZ*ûFŸ&(vrŒÜ%uè‚qçvcu.på‡dqU…U¡ ¤äÀz>KìZà€K©B\hÈù8¨o·C3•ƒÙA´àS‘AŒ=KW9(…B¤–©Âm¼Ëy©;²ŸH‚~08¨ÇG*ïJøGMUƒ
+š:B ªá/8 UÜ&Ób0¼Ã…B„L¦"ÿÉøB2¼î¹eÀU­ wòƒv‚) [0 ͦª?«Æ
+¢½þLå“¿ÚZª¡I_e˜˜j ‰p÷š'6Ã^Ü«ÂdàN&Ȧ5·£¤.c–ú·Aj»¢'î%SŽ!:çBTÃh˜îT-áQ›G#3¿Ù (¤†AfÆ
+ƒH+y™j!ƒšF§Å¡ ‚ G"{ª§ZB‰ª5–¡‘ºøQ¢‡XFq²ÊC™ˆ$á£Ï\‹cŠ´´n1¼>!Ū â4 ñõ¾ð
+Rˆ cÖ324¬ ’™qPB 4Ž
+
+¸<}g2
+úR *‰U|à¥ú…e\×Ô0r
+t.TÁ)ÒEî`zÁ€%<7GE¢I\;ô-8¯³‘MÅK‚ìFaê0ø>M.5Ÿäõ9 ‘aí¤˜zÂU2vèÔÓ:52­ê|'ÂF6Jrfê³j5(«HçV!ÙIH
+3OÌaƒé¤T‰Œ{ÐtHÂ,(š1Äà.Æ7 ³`nqoˆ¡i…Š•Ð?–šfõ‚  ‚”ãž03R
+µ !F`
+-B4(U#Á¡fOäò¦ ECö¡פ†<
+Q8誒ï¢àhV´¥ïèh$²¡YaˆH W‹ÄbpÈܶlú…(‚ÙQ ¯¦©$ P_‹·/W.uú£bêýQἉBUáNò—TÕAúI‚8/) 4A€dÀè@JØ
+åÉÓSž`DCT E.!À`A:P.
+(€‚
+$0  Á‰nÌ_²ÑGãÌL|[ •ó8ÉMÊ.R6dá\QÇM8†ñ_G­ª‘KÌ^Çcˆ³„¢ ‚ º $r©SM5Œù€À€ÈŸñfÁ`
+*耰ñ؃åCZ\(ZÝü{)𒈩ªwRŠ‹7sCK8„U›p
+U•Ì1ª´¢ŠŽŒÿÓTœoÒOTнb˜‚
+NPoƒ5ý\F•,à _ébùȨ²` *³T_œD!$ãš 2(a‰SÒÃéHÝj
+]Ð/à
++¢1´9œ^CSðÙw¡ðà¼8Ÿ†œûE³V¾ ´OI¼Î,Bf”ÅCDÕAúåJ87!ã1ôÄ–kƒFW×i¨v ¹C¿„N²Q䙺-
+eìÖH#Ö‰`ï8‡äVxï*[£ñ- ±‘tKÊUÍSÒÿ ¡p.h•)ñcè*å#1»V2¡ÎDIO ˆ¤Ä5MF
+oˆ™azñóå™F"ÄDE†¨Í:t¾†D‚"^ÓQ5fY°ÒŠ1 Å*^BM sçøˆÑ¿™@‹ðb| gˆ™ ÉÑ@Ó)ª‘誴¡i É/j$GE¿yÒ…b¼PƒT21Ô¢MÍ]u¦¡š ÕH°Xó•%¡$pRƒpùZc†&¹¾<½£äQY©_R2m`±Dz$„KÛÕ;1ÃÊWÙúXÅØ“™ìåT
+••ÀºWBµQ<žçʦ #¦ #n¥zb:'Œó(b$ÓbM¨C“"p BAsÏX<·Ó_öŠ6Ÿ&6qâ£ÜPêû⩦¯A!šKÉu~§|œRL2Ù–’W½Žˆ…ˆƒÉ(í²V1ÔÈÖ|b"h±|æ²õôÁ 5)›mù.’ÒÆ*4)ÒØaâ‰m©1ãwj1Ñ$ªªµy÷P""%·ŒÆyÄžÙb±‘Ÿ¦˜mª&ÆhžÄ¹—,bÄ”¡ ŽhŠ$hR‘2ݤªpêÒS/¬¸-kÿ‰ÎWåá3rh¥ âOÆ{Ì 1þ±Å£Ôúm>Út§”<î’7è3ßDÈÔ‹ŠIOxT× çë Œ9Ž×$kÕ6%ÓYVkTfáÿ/k˜RD!Š–>qÜ©ÎPE=Ÿ™Th1òC$+x‚L3š8š¹JhÕgÜ ê*L¦ê^²Ú&Š©:©D¨T¥ZMŠmMŒ›Õ§ZaSÓd&ÊÕ°³1ƒ=$mã²8Ga¤4‚Y-™0˼”!‰ëoÁàÑ2E}£ I‘j'>ò 839™óc¶šê×]Z_(¯™§ctøñ‡UQ ›~ ¥ªL¥šê ^P´FTÃ{´ñÒFjE­Mü1½NÄDBvÙ šG(W‹Ð7$FDèãOJ½´zŽXL¡ ý&%¥ -îD7_–VÍ¥¡ÑKìBÞëTÎ(ZfD¢¬b³ÆD®)‹TÁ‰ïГÇ›
+ŠÎpÌnJÞÀeµ‹‡Ú¿2¬f2-Ã,uìñ€b
+!ùaÈÔq•Œ×‘"±…Êq‘
+ûêFìÜÀ‘…²°~bèTŽ$°h8gÉYr¤qH…ŽY™ü•æ†‡ç+!§’áÂc"ÓœùY<ðQK ú´W¸m˜üA,r“£[+Í7¼ ÂŽ ƒ.(~I!ܪ !áA'R›ÅCbˆ!A,³HUÈ"D…qSpL£*,óÌH(1’‡B*°<yëá^$%ÁNñÚ쳿¢•Š¿Ì¨H‘tN™V¦
+VxB¡`ÉxN1Í
+ É =T’ù3sM«W›†ÝÈ×hÓqœô£•\ßhî=Eè4A :hs —ÄXCŽ¨=pÐÒTHˆœH'û8öR
+b ’ò4màà7t…?•
+§˜D´µþþÈÌç5"ÎÖFÛªsÓ3®RÔ$’””œ¨´Lìº –PÑ
+ 1ú¬‰ÐYO•þÇÓ†G;¹]Ö!½_‚^ûXŸ¸á2DD”[ɼ{†-¥æ*ЋºÄ÷ù'ŠTOi¦Ûœ$9÷^¦i; ‘^ÓÝ,›Ã«lQ¹falRE@û×iDS˜â,â¾}9/ú‘ קÓÄ]Ù%n“ù_eN)· È"îëOœÚØMç(4K,$}uýð9q~V…½_[6þõÀ\Æ…b`m¡M蛫•m£íLÉ"ö‡ëòäB»NkSY·B7¥}ir×
+äÙí2G²"« o¤âïß=N‘™%­‹¨^¬ZKݾ§-Yf¨”’©9
+!äIå¥R^Mcàá{ÿEÜçsÄ6“vYYˆlÂŽDE•O)24ðW¸Ô·΄xÄË„ýìÊÅÛáè"v{EWùí&¹2 ª&e`>ì¸ë_Qø—„:ŠÇ”Ã<8ØŸ˜¸ Û"àSÙ‡‰ õzÌgÿ^µìuÿpÊšM„Ü4tk»)Ón==èÀK)ÉU|÷ýù.†‡qÉX…÷×Ø•VÜ °e(tKxRD‚*”#Ÿâêüô«JI”\bw;ànóÿò1ø<è_ÎÂí–…HÙ ’vág‚fº/–¼ÐõŒ¿²€½4w%jqªÃñy!'¢wšÎ&·¬2`ÓݳJ+±'²rdR‹l”A,‚­¡ñù´Iºï5ªc&í0JNÇm‚;´baýVJ¿?¬¨R¡b±N¦Áïo•áf‘bKSƒ#6!ÆVÅ vgbDþãà úÿG:º_‹ý<ÓÀÕõäÃê@é¿5Är½S„ñs>øiÞª$mÖŒŠs7‘ívFÇ ˜ë!B}®ñOùaxm™™Ž†w6SG<Ø;HZXä5 œìi¦>Ùòž£O@‹3µÂŒ~ô1añtƒ<+›h(½T|O2·Ù®™Ñ SÒ‡ óNxÐymýY›ÄëHA(Óñ_µÑ€˜OWnĽvWúT×kÀØnïøØ î’Ã@íòaë%ó›×wÙT§6‹Õ–Õ†Þ´â)æ›0rv¤l*(BøÄ~i!5úÍëjš{X,Ùç~æóýßn"SÜë¸wÙ›Ù°¹Âj³›Á£=C·I¦qq¡¸”lÕ÷‹æýU¿03afžܺ̅4Æ=­‡°ÀµÛêÖ³Wúþ5ÿOpÖ+¿mžöx?³šLtå(K1¢qÔnLX¸×p¶ë3ÏŠŸCh
+^ï"ÕÐê6„i¾&ˆ"yÃÖ'-¾Ò‹ê°bî0ÁtoOà B¸©m\øÊJ®î®Úb fšÙšª‰<mhT~
+†åîr‡Ös»]ÝV]Cànè3å‡Ä]3ø‘GY¶¶\MW
+O2~0´†ø’e&/ã)¡
+ieÜ#n
+7c±¡ÔŒ*’@b'»™¢c-lÀÓ†/óûM ÄQ½þHäØ„µ\¬¤QB£¬. ~‰q+Õâ*¡ôéŽYä€U4ZZ‘s¤9}HÒMBŠ™ j&$è’YõðOÒ”­àtN·ÒÛT¯·‘m‰ù‘œÞû%Ÿ=MQûÕ
+¼G
+÷ªþ¸â'5'x/ìP5uݪ)n/œÛâS]8orûL0@âlhü´gÅ,XÁAxß—ÔÄ_ë‡ K`ò4¶ ÊÇV_¯Ó@s{Å´XÎx7H
+ñŽ I‹?ÚM´y ópã¢ÎAlGU{‘T®|¼¤Íľõ{h0àª#01ê¦Õ´äå…ñG€º¦'“àiù7 èbq þ4S6€j¥€æMÔ­p}–ÛÁû|)Nè7L\G8 ·ÚÊIëS¤-»<ÿ
+>•§9Dt Á)n1 ¼ŸÐåômj‡|ÕÚ<ý„†e[kU|ðö \›£¼‘~äN"L ±ï‚o®Ù™V¤"˜ÆÍßtÏö¥“ª.%èI·;(½1aMM›p£ÚýUµ0…y5'd«’Ø]â8´dgAYÕ¦
+»’@+G`ƒVTáúªjÞƒè¦1)“Mƒ(uÅŽõv' ½³#ðW=2„ÃïÚl’L!ìªakÝú·S÷ÿàuúŠ™hröø…·´&o–U—oë2óãLd):ê†9úì•'®ÅÒ 0ß“@× ‰g&zK~7½^Ì«ÓðYyÀ Òý^Ñbúçh<¸FK|A…úeÉNë÷ ,E€ö’‚ñ¨ïuðøú:O#O/þ¬´¤I—Ÿ¾—*@vÄÈ=£w'P„ëDž‡ÇvY…òšÇ4f_š)3¸Û˜m]Ôͬâèp]5ÒW¡êŽ}¹ã!X“n6½qr²ØË^¯8˜Dq’vÃ
+ÔàéÆ®o¸õ {º­ê˜3`¦ƒaf <uÀƒ.Ä!ìF áõ~7òP~¦ê:ª%Ǧˆî\š +Ê2G« |[\å´"X5öÉE­‚«AÌ ¹Gƒ®Ì" &õÛ§œ‘á²ÐæŠ^1ñAý¯í›'vá  VÂó´i’C>j²¾ë†ëÃIQ—WgŠâðL’À…Âf˜|¼ql!öÄ’(ÀGïK9s6it7Á P†¢Ì1NénaÎsz£y­‘ØéD•>X›·âLqv÷ø˜Fú;}ÒyO—2EyfŠ½Î°`;èŽRqÒ½bÊÐê1Ø4q;ùÆÿQstÈ'êaH:6¥ñˆØZ
+;[`*ÿÌYf¦ÔVZi‡T¸ êŠQ÷è¥l±íww$Gs"ðŠ
+1
+Q~7 S^=xDÓúÀí† q{É7‚[—Ç „øö=h+¢¨”‘g«jÎÈ)`?%V
+·> ÐMËô…[–ę荒¤)ѵü9Î;QH´BÂôŘ4dsùÏ ¹Í ìGä<h(JQã –1Žhé&•Þ¾NlÜž$E&üÚâŸßyå]cjØ3 ¦Í¶×Sž¥u4^סæ ×)¸!¢JUÔq6¸¿×Óx ò\ƒFˆ¬Ù²´t¤ô. `‰p û ÞòRˆbíu´e`¨
+ö22¹ŠH1y0ycäLÅ2t¢ÝÓß±‘ÂMAâÃtp‡É&L°òy†µ*ÌÐzœ< Ky#û’m¯ €—/”ЊÓ õ©ÓX_% iyàÕ›C›:§5¦,'–$öa)„HâP9ÞZÅoõåN!·Ñ÷ÞkRŸÎö6p5¢¿Ãî”z’¯Î¿„EUZêEW]M”²û•tÈqÚÆI+ÌÔñüÐÇ%ÝjOÀà
+PJ»ÇU‚,ðºB}*è]û¥ÁÓBYod›ú;Aࣲs¤0IHaSº€Cah‰SˆaLR¦-šøéè
+Æ á{„©ö.Q_¤}#+׸]¬(¹>ò@5ÊU’µÛò\€ÁÐwÊ$JêýDÌ
+™+ÙqÇ@7`%·³=í³ k¿çÐÕ¿|7
+m¢ý~õ;oÇàÕ{¾°È—KÐ>°e
+þ¬Ï<šµœóŽ{7¸M™ã(®¶ªŽö«eûi™"‘óB%hÑÐå)†ÝÂeWAÒãQŠRMÔ w×6êW²“<”c•ˆûâOênIt°Ø]-쇻ü%û1i‚‡\*@ý´è©›(èé<ã/CÌ^g‡4_.–™…ñlªo-ÇCŒ´$LOè•\ú®±”…&Ë2~ßR±4è2ŸÞýÞ¹GJzÝÀÎS:¤#5‘ÜEÔE ’q
+”nñОd">Q!=â$ì‚Ži$¦¦¤R€¥øÈ~þ,oÉž
+mŒ¬3 „¬”•ôÑHž"’Þ¬Šö[WAå§N_ÜO—a¿ia4ç
+°7ŒÞið¶ìc Ûå"J™JÚ΋ÈÂy¼–èü Å™¤»÷™·‹Úú^µRg{Ujû”ߌóbL†"A]Ô’ +¸E¯è±Y€!¢ÚÇ‚üNŒÐÍà/6Ó70ü>5jÌÐ΄Úut¾Zö¼>9_œÕsúH~íGᆭtÌ­!åÂd±+á½sy˜¬’Ø&ôêSa{IÆH éô´)Å2pŒ½—…Èßð
+º‘C…9ý½ßPÚBÏêvÆ»8„v-§Kbk‚4ÅT0Gl©
+02ΰ]P§È)’CÉJ, ¦çz\
+sé‘ Ù¸Ìq^…„–GX°Y56 X!A:Ùú-n÷
+~Ï€àÕAÄŒŒ 'yF°â#½Û2) Øæý œ3žTï8ø`3åAµ¿ÂÿD0Ûð¸ØSD^ºg;ÍZÂw2<ö1„
+ƒ|€½ÝQÏÝ
+uDp"^'ê3 pÕkAjš ?n²+ô™ß„͈˜4ÛO¯ù+žØ{Q‹¢íÍ㎴Ú-œa–Ðr8S ¼‡Ô¨Ó²,˜*S5ºcÓ«»Ü±Q
+´ÆÄhŽÌ ?`b-‚šÞŠ’¥[ÓÌ9þr 0ºì†%¬òÏ{;Öê³û{ç%™t‹¹û!¡xSéŸW8L3¸å)…:ÉâœZýñôÚ ¬i?8%£Gã+: ñ3Úþ,4à[¨uÛî-vh£šÓFGkKé”"#éÊrÞ$ИúQ6d²Dhú÷©pkÅ—F}œä$-(
+Q'TUɾõyõ–-ÃöÑC=ØU2¤ziðTÖ”i‹€g>C‰eá|Ár¡ÔämDfI{¿7AÀCèIÄÔœ}uÚ&~ïÛ)$ˆ1ø1)’';ó†(lVÌž
+>bOVÚå;þpƒ !d ¸Æº4ŒÞ°¾Þ¶&÷ôPá”öD¢bˆi‰6¢¶‚„¹¢}ØŠ+Ãwï¥`…²¨olú£5‚ˬ ¬ŽLÅyrOö–µµOzÂ_ÙÎ\윇çžtZ£p2z`œ>üs%†8¦¯T8e#1—nph¶¤ƒ¼éù²EC²UŠP]¯E4¨ÁÇÈF´ùqˆ=8 ®®,²7Ï"\HkœÜQ­Ò£NfP”˜a¨T¥…u*¢OcƒINų2·'û:¡KÝ CÀ ¹
+Œ1àçZ=í=× ª*Ë(Š<È•4côýtφxŠ1©KYô˜]6
+‡SÑ­×ÄqËŒ?ÚÄÙÐ#Õ­ £›nzØØ<†øJ\Cðã4Êçö-ï?æ‹yGúnqÎÂLRþ{ª~Ìbˆ-T!B?
+{ñ¥ 5ÔþL… bAþåý­Îd³Ð~¿ÁN8Rµþ·í%ÌÕ2¤-{ý¾oæPÌ»­Bà!ªŒ›­ýóè
+‘é©$6Dn}í“í¨¼~NsD%~O͹!;„DEÁØU½ÿ¤4 >íüXÉü$ ½4°ÁÚÂXoFŠóÇú·ì™Æá°×ϼ±á@$K£ á‚ŸÍ´¾“µöÞ?WSN¤À¡º—hèÐ*òmhÅ[°Û{tCSµØ£0£˜”´!ãÀÖ»Y$«»,ñðS7€berÀùôB6ß|<Ñ! ™€¬ó š§AöqosÔ…7Ëñ[½Ž% ÄÆÙì%ØTÊhé=S$7 ÍÛ|yZ\¼ïà6¢Û^ouDT3ü‹YBk­…˜\mu4Ý U\òâ³Vy›žkV#¾ÈèÎ$Š|_OÀÒf•"Ú8Hä)Æ‘h,Vä> ëÁöÃ+Sr’ª¯ŒQ6¹!’Agô½‰:@õq[œÈ-‰ÒþÃ>£ìüý³°Š€ÿÒ? îÐa1×I‹n
+;aúõзâ€Q/Õä*ì|…Ä©ó‘›‹¥ÿÝ À‚aCG£²Vj0E
+;$9 úÈ’ëh‚@÷Ô ½·‘çþ4Nõ¼¸æÜÂbÑÌЉ ª”Þ§,îÜ<ÜtÍŒ?žÓ9½[¯¿¸iF SÐÎBÑâ4"*„2
+‚‰yŽ„3ýÀžô®ZSIa0N„朔´*÷â:³âHpOÒk:%¬¡©7ÎÙ çA6fìò6€µÉÓ§u)1ÈÈi¹Å=ß ô‹²S[–™¾}-, ñ@Þé)~UÒzUþ@÷¼îÝê ¡õÆgU¹®Þz½†t—„ãƒÀ?œÍ’©ù¬ndìÞÂdž„Ž/ëÃZ­¡Wv{Ä’žÌu¨‚tY07l¨›J†9€nˆK}5~›‰‘cJZª$çVF@¹Z!dnó®ß—ÆŽ*0«?':HlOÝjöV!N‡{h[37 &M‚Ï}  $u‹ ßô}Ó½nÈŸøÑ=8‹#qE ½×™µA´Ðú¹šÂŠ)¦¥Ö‚'_´{À¨ôc>œ°ÒÁÓqW"b
+'U)µ*NE7ãaªÊ7†ìŽÓÃblŽ×å¡Î¾WZ’»´ju—ÅþòöÒ?•§7
+uüËöGa%n•ïðÓ‹-sÓˆ+.ßß
+¶~ØA¬¥ÿŒ\ÍÉ­í<àü„ú4=+¡öw‰á¤ùƒùGp²s³Ý»{«-¯cæÃ%nêŸ`.%~þùæ(ø¯||V¿£Õ–ʾ|„ 2²×t—Î4œàñøxãF`ÙglA"æßZ˜€è„/ƒ”
+,. ÌÊ<j ‹¨(KËh}^êLM7‰$#‡©açVîöí±†WÉ"PŒ’iÞ]í»ú!¢ñ™ë{âoŒ×Ü"‚aêº[›îµ Çë ±zïÐ;I{`³k,Zp¾ŸíöyýŸöFmlÎUÜw”MíIXÃæ Ò]a{geƒåK^üäÙƒH2lØz|ŠIOÊFa¬^…xÀŒÍhÀgµø½÷eU„g»øû‹FÏCe[pĦd³ÆRM»žØ9ªG1mþ!röÎO–$²ËÌ„$å«5RØÌr«iŒI³Åûð+Y87h1 ݼA¥÷Œ mt‰Ü`ˆ¡ž ö(ôCác—@ïéND"’‘.•¶œå| dÑdéÛ?ðÝ"—ÊÀW)%Ó>&ª x5V¹
+S‹Ó¿:`“²†%\´ã†‚–=^w
+»•ê&­Š.‘ýÕ<* +éýµÛy‚ì|W Àâåm_4>íÖ–Wèñø@y…„s8ÎÜ53À.1¨³†hi@›(²HÓOQB “Ã>jœfqÊÞz’º+Ñp‰µ2Ø¡Ðè•Èÿ;è cõ¾öºÎ©Ã_VØ™@Š ÇÕà|ì
+¸¬¡ø<b8ã‡CƳtÖO™Pô8Â*&ãšNòw{äÚO(2çÞIZu™i2lnÞ
+±‰ÆÖ>,püd„‘©}pûq¸¹¼¶è¸]á+§ [iK¯ØÄfÌ{ë½]pWZ#%qÛà&ÓϵCèÍ ¾‚Û|ŠP¤>ÍêJ_É­«œÉ“Ñh6ëBz£m©RZ=Ú:ݨÖÚ$.<qê+O q²ŒB4¥àd
+endstream endobj 16 0 obj <</Length 8515>>stream
+béM›šøÇÌ=¼"†0¥Ï
+í$x…¯QŸG"~_-½n6ñTÅq4â¹èdÌEeá_ÑÄG§
+Í òH¥åjÎg0æ†ñÌzø)r-ÔÁ¹-ò*˜ñxîë ½‰YìQªÍm1Ú«°N$ª=Îp
+œµrp¡
+ I*¦à£³#s@“ÆVcÇU š …¤9rÍêßÙRÍüÞ=t¥Ì#eäÎ}ip‚ùhuçn©çèã=³¡c‹!Ñ›j/T·iÚd0¡éþ’ÇÜ WéPÞ‡ŸyEMm¨ŠÌ´û€0TW’{¨ÈÔq’¨ung…¨eS‡Õ6²Rª_?jöÕré‘Œtc–æf;ƒ«¡~ÎU7•<Àüqß qZöÆöBõId„ÕIW‘xŽ’\V¡@5NTôs– <£ iÈ•BÜÖ宥ç Z(Ï]"!âÃè.Íõàãuhj o‹Ÿ/KMl~,°o̽g{L.ÅWÿU+×Ü:J›Ð ™ŠA1ÑJ*9Æ-]i > 6ºË%E°¬©­ÞH”ž
+ªÙ'íÀøÜN£½€åZá“tQº½6A<žvÚ%šR¨ÛÒjŠSÃp<
+ÝVYÞîkŒ¡»Å©{»ðǦê°¨mSÂm£FÑc šËÿ©Áì$GÚ8› [š?KZK·SÃ#ﯓê:„û’p
+Ž—ÚÇ–W1Ò wü—÷‹RµE*J"<
+Ó7æ˪µF F„c±g°"Ø%#ÅIfЀÔ'ö%u/¿!°tÁ<c)…? WËx8.ç4Û…r ˆÔU‘YŒ|Ò-¡”ã
+ú‡"ÖUº°†<›á½¾„Âõq 7†(Á_ ë-©D¦V#;
+¹½<:š™ßºsþi¯“R3Nœ¢
+d1#
+uÛThð½åŠé±Òœ\ËL±Røó
+€qW q±‡!K,½è•x÷”]0ôlY Gz ¨¿³¢T€* &X|Ú'Ùø
+jŠŒ+ÉQÑ®;”Ÿ½` R¯.^å`Â>ðž‹^k`øàÝ„:ú“ b`0}ôÅúT¸ê#æœw=T›îõhÓŒOm{!AqM%­Yó–Â-8­ªœõ@¯‰#y1ŽLN a _`%u§
+ÕÍ”ád¹&fËÂ7X¿_ëmÈÀþi‹]$DšB½
+u¥—=ô}y#µ?’w™ÎÕ¡m·xéÒQ\”•‘*[õ¾ê’2.Snˆãê™dßPdŒUt8.'»DplÀ%1Ž<?왈<Ô[Ÿ!kMÒh²öùÛtøƒòóçªuHŒR%Îÿ$ì¤lTgÄ
+Y#7·u¸I;ü?n­D
+!ñ*… ÄG¡48JÌ–¹€Cš‹ƒùcqW}?8Ù †x÷g­¡X7À
+ÁáúßW3=èEäéhyJ©gKLIcuhaÅ<ön<uؤÚþâ7‰A™n¨™§mÜK¢ºÍŒ
+^òIP\ ƒ¥3…”ïÝõkçÇT]ž_Þ•
+¸‰*6ù¿l®VˆŒA/ËÊÚ!:Sñ/`©-pf1Èô­öÿ$%~å0Ió" Ô^íÓh¸GØTP†¹˜Õ¤hÒz2”  G‘TGäaº~UªÚfìJ‚Éߢ‹èG
+.8ꦕ‚I9^ /´6ˆ©ù·ˆ þb»¸ï®@+uL \X‚JöÖVòñ»¤¶ˆ~¾ÍS¬Kþ¯ªaÖˆ9€?£³`"þi"´ˆ”„F-Àa'‚¢i+Èž9ì¡ÐIµ)”Ü8>Ñó L5“R1é¡{§:-åÓ`àÈ¢ë[OÚ'²tÙiîùí‘[ä_¥ƒ¥„‚wâD3÷²+ëæÒ@ŒyO[¾C-ó$ æ¿WYÂÁÿiÿ
+VÜ"ÂG28-Žá/ðÃ3VÔz^šŸ,Ý®slת~i.HÌùxH–bºªÒINŒ%ð¡·
+Ë¥¡’þµ€ñëhé^¨' øÓùÅ%ù`7B\S‘eZ1QLiÈo»mÖ¼(ÚDæÎ͖К¥^÷Kl*™¼gY¯\eMlÊ®Ìñ>* e(߆MÉ^¸ºáŸ®š*–yïw߬…aÖólSsÕš!™i^à©ñ³2 ýÒj…yÉÛ¨ðÇ!W;ùšÿYÜ¡A³£ÂsTi;ecÀð1˧éu
+¡!¹¤°“mÏ
+±Ý_ßÙ-àƒ§"–o9è;8vÀnøí‹tÆ%#amÓž‘”möc©S\`ͨŠ—ºbY-c¤¾Ü@¥«²YªjÞ$z—û/è
+Ç;ÿÅ€BM÷].Œ»4Ï¥NL™‘¨¿Ü DhÕAaI{Pƒ5JS³`’Sí],¤hã:º²¤Ît)2H3ÍzÝ¢/Á¥¿v_§FóšFËË÷Å[v‘`ÜmR҆±šb±$hfêü¹mHûÇæ{…GË¿´¥œ0M¯¾Í=V|‘n‚!]B·²|ª•3:â­ù«zÓƒû÷…_²Ï…q¾ËFŒ£ã†«vpÈzïU]ÙÝqÛM°­ép™`\š7l0y‚
+hZãcÿh ŽvÚ\ÕÉðšt{#®+ýW˜©$3ÍìÂÛ¥· ]ßÑ¥¢(Nñ-¨\oúCl÷»ÃçákÁïåìÉð&’·€à Šew+SP‘õ§l:cê7€ìGÜ
+1G´Š|za ÎT¼Yã×Ç…vŽ(¼ƒ¾­[†hvT
+›Ž—¤BNwíÔÔ±•um% cïw•˜Çˆ¤‰€Ó‹
+ ˜½ª‡¤›ÛGÕÝ’¦ûqÍPD”`9M¹ñ e¦»
+»’~¿9ŒÍˆ ˆ\é¬û~–OUo ÚT(q±dñ‚›?݃&«d» ÆTúYyEÎÕÂ?xã “€÷Ž82­ôm£H4’’`Ækè ù¿”`6'T©l'gŸç;mH²u“r7xrnpØö6Aþ½¦ð×r{Îð˜MEö£Éû·j)ño±þ‹î—˜1Ú³jìXøh4麶:mT”*êq*i땳è"vz»o‰ýP@)p¯~@W›|CÀÏkîÜRʦP4<ÊkÓî«¡˜[å‚îÈU²¿å¹Îú/@Ž:-Î(Û“è%¥­ÆØ­ÓZ‚XÀæ-mЕR0è®!ŽhøZ*[‰–¡âÀ
+H}Šy?άl1SJC¡•ªB"±î…ãy6ˆùíɿrV ¶L.øù!¼(‡D,‘äù“Ùls#kžþÚž®B‡Ú™(ÖÉŽ" 2P/]"SMèÕB¨·§¶Ëþší,{ù†ßwögÃ
+¾½ŠÀû:ìTZ2¼MºÅ'ƒ9a‡|=žaŽÂC[pHÄbüC5…î[7Ðu#qPuã¤T)̦ºE†¸‘.ç8$šuôœòºE Ò[Š§Ç ¸så^Ø/ðòॹvpÔÌÃN߶#ríd¨W ÁÖÇß;‚HwïÄl¥1§~
+ÇìéÊÇ
+endstream endobj 31 0 obj [/Indexed/DeviceRGB 255 32 0 R] endobj 32 0 obj <</Filter[/ASCII85Decode/FlateDecode]/Length 428>>stream
+8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0
+b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup`
+E1r!/,*0[*9.aFIR2&b-C#s<Xl5FH@[<=!#6V)uDBXnIr.F>oRZ7Dl%MLY\.?d>Mn
+6%Q2oYfNRF$$+ON<+]RUJmC0I<jlL.oXisZ;SYU[/7#<&37rclQKqeJe#,UF7Rgb1
+VNWFKf>nDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j<etJICj7e7nPMb=O6S7UOH<
+PO7r\I.Hu&e0d&E<.')fERr/l+*W,)q^D*ai5<uuLX.7g/>$XKrcYp0n+Xl_nU*O(
+l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~>
+endstream endobj 25 0 obj <</Intent 33 0 R/Name(Layer 1)/Type/OCG/Usage 34 0 R>> endobj 33 0 obj [/View/Design] endobj 34 0 obj <</CreatorInfo<</Creator(Adobe Illustrator 27.1)/Subtype/Artwork>>>> endobj 29 0 obj <</AIS false/BM/Normal/CA 1.0/OP false/OPM 1/SA true/SMask/None/Type/ExtGState/ca 1.0/op false>> endobj 28 0 obj [/ICCBased 35 0 R] endobj 35 0 obj <</Filter/FlateDecode/Length 2574/N 3>>stream
+H‰œ–yTSwÇoÉž•°Ãc [€°5la‘QIBHØADED„ª•2ÖmtFOE.®c­Ö}êÒõ0êè8´׎8GNg¦Óïï÷9÷wïïÝß½÷ó
+ 
+V³)gB£0ñiœWו8#©8wÕ©•õ8_Å٥ʨQãüÜ«QÊj@é&»A)/ÇÙgº>'K‚ó
+€x¯Íú·¶Ò-
+¨ꇆ¡Ðnè÷ÐQètº}MA ï —0Óal»Á¾°ŽSàx ¬‚kà&¸^Á£ð>ø0|>_ƒ'á‡ð,ÂG!"F$H:Rˆ”!z¤éF‘Qd?r 9‹\A&‘GÈ ”ˆrQ ¢áhš‹ÊÑ´íE‡Ñ]èaô4zBgÐ×Á–àE#H ‹*B=¡‹0HØIøˆp†p0MxJ$ùD1„˜D, V›‰½Ä­ÄÄãÄKÄ»ÄY‰dEò"EÒI2’ÔEÚBÚGúŒt™4MzN¦‘Èþär!YKî ’÷?%_&ß#¿¢°(®”0J:EAi¤ôQÆ(Ç()Ó”WT6U@ æP+¨íÔ!ê~êêmêæD ¥eÒÔ´å´!ÚïhŸÓ¦h/èº']B/¢éëèÒÓ¿¢?a0nŒhF!ÃÀXÇØÍ8ÅøšñÜŒkæc&5S˜µ™˜6»lö˜Iaº2c˜K™MÌAæ!æEæ#…åÆ’°d¬VÖë(ëk–Íe‹Øél »—½‡}Ž}ŸCâ¸qâ9
+N'çÎ)Î].ÂuæJ¸rî
+î÷ wšGä xR^¯‡÷[ÞoÆœchžgÞ`>bþ‰ù$á»ñ¥ü*~ÿ ÿ:ÿ¥…EŒ…ÒbÅ~‹ËÏ,m,£-•–Ý–,¯Y¾´Â¬â­*­6X[ݱF­=­3­ë­·YŸ±~dó ·‘ÛtÛ´¹i ÛzÚfÙ6Û~`{ÁvÖÎÞ.ÑNg·Åî”Ý#{¾}´}…ý€ý§ö¸‘j‡‡ÏþŠ™c1X6„Æfm“Ž;'_9 œr:œ8Ýq¦:‹ËœœO:ϸ8¸¤¹´¸ìu¹éJq»–»nv=ëúÌMà–ï¶ÊmÜí¾ÀR 4 ö
+n»3Ü£ÜkÜGݯz=Ä•[=¾ô„=ƒ<Ë=G</zÁ^Á^j¯­^—¼ Þ¡ÞZïQïBº0FX'Ü+œòáû¤útøŒû<öuñ-ôÝà{Ö÷µ__•ß˜ß-G”,ê}çïé/÷ñ¿ÀHh 8ðm W 2p[àŸƒ¸AiA«‚Ný#8$X¼?øAˆKHIÈ{!7Ä<q†¸Wüy(!46´-ôãÐaÁa†°ƒa†W†ï ¿¿@°@¹`lÁݧYÄŽˆÉH,²$òýÈÉ(Ç(YÔhÔ7ÑÎÑŠèÑ÷b<b*böÅ<Žõ‹ÕÇ~ûL&Y&9‡Ä%ÆuÇMÄsâsã‡ã¿NpJP%ìM˜I JlN<žDHJIÚtCj'•KwKg’C’—%ŸN¡§d§ §|“ꙪO=–§%§mL»½Ðu¡váx:H—¦oL¿“!ȨÉøC&13#s$ó/Y¢¬–¬³ÙÜìâì=ÙOsbsúrnåºçsOæ1óŠòvç=ËËïÏŸ\ä»hÙ¢óÖê‚#…¤Â¼Â…³‹ãoZ<]TÔUt}‰`IÃ’sK­—V-ý¤˜Y,+>TB(É/ÙSòƒ,]6*›-•–¾W:#—È7Ë*¢ŠÊe¿ò^YDYÙ}U„j£êAyTù`ù#µD=¬þ¶"©b{ųÊôÊ+¬Ê¯: !kJ4Gµm¥ötµ}uCõ%—®K7YV³©fFŸ¢ßY Õ.©=bàá?SŒîÆ•Æ©ºÈº‘ºçõyõ‡Ø Ú† žkï5%4ý¦m–7Ÿlqlio™Z³lG+ÔZÚz²Í¹­³mzyâò]íÔöÊö?uøuôw|¿"űN»ÎåwW&®ÜÛe֥ﺱ*|ÕöÕèjõê‰5k¶¬yÝ­èþ¢Ç¯g°ç‡^yïkEk‡Öþ¸®lÝD_p߶õÄõÚõ×7DmØÕÏîoê¿»1mãál {àûMśΠnßLÝlÜ<9”úO
+¾„¾ÿ¿z¿õÀpÀìÁgÁãÂ_ÂÛÃXÃÔÄQÄÎÅKÅÈÆFÆÃÇAÇ¿È=ȼÉ:ɹÊ8Ê·Ë6˶Ì5̵Í5͵Î6ζÏ7ϸÐ9кÑ<ѾÒ?ÒÁÓDÓÆÔIÔËÕNÕÑÖUÖØ×\×àØdØèÙlÙñÚvÚûÛ€ÜÜŠÝÝ–ÞÞ¢ß)߯à6à½áDáÌâSâÛãcãëäsäüå„æ æ–çç©è2è¼éFéÐê[êåëpëûì†ííœî(î´ï@ïÌðXðåñrñÿòŒóó§ô4ôÂõPõÞömöû÷Šøø¨ù8ùÇúWúçûwüü˜ý)ýºþKþÜÿmÿÿ
+endstream endobj 26 0 obj [25 0 R] endobj 36 0 obj <</CreationDate(D:20231213190848-03'00')/Creator(Adobe Illustrator 27.1 \(Windows\))/ModDate(D:20231213190848-04'00')/Producer(Adobe PDF library 17.00)/Title(1 icono bug 16x16)>> endobj xref
+0 37
+0000000004 65535 f
+0000000016 00000 n
+0000000147 00000 n
+0000058630 00000 n
+0000000000 00000 f
+0000058681 00000 n
+0000000000 00000 f
+0000000000 00000 f
+0000068738 00000 n
+0000068810 00000 n
+0000069071 00000 n
+0000070621 00000 n
+0000136210 00000 n
+0000201799 00000 n
+0000267388 00000 n
+0000332977 00000 n
+0000398566 00000 n
+0000000000 00000 f
+0000000000 00000 f
+0000000000 00000 f
+0000000000 00000 f
+0000000000 00000 f
+0000000000 00000 f
+0000000000 00000 f
+0000000000 00000 f
+0000407695 00000 n
+0000410679 00000 n
+0000059050 00000 n
+0000407995 00000 n
+0000407882 00000 n
+0000068579 00000 n
+0000407133 00000 n
+0000407181 00000 n
+0000407766 00000 n
+0000407797 00000 n
+0000408030 00000 n
+0000410704 00000 n
+trailer
+<</Size 37/Root 1 0 R/Info 36 0 R/ID[<42761FD87B1C6544A9C765F49F32DD3E><FF8EC1EE870DD14E858387F437A0B9BD>]>>
+startxref
+410899
+%%EOF
diff --git a/contrib/bug-icon/1_bug_icon_16x16.svg b/contrib/bug-icon/1_bug_icon_16x16.svg
new file mode 100644
index 0000000000..ff4db7474f
--- /dev/null
+++ b/contrib/bug-icon/1_bug_icon_16x16.svg
@@ -0,0 +1,266 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 21.333332 21.333332"
+ height="21.333332"
+ width="21.333332"
+ xml:space="preserve"
+ id="svg2"
+ version="1.1"><metadata
+ id="metadata8"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs6"><clipPath
+ id="clipPath18"
+ clipPathUnits="userSpaceOnUse"><path
+ id="path16"
+ d="M 0,0 H 16 V 16 H 0 Z" /></clipPath></defs><g
+ transform="matrix(1.3333333,0,0,-1.3333333,0,21.333333)"
+ id="g10"><g
+ id="g12"><g
+ clip-path="url(#clipPath18)"
+ id="g14"><g
+ transform="translate(4.022,5.7807)"
+ id="g20"><path
+ id="path22"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 C -0.005,-0.002 -0.011,-0.003 -0.017,-0.002 L -0.755,0.109 C -0.771,0.111 -0.783,0.124 -0.784,0.14 l -0.047,0.484 h -0.263 c -0.019,0 -0.034,0.016 -0.034,0.035 0,0.019 0.015,0.034 0.034,0.034 H -0.8 c 0.018,0 0.033,-0.014 0.035,-0.032 L -0.718,0.173 -0.007,0.066 C 0.012,0.063 0.025,0.046 0.022,0.027 0.02,0.014 0.011,0.004 0,0" /></g><g
+ transform="translate(3.3183,4.6576)"
+ id="g24"><path
+ id="path26"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c -0.002,-0.001 -0.004,-0.001 -0.007,-0.002 l -0.138,-0.023 c -0.018,-0.003 -0.036,0.009 -0.039,0.028 -0.004,0.019 0.009,0.037 0.028,0.04 l 0.115,0.019 0.11,0.55 c 0.003,0.014 0.014,0.025 0.028,0.027 l 0.57,0.097 C 0.686,0.74 0.704,0.727 0.707,0.708 0.71,0.69 0.698,0.672 0.679,0.669 L 0.132,0.575 0.021,0.025 C 0.019,0.014 0.011,0.004 0,0" /></g><g
+ transform="translate(3.9726,3.8581)"
+ id="g28"><path
+ id="path30"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c -0.014,-0.007 -0.031,-0.004 -0.042,0.008 -0.012,0.014 -0.011,0.036 0.003,0.049 l 0.088,0.076 -0.192,0.526 c -0.005,0.014 -0.001,0.029 0.01,0.038 L 0.303,1.078 C 0.317,1.09 0.339,1.089 0.351,1.074 0.364,1.06 0.362,1.038 0.348,1.026 L -0.07,0.661 0.122,0.134 C 0.127,0.121 0.123,0.106 0.112,0.096 L 0.007,0.005 C 0.005,0.003 0.002,0.001 0,0" /></g><g
+ transform="translate(5.0103,6.1574)"
+ id="g32"><path
+ id="path34"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 0.005,0.002 0.01,0.005 0.014,0.01 l 0.477,0.574 c 0.01,0.012 0.011,0.03 0.002,0.042 L 0.205,1.019 0.401,1.194 C 0.415,1.207 0.416,1.228 0.404,1.243 0.391,1.257 0.369,1.258 0.355,1.245 L 0.136,1.049 C 0.122,1.037 0.12,1.017 0.131,1.003 L 0.421,0.607 -0.039,0.054 C -0.051,0.04 -0.049,0.018 -0.034,0.006 -0.024,-0.003 -0.011,-0.004 0,0" /></g><g
+ transform="translate(6.3269,4.7555)"
+ id="g36"><path
+ id="path38"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 0.015,0.004 0.026,0.018 0.026,0.034 0,0.019 -0.016,0.034 -0.035,0.034 L -0.125,0.066 -0.332,0.587 C -0.337,0.601 -0.35,0.609 -0.364,0.609 L -0.943,0.603 C -0.962,0.602 -0.977,0.587 -0.977,0.568 c 0,-0.019 0.016,-0.034 0.035,-0.034 L -0.387,0.54 -0.18,0.019 c 0.005,-0.013 0.018,-0.022 0.032,-0.022 l 0.14,0.002 c 0.003,0 0.005,0 0.008,0.001" /></g><g
+ transform="translate(4.0513,6.5954)"
+ id="g40"><path
+ id="path42"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 C -0.008,-0.003 -0.017,-0.003 -0.025,0 L -0.328,0.12 C -0.342,0.125 -0.351,0.138 -0.35,0.152 l 0.004,0.237 c 0,0.019 0.016,0.034 0.035,0.034 0.019,0 0.034,-0.016 0.034,-0.035 L -0.281,0.175 0,0.064 C 0.018,0.057 0.027,0.037 0.02,0.02 0.016,0.01 0.009,0.003 0,0" /></g><g
+ transform="translate(4.4462,6.7459)"
+ id="g44"><path
+ id="path46"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 C 0.008,0.003 0.014,0.009 0.018,0.017 L 0.166,0.308 C 0.172,0.321 0.17,0.336 0.16,0.347 L -0.001,0.521 C -0.014,0.535 -0.036,0.535 -0.05,0.523 -0.064,0.51 -0.064,0.488 -0.051,0.474 L 0.093,0.318 -0.043,0.048 C -0.052,0.031 -0.045,0.01 -0.028,0.001 -0.019,-0.003 -0.009,-0.003 0,0" /></g><g
+ transform="translate(4.6939,6.6224)"
+ id="g48"><path
+ id="path50"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 0.078,-0.206 -0.025,-0.436 -0.231,-0.515 -0.205,-0.078 -0.436,0.025 -0.514,0.231 -0.079,0.206 0.025,0.436 0.23,0.515 C -0.309,0.309 -0.078,0.206 0,0" /></g><g
+ transform="translate(5.3048,5.0195)"
+ id="g52"><path
+ id="path54"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c -0.081,-0.117 -0.192,-0.208 -0.328,-0.26 -0.136,-0.051 -0.279,-0.057 -0.417,-0.024 0,0 -0.308,0.808 0.064,0.95 C -0.308,0.808 0,0 0,0" /></g><g
+ transform="translate(5.3871,5.9087)"
+ id="g56"><path
+ id="path58"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 0.104,-0.274 0.089,-0.559 -0.02,-0.782 -0.018,-0.038 -0.039,-0.073 -0.062,-0.107 -0.308,0.016 -0.545,0.31 -0.545,0.31 0,0 0.014,-0.379 -0.2,-0.594 -0.04,0.009 -0.08,0.022 -0.118,0.038 -0.23,0.094 -0.431,0.297 -0.536,0.571 -0.182,0.478 0.037,0.899 0.446,1.054 C -0.626,0.646 -0.182,0.479 0,0" /></g><g
+ transform="translate(5.2032,5.8241)"
+ id="g60"><path
+ id="path62"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 0.116,-0.305 -0.037,-0.646 -0.341,-0.762 -0.305,-0.116 -0.646,0.037 -0.762,0.342 -0.116,0.304 0.037,0.645 0.341,0.761 C -0.457,0.457 -0.116,0.305 0,0" /></g><g
+ transform="translate(4.5752,6.005)"
+ id="g64"><path
+ id="path66"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 0.164,-0.509 0.09,-0.538 -0.127,-0.048 Z" /></g><g
+ transform="translate(4.8228,5.3149)"
+ id="g68"><path
+ id="path70"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 -0.1,-0.038 -0.038,0.1 0.1,0.038 z" /></g><g
+ transform="translate(6.9544,3.7147)"
+ id="g72"><path
+ id="path74"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c -0.004,-0.002 -0.009,-0.004 -0.015,-0.004 l -0.639,0.012 c -0.014,0 -0.026,0.01 -0.029,0.023 L -0.776,0.437 -1,0.408 c -0.016,-0.002 -0.031,0.009 -0.033,0.026 -0.002,0.016 0.009,0.031 0.026,0.033 l 0.25,0.032 c 0.015,0.002 0.029,-0.008 0.032,-0.022 L -0.63,0.067 -0.013,0.056 C 0.003,0.055 0.016,0.042 0.016,0.025 0.015,0.014 0.009,0.005 0,0" /></g><g
+ transform="translate(6.4819,2.6811)"
+ id="g76"><path
+ id="path78"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 C -0.002,-0.001 -0.003,-0.002 -0.005,-0.002 L -0.12,-0.037 c -0.016,-0.005 -0.032,0.003 -0.037,0.019 -0.005,0.016 0.004,0.032 0.019,0.037 l 0.096,0.029 0.032,0.48 c 0.001,0.012 0.009,0.023 0.021,0.026 L 0.485,0.701 C 0.5,0.706 0.517,0.697 0.522,0.681 0.526,0.666 0.518,0.649 0.502,0.644 L 0.047,0.504 0.015,0.024 C 0.015,0.014 0.009,0.005 0,0" /></g><g
+ transform="translate(7.1277,2.0748)"
+ id="g80"><path
+ id="path82"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 C -0.011,-0.008 -0.026,-0.007 -0.037,0.002 -0.049,0.013 -0.05,0.032 -0.039,0.044 L 0.027,0.118 -0.196,0.544 C -0.201,0.555 -0.2,0.569 -0.191,0.578 L 0.137,0.95 C 0.147,0.962 0.166,0.963 0.178,0.953 0.191,0.942 0.192,0.923 0.181,0.911 L -0.134,0.554 0.089,0.128 C 0.094,0.117 0.093,0.104 0.085,0.094 L 0.005,0.005 C 0.004,0.003 0.002,0.001 0,0" /></g><g
+ transform="translate(7.7523,4.1457)"
+ id="g84"><path
+ id="path86"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 C 0.004,0.002 0.008,0.006 0.011,0.01 L 0.352,0.551 C 0.359,0.563 0.358,0.578 0.349,0.588 L 0.06,0.889 0.207,1.06 C 0.218,1.072 0.216,1.091 0.204,1.102 0.191,1.112 0.173,1.111 0.162,1.098 L -0.002,0.907 c -0.01,-0.012 -0.01,-0.029 0.001,-0.04 L 0.29,0.563 -0.039,0.042 C -0.048,0.028 -0.044,0.01 -0.03,0.001 -0.02,-0.005 -0.009,-0.005 0,0" /></g><g
+ transform="translate(9.0287,3.1014)"
+ id="g88"><path
+ id="path90"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 C 0.012,0.005 0.02,0.018 0.018,0.032 0.016,0.048 0.001,0.059 -0.015,0.057 l -0.099,-0.015 -0.234,0.42 c -0.006,0.011 -0.018,0.017 -0.03,0.015 L -0.869,0.407 C -0.885,0.404 -0.897,0.389 -0.894,0.373 -0.892,0.357 -0.877,0.346 -0.861,0.348 l 0.471,0.068 0.234,-0.42 c 0.006,-0.011 0.018,-0.017 0.031,-0.015 l 0.118,0.017 C -0.004,-0.001 -0.002,-0.001 0,0" /></g><g
+ transform="translate(6.888,4.4106)"
+ id="g92"><path
+ id="path94"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c -0.006,-0.003 -0.014,-0.005 -0.021,-0.003 l -0.272,0.068 c -0.012,0.003 -0.02,0.013 -0.022,0.025 l -0.023,0.202 c -0.001,0.016 0.01,0.031 0.026,0.033 0.017,0.002 0.031,-0.01 0.033,-0.026 L -0.258,0.117 -0.007,0.055 C 0.009,0.051 0.019,0.035 0.015,0.019 0.013,0.01 0.007,0.004 0,0" /></g><g
+ transform="translate(7.2069,4.5828)"
+ id="g96"><path
+ id="path98"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 C 0.006,0.003 0.011,0.009 0.014,0.016 L 0.106,0.28 C 0.11,0.292 0.107,0.305 0.097,0.313 l -0.156,0.13 C -0.072,0.453 -0.09,0.451 -0.101,0.439 -0.111,0.426 -0.109,0.407 -0.097,0.397 L 0.044,0.28 -0.042,0.036 C -0.047,0.02 -0.039,0.003 -0.024,-0.002 -0.016,-0.005 -0.007,-0.004 0,0" /></g><g
+ transform="translate(7.4312,4.5055)"
+ id="g100"><path
+ id="path102"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 0.09,-0.166 0.028,-0.374 -0.138,-0.463 -0.167,-0.09 -0.374,-0.028 -0.464,0.138 -0.089,0.166 -0.028,0.374 0.139,0.463 C -0.297,0.228 -0.09,0.166 0,0" /></g><g
+ transform="translate(8.1302,3.2114)"
+ id="g104"><path
+ id="path106"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c -0.056,-0.108 -0.14,-0.198 -0.25,-0.257 -0.109,-0.06 -0.231,-0.081 -0.352,-0.068 0,0 -0.352,0.652 -0.051,0.815 C -0.352,0.652 0,0 0,0" /></g><g
+ transform="translate(8.1005,3.9765)"
+ id="g108"><path
+ id="path110"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 C 0.119,-0.221 0.138,-0.465 0.071,-0.667 0.06,-0.701 0.046,-0.734 0.03,-0.765 c -0.263,-0.021 -0.499,0.203 -0.499,0.203 0,0 0.055,-0.321 -0.103,-0.528 -0.035,0.004 -0.07,0.01 -0.105,0.019 -0.205,0.055 -0.399,0.204 -0.519,0.425 -0.208,0.387 -0.069,0.769 0.261,0.947 C -0.605,0.479 -0.209,0.386 0,0" /></g><g
+ transform="translate(7.9536,3.8839)"
+ id="g112"><path
+ id="path114"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 0.133,-0.246 0.041,-0.553 -0.205,-0.686 -0.246,-0.133 -0.553,-0.041 -0.686,0.205 -0.133,0.246 -0.041,0.553 0.205,0.686 C -0.44,0.338 -0.133,0.246 0,0" /></g><g
+ transform="translate(7.3995,3.9674)"
+ id="g116"><path
+ id="path118"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 0.197,-0.415 0.136,-0.447 -0.102,-0.055 Z" /></g><g
+ transform="translate(7.6873,3.4084)"
+ id="g120"><path
+ id="path122"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 -0.081,-0.044 -0.043,0.081 0.08,0.044 z" /></g><g
+ transform="translate(4.2747,2.386)"
+ id="g124"><path
+ id="path126"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c -0.004,-0.002 -0.008,-0.003 -0.013,-0.003 l -0.573,0.01 c -0.012,0 -0.023,0.009 -0.025,0.021 l -0.084,0.364 -0.2,-0.026 c -0.015,-0.002 -0.028,0.008 -0.03,0.022 -0.002,0.015 0.008,0.028 0.023,0.03 l 0.224,0.029 c 0.013,0.002 0.026,-0.007 0.029,-0.02 L -0.564,0.06 -0.012,0.05 C 0.003,0.049 0.014,0.037 0.014,0.023 0.014,0.013 0.008,0.004 0,0" /></g><g
+ transform="translate(3.8515,1.4604)"
+ id="g128"><path
+ id="path130"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c -0.002,-0.001 -0.003,-0.001 -0.005,-0.002 l -0.103,-0.032 c -0.014,-0.004 -0.028,0.004 -0.033,0.018 -0.004,0.014 0.004,0.029 0.018,0.033 l 0.085,0.026 0.029,0.43 c 10e-4,0.011 0.008,0.02 0.019,0.023 L 0.434,0.628 C 0.448,0.632 0.463,0.624 0.467,0.61 0.471,0.596 0.464,0.581 0.45,0.577 L 0.043,0.451 0.014,0.021 C 0.013,0.012 0.008,0.004 0,0" /></g><g
+ transform="translate(4.4299,0.9173)"
+ id="g132"><path
+ id="path134"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c -0.01,-0.007 -0.023,-0.006 -0.033,0.002 -0.011,0.01 -0.012,0.026 -0.002,0.037 L 0.024,0.106 -0.175,0.488 C -0.18,0.497 -0.179,0.509 -0.172,0.517 L 0.122,0.851 C 0.132,0.862 0.149,0.863 0.16,0.853 0.171,0.843 0.172,0.827 0.162,0.816 L -0.12,0.496 0.08,0.114 C 0.085,0.105 0.083,0.093 0.076,0.085 L 0.005,0.004 C 0.003,0.003 0.002,0.001 0,0" /></g><g
+ transform="translate(4.9893,2.772)"
+ id="g136"><path
+ id="path138"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 C 0.004,0.002 0.007,0.005 0.01,0.009 L 0.315,0.494 C 0.322,0.504 0.321,0.518 0.312,0.526 L 0.054,0.796 0.185,0.949 C 0.195,0.96 0.194,0.977 0.183,0.987 0.171,0.996 0.155,0.995 0.145,0.984 L -0.002,0.812 C -0.011,0.802 -0.01,0.787 -0.001,0.777 L 0.26,0.505 -0.035,0.037 C -0.043,0.025 -0.039,0.009 -0.027,0.001 -0.018,-0.004 -0.008,-0.004 0,0" /></g><g
+ transform="translate(6.1324,1.8367)"
+ id="g140"><path
+ id="path142"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 C 0.011,0.004 0.018,0.016 0.016,0.028 0.014,0.043 0.001,0.053 -0.014,0.051 L -0.102,0.038 -0.312,0.414 C -0.317,0.423 -0.328,0.429 -0.339,0.427 L -0.779,0.364 C -0.793,0.362 -0.803,0.349 -0.801,0.334 -0.799,0.32 -0.786,0.31 -0.771,0.312 l 0.422,0.06 0.21,-0.376 c 0.005,-0.009 0.016,-0.015 0.027,-0.013 l 0.106,0.015 C -0.004,-0.001 -0.002,-0.001 0,0" /></g><g
+ transform="translate(4.2152,3.0093)"
+ id="g144"><path
+ id="path146"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c -0.006,-0.003 -0.012,-0.004 -0.019,-0.002 l -0.243,0.06 c -0.011,0.003 -0.019,0.012 -0.02,0.023 l -0.02,0.181 c -0.002,0.014 0.008,0.027 0.023,0.029 0.014,0.001 0.028,-0.009 0.029,-0.023 L -0.231,0.105 -0.006,0.049 C 0.008,0.045 0.017,0.031 0.013,0.017 0.011,0.009 0.006,0.003 0,0" /></g><g
+ transform="translate(4.5008,3.1635)"
+ id="g148"><path
+ id="path150"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 C 0.006,0.003 0.01,0.008 0.012,0.015 L 0.095,0.251 C 0.099,0.261 0.096,0.273 0.087,0.28 l -0.14,0.116 C -0.064,0.406 -0.081,0.404 -0.09,0.393 -0.1,0.382 -0.098,0.365 -0.087,0.356 L 0.039,0.251 -0.038,0.032 C -0.042,0.018 -0.035,0.003 -0.021,-0.002 -0.014,-0.004 -0.006,-0.003 0,0" /></g><g
+ transform="translate(4.7017,3.0943)"
+ id="g152"><path
+ id="path154"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 0.08,-0.149 0.025,-0.335 -0.124,-0.415 -0.149,-0.08 -0.335,-0.025 -0.415,0.124 -0.08,0.149 -0.025,0.335 0.124,0.415 C -0.266,0.204 -0.08,0.149 0,0" /></g><g
+ transform="translate(5.3277,1.9352)"
+ id="g156"><path
+ id="path158"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c -0.05,-0.097 -0.125,-0.178 -0.224,-0.231 -0.098,-0.053 -0.207,-0.071 -0.315,-0.06 0,0 -0.315,0.584 -0.046,0.73 C -0.315,0.584 0,0 0,0" /></g><g
+ transform="translate(5.3011,2.6205)"
+ id="g160"><path
+ id="path162"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 0.107,-0.198 0.124,-0.417 0.064,-0.597 -0.011,-0.031 -0.023,-0.06 -0.037,-0.088 -0.236,-0.019 -0.447,0.182 -0.447,0.182 0,0 0.049,-0.288 -0.092,-0.473 -0.032,0.003 -0.063,0.009 -0.094,0.017 -0.184,0.049 -0.358,0.183 -0.465,0.381 C -1.258,-0.232 -1.133,0.11 -0.837,0.27 -0.541,0.429 -0.187,0.346 0,0" /></g><g
+ transform="translate(5.1696,2.5376)"
+ id="g164"><path
+ id="path166"
+ style="fill:#fc0204;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 0.119,-0.22 0.037,-0.495 -0.183,-0.614 -0.221,-0.119 -0.496,-0.037 -0.615,0.183 -0.119,0.22 -0.037,0.495 0.184,0.614 C -0.394,0.302 -0.119,0.22 0,0" /></g><g
+ transform="translate(4.6733,2.6123)"
+ id="g168"><path
+ id="path170"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 0.176,-0.371 0.122,-0.4 -0.092,-0.05 Z" /></g><g
+ transform="translate(4.931,2.1117)"
+ id="g172"><path
+ id="path174"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 -0.072,-0.039 -0.039,0.072 0.072,0.039 z" /></g><g
+ transform="translate(10.5983,13.5956)"
+ id="g176"><path
+ id="path178"
+ style="fill:#292974;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c -0.1,-0.21 -0.136,-0.276 -0.236,-0.234 l -0.007,0.003 h -0.008 c -0.019,-0.001 -0.037,-0.001 -0.056,-0.003 -0.035,0.005 -0.093,0.012 -0.143,0.015 0.021,0.045 0.056,0.106 0.109,0.185 C -0.12,0.303 0.183,0.505 0.365,0.601 0.176,0.369 0.074,0.155 0,0 m 17.206,-10.696 -0.021,0.016 c -0.033,0.022 -3.325,2.248 -4.636,3.731 -0.173,0.196 -0.35,0.422 -0.527,0.666 10e-4,0.017 0,0.034 0,0.052 v 0.015 l -0.01,0.011 c -0.04,0.037 -0.082,0.075 -0.125,0.113 -0.187,0.269 -0.373,0.555 -0.552,0.845 0.226,-0.145 0.453,-0.316 0.684,-0.526 l 0.065,-0.06 -0.006,0.089 c -10e-4,0.007 -0.059,0.777 -0.104,1.107 l -10e-4,0.009 -0.005,0.007 c -0.134,0.191 -0.994,0.713 -1.601,1.067 -0.033,0.062 -0.066,0.123 -0.096,0.182 0.53,-0.149 1.007,-0.44 1.61,-0.873 l 0.068,-0.049 -0.012,0.083 c -10e-4,0.008 -0.117,0.767 -0.219,1.153 l -0.002,0.007 -0.005,0.007 c -0.118,0.148 -0.88,0.528 -1.278,0.719 0.372,-0.074 0.638,-0.171 1.144,-0.396 l 0.075,-0.034 -0.028,0.079 C 11.616,-2.656 10.843,-0.633 7.987,1.04 7.601,1.266 7.227,1.435 6.869,1.56 v 0 C 6.727,1.715 6.469,1.829 6.074,1.919 5.843,1.972 5.614,2.012 5.389,2.04 5.429,2.104 5.469,2.165 5.502,2.207 5.728,2.504 6.094,3.175 6.109,3.203 L 6.128,3.237 4.605,3.852 4.593,3.815 C 4.592,3.81 4.438,3.291 4.077,2.877 3.738,2.489 3.406,2.084 3.355,2.022 2.915,1.955 2.509,1.851 2.144,1.722 1.712,1.679 0.864,1.564 0.218,1.284 -0.128,1.134 -0.458,0.804 -0.627,0.444 -0.709,0.267 -0.727,-0.033 -0.726,-0.261 -0.77,-0.272 -0.808,-0.283 -0.828,-0.289 -1.56,-0.41 -2.248,-0.714 -2.741,-1.146 -3.037,-1.404 -3.291,-1.735 -3.486,-2.105 -3.506,-2.146 -3.527,-2.186 -3.547,-2.228 -3.576,-2.257 -3.591,-2.275 -3.593,-2.276 L -3.597,-2.28 -3.6,-2.286 c -0.005,-0.012 -0.133,-0.315 -0.178,-0.477 -0.05,-0.178 -0.104,-0.535 -0.106,-0.549 l -0.014,-0.101 0.012,0.012 c -0.006,-0.126 0.004,-0.268 0.025,-0.416 -0.31,-0.111 -0.613,-0.263 -0.757,-0.452 -0.618,-0.738 -0.567,-1.514 -0.517,-1.813 0.052,-0.311 0.162,-0.522 0.243,-0.571 0.064,-0.038 0.118,-0.046 0.17,-0.037 0.081,0.014 0.156,0.075 0.241,0.145 0.04,0.035 0.086,0.073 0.139,0.11 0.037,0.027 0.082,0.061 0.135,0.101 0.271,0.205 0.725,0.547 1.168,0.716 0.046,0.018 0.09,0.034 0.131,0.048 0.243,-0.141 0.55,-0.262 0.909,-0.291 0.125,-0.01 0.247,-0.017 0.362,-0.023 l -0.017,-0.009 0.123,-0.003 c 0.55,-0.014 0.945,0.019 1.257,0.071 0.051,0.009 0.098,0.017 0.145,0.026 0.03,0.006 0.061,0.014 0.091,0.02 0.008,0.001 0.016,0.003 0.024,0.006 0.018,0.003 0.035,0.008 0.053,0.012 0.052,0.012 0.102,0.025 0.151,0.038 0.016,0.005 0.032,0.009 0.048,0.014 0.06,0.016 0.118,0.033 0.174,0.048 0.271,0.078 0.527,0.153 0.893,0.176 0.725,0.046 1.616,0.064 2.362,0.038 L 3.436,-6.039 C 3.313,-6.045 2.764,-6.106 2.132,-6.639 1.433,-7.228 1.085,-7.946 1.081,-7.953 l -0.04,-0.085 0.351,0.151 0.007,0.008 c 0.005,0.006 0.536,0.698 0.884,0.904 0.196,0.115 0.409,0.208 0.559,0.268 C 2.61,-6.895 2.21,-7.219 2.103,-7.314 1.921,-7.475 1.696,-7.776 1.687,-7.789 l -0.069,-0.091 0.464,0.159 0.006,0.004 c 0.534,0.444 1.226,0.973 1.845,1.133 0.519,0.121 1.29,-0.201 1.297,-0.205 l 0.041,0.055 -0.239,0.303 -0.01,0.004 C 5.017,-6.425 4.571,-6.265 4.33,-6.138 4.312,-6.129 4.3,-6.113 4.293,-6.089 4.261,-5.971 4.356,-5.724 4.464,-5.502 4.568,-5.513 4.665,-5.526 4.75,-5.541 5.088,-5.601 5.591,-5.62 6.044,-5.623 6.061,-5.628 6.142,-5.652 6.269,-5.689 L 6.153,-6.538 C 6.038,-6.56 5.613,-7.448 6.137,-7.23 c 0.063,0.027 0.117,0.051 0.165,0.073 0.222,0.101 0.334,0.154 0.977,0.172 0.359,0.01 0.608,-0.037 0.61,-0.037 l 0.12,-0.023 -0.267,0.249 -0.006,0.003 c -0.009,0.005 -0.232,0.098 -0.351,0.136 -0.121,0.04 -0.34,0.093 -0.423,0.111 l 0.102,0.628 c 0.681,-0.196 1.586,-0.45 2.336,-0.641 0.204,-0.053 0.407,-0.099 0.604,-0.14 0.431,-0.77 0.909,-1.561 1.356,-2.179 1.274,-1.753 4.515,-5.216 4.548,-5.25 l 0.036,-0.039 0.642,1.439 -2.529,2.712 2.626,-2.231 z" /></g><g
+ transform="translate(8.3941,8.2561)"
+ id="g180"><path
+ id="path182"
+ style="fill:#292974;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c -0.032,-0.099 -0.122,-0.115 -0.269,-0.142 -0.13,-0.023 -0.306,-0.055 -0.558,-0.152 -0.449,-0.172 -0.91,-0.519 -1.185,-0.727 -0.053,-0.04 -0.099,-0.075 -0.137,-0.102 -0.053,-0.038 -0.1,-0.077 -0.141,-0.111 -0.087,-0.072 -0.162,-0.134 -0.244,-0.148 -0.053,-0.01 -0.109,0 -0.173,0.038 -0.083,0.05 -0.194,0.264 -0.247,0.579 -0.051,0.303 -0.102,1.091 0.525,1.839 0.331,0.433 1.467,0.677 1.712,0.673 0.219,-0.005 0.352,-0.334 0.48,-0.653 C -0.216,1.041 -0.195,0.988 -0.173,0.938 -0.024,0.584 0.053,0.163 0,0" /></g><g
+ transform="translate(3.4987,9.1203)"
+ id="g184"><path
+ id="path186"
+ style="fill:#292974;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c -0.143,-0.113 -0.35,-0.165 -0.59,-0.158 -0.037,0.001 -0.075,0.004 -0.114,0.008 -0.252,0.026 -0.531,0.113 -0.802,0.258 -0.026,0.014 -0.053,0.029 -0.079,0.044 -0.044,0.026 -0.087,0.052 -0.13,0.081 -0.221,0.146 -0.409,0.318 -0.555,0.498 -0.025,0.03 -0.048,0.06 -0.07,0.091 -0.027,0.036 -0.051,0.072 -0.074,0.109 -0.024,0.038 -0.046,0.077 -0.067,0.115 -0.094,0.18 -0.144,0.357 -0.141,0.516 0,0.032 0.003,0.062 0.007,0.092 0.012,0.079 0.04,0.152 0.082,0.217 0.09,0.135 0.238,0.22 0.423,0.258 0.049,0.01 0.1,0.017 0.153,0.02 0.036,0.002 0.073,0.003 0.11,0.002 C -1.553,2.145 -1.209,2.049 -0.879,1.865 -0.853,1.851 -0.827,1.836 -0.801,1.82 -0.769,1.801 -0.737,1.781 -0.705,1.76 -0.403,1.56 -0.161,1.311 -0.004,1.058 0.017,1.025 0.036,0.992 0.054,0.959 V 0.958 C 0.221,0.646 0.255,0.338 0.112,0.121 0.1,0.102 0.086,0.085 0.071,0.068 0.05,0.043 0.026,0.02 0,0 M 7.404,-2.984 7.374,-2.97 0.53,0.207 C 0.672,0.788 0.267,1.544 -0.508,2.057 -0.928,2.335 -1.394,2.495 -1.818,2.508 -2.274,2.521 -2.633,2.365 -2.83,2.068 -2.923,1.927 -2.972,1.766 -2.979,1.596 -2.982,1.539 -2.98,1.482 -2.973,1.423 -2.951,1.217 -2.874,1 -2.749,0.788 -2.733,0.761 -2.716,0.734 -2.698,0.707 -2.651,0.635 -2.599,0.563 -2.541,0.493 -2.51,0.457 -2.478,0.42 -2.445,0.384 c 0.15,-0.161 0.329,-0.313 0.533,-0.448 0.062,-0.042 0.126,-0.08 0.19,-0.116 0.026,-0.015 0.053,-0.029 0.079,-0.043 0.214,-0.112 0.434,-0.194 0.651,-0.242 0.035,-0.008 0.07,-0.015 0.105,-0.021 0.096,-0.016 0.192,-0.026 0.285,-0.029 0.031,-0.001 0.061,-0.001 0.091,0 0.032,0 0.063,0.002 0.094,0.004 0.062,0.005 0.121,0.013 0.178,0.025 0.087,0.017 0.167,0.043 0.242,0.075 0.056,0.025 0.109,0.054 0.158,0.088 0.085,0.057 0.158,0.126 0.218,0.207 l 6.83,-3.17 c 0,-10e-4 0,-10e-4 0,-10e-4 l 0.056,-0.026 c 0.015,-0.003 0.029,-0.006 0.045,-0.006 0.099,-10e-4 0.181,0.078 0.182,0.178 0.001,0.067 -0.035,0.126 -0.088,0.157" /></g><g
+ transform="translate(3.8027,9.3432)"
+ id="g188"><path
+ id="path190"
+ style="fill:#455a64;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 -0.01,-0.035 0.003,-0.002 c -0.026,-0.102 -0.085,-0.236 -0.162,-0.376 -0.017,0.087 -0.038,0.173 -0.064,0.258 -0.05,0.165 -0.118,0.323 -0.203,0.473 0.07,0.135 0.132,0.275 0.186,0.418 v 0 c 0.021,0.055 0.04,0.11 0.059,0.166 C 0.002,0.59 0.079,0.267 0,0 m -0.623,-1.053 c -0.025,0.109 -0.057,0.216 -0.098,0.319 -0.047,0.122 -0.106,0.24 -0.175,0.351 0.001,0.001 0.002,0.001 0.002,0.002 0.155,0.189 0.291,0.392 0.409,0.606 0.076,-0.142 0.137,-0.293 0.181,-0.448 0.029,-0.101 0.051,-0.205 0.066,-0.31 C -0.259,-0.566 -0.28,-0.6 -0.301,-0.634 -0.405,-0.794 -0.52,-0.945 -0.623,-1.053 m -0.138,-0.119 c -0.044,-0.029 -0.09,-0.058 -0.135,-0.086 -0.092,-0.058 -0.184,-0.116 -0.272,-0.18 -0.057,0.178 -0.141,0.348 -0.253,0.499 0.045,0.046 0.09,0.091 0.135,0.135 0.031,0.032 0.063,0.063 0.095,0.095 0.046,0.047 0.093,0.095 0.137,0.143 0.034,0.036 0.066,0.073 0.098,0.11 0.054,-0.09 0.101,-0.185 0.141,-0.282 0.051,-0.125 0.089,-0.255 0.116,-0.387 -0.022,-0.018 -0.042,-0.035 -0.062,-0.047 m -0.745,-0.612 c -0.026,-0.042 -0.047,-0.087 -0.067,-0.132 -0.077,0.147 -0.172,0.283 -0.287,0.403 0.03,0.056 0.061,0.111 0.096,0.164 0.081,0.123 0.179,0.236 0.281,0.344 0.108,-0.149 0.189,-0.315 0.24,-0.491 -0.101,-0.082 -0.193,-0.175 -0.263,-0.288 m -0.145,-0.331 c -0.035,-0.097 -0.071,-0.196 -0.12,-0.285 -0.06,-0.107 -0.144,-0.198 -0.241,-0.27 -0.042,0.136 -0.103,0.266 -0.185,0.382 0.047,0.102 0.09,0.207 0.131,0.309 0.051,0.127 0.104,0.256 0.164,0.381 0.117,-0.127 0.211,-0.272 0.283,-0.428 -0.011,-0.03 -0.022,-0.06 -0.032,-0.089 m -0.924,-0.746 c 0.124,0.125 0.237,0.283 0.334,0.478 0.067,-0.105 0.117,-0.22 0.151,-0.339 -0.146,-0.088 -0.316,-0.137 -0.485,-0.139 m -0.118,0.008 c -0.066,0.008 -0.131,0.023 -0.194,0.046 -0.009,0.004 -0.018,0.009 -0.027,0.013 l 0.318,0.771 c 0.006,-0.004 0.013,-0.007 0.019,-0.012 0.107,-0.071 0.199,-0.163 0.277,-0.266 -0.001,-0.003 -0.003,-0.006 -0.004,-0.009 -0.085,-0.178 -0.213,-0.387 -0.389,-0.543 m -0.301,0.098 c -0.038,0.02 -0.074,0.043 -0.107,0.069 -0.052,0.234 -0.051,0.479 0.012,0.711 0.012,0.042 0.026,0.083 0.04,0.125 0.132,-0.023 0.258,-0.065 0.375,-0.128 z m -0.18,0.803 c -0.057,-0.207 -0.067,-0.424 -0.035,-0.636 -0.184,0.195 -0.286,0.471 -0.257,0.738 0.108,0.018 0.218,0.025 0.328,0.014 -0.013,-0.038 -0.026,-0.077 -0.036,-0.116 m -0.152,0.539 c 0.048,0.108 0.095,0.219 0.124,0.335 0.127,0.007 0.254,0.001 0.379,-0.02 -0.042,-0.121 -0.097,-0.239 -0.154,-0.361 -0.045,-0.095 -0.089,-0.192 -0.128,-0.292 -0.115,0.014 -0.231,0.011 -0.345,-0.005 0.026,0.117 0.076,0.232 0.124,0.343 m 0.027,1.026 c 0.187,0.032 0.365,0.046 0.532,0.04 0.022,-0.159 0.035,-0.313 0.015,-0.469 -0.009,-0.067 -0.024,-0.133 -0.043,-0.197 -0.128,0.023 -0.259,0.031 -0.389,0.025 0.004,0.027 0.007,0.053 0.008,0.08 0.008,0.184 -0.051,0.355 -0.123,0.521 m -0.058,0.13 c -0.043,0.093 -0.087,0.189 -0.12,0.285 -0.033,0.096 -0.048,0.194 -0.051,0.293 0.167,0.078 0.344,0.133 0.526,0.163 0.055,0.009 0.11,0.016 0.166,0.02 -0.007,-0.078 -0.011,-0.156 -0.009,-0.233 0.001,-0.013 0.001,-0.026 0.002,-0.038 0.006,-0.138 0.029,-0.276 0.05,-0.41 0.004,-0.027 0.008,-0.053 0.013,-0.08 -0.176,0.005 -0.362,-0.011 -0.557,-0.045 -0.007,0.015 -0.014,0.03 -0.02,0.045 m 0.08,1.458 c 0.007,0.013 0.014,0.025 0.021,0.038 0.107,0.042 0.217,0.075 0.33,0.101 0.114,0.025 0.23,0.042 0.346,0.049 C -2.673,1.198 -2.741,1.009 -2.785,0.823 -2.803,0.746 -2.817,0.67 -2.827,0.594 -2.903,0.588 -2.978,0.579 -3.053,0.565 -3.216,0.536 -3.375,0.487 -3.526,0.42 c 0.016,0.265 0.117,0.533 0.249,0.781 m 0.329,0.521 c 0.11,0.166 0.289,0.272 0.509,0.316 0.087,0.018 0.179,0.026 0.277,0.025 C -2.196,2.018 -2.229,1.972 -2.261,1.926 -2.34,1.812 -2.415,1.695 -2.484,1.574 -2.501,1.543 -2.516,1.512 -2.533,1.481 -2.662,1.476 -2.792,1.458 -2.919,1.431 -3.01,1.412 -3.1,1.387 -3.189,1.357 c 0.079,0.132 0.162,0.255 0.241,0.365 m 0.893,0.336 C -1.767,2.037 -1.445,1.94 -1.132,1.767 L -1.183,1.642 -1.357,1.222 c -0.331,0.174 -0.702,0.261 -1.074,0.261 0.009,0.016 0.016,0.031 0.025,0.047 0.077,0.137 0.164,0.27 0.255,0.398 0.032,0.044 0.063,0.087 0.096,0.13 M -1.601,0.317 C -1.61,0.322 -1.619,0.326 -1.627,0.331 l 0.315,0.764 c 0.32,-0.189 0.585,-0.461 0.774,-0.779 -0.116,-0.219 -0.253,-0.428 -0.409,-0.621 -0.171,0.251 -0.393,0.467 -0.654,0.622 m -0.597,-1.556 c -0.02,0.013 -0.041,0.024 -0.061,0.035 l 0.269,0.655 c 0.174,-0.098 0.327,-0.229 0.453,-0.384 -0.109,-0.114 -0.214,-0.234 -0.301,-0.367 -0.032,-0.048 -0.061,-0.098 -0.088,-0.148 -0.083,0.079 -0.174,0.149 -0.272,0.209 m -0.095,-0.047 c 0.016,-0.01 0.032,-0.019 0.048,-0.028 0.1,-0.062 0.193,-0.135 0.277,-0.216 -0.067,-0.137 -0.125,-0.277 -0.181,-0.416 -0.035,-0.086 -0.071,-0.174 -0.109,-0.261 -0.078,0.094 -0.168,0.178 -0.269,0.246 -0.011,0.008 -0.023,0.015 -0.035,0.022 z m -0.079,0.041 -0.268,-0.649 c -0.117,0.062 -0.244,0.106 -0.375,0.13 0.037,0.09 0.078,0.18 0.119,0.267 0.057,0.124 0.116,0.25 0.161,0.381 0.126,-0.028 0.247,-0.072 0.363,-0.129 m -0.305,0.893 c 0.194,-0.015 0.373,-0.055 0.537,-0.124 0.024,-0.009 0.048,-0.021 0.071,-0.032 l -0.269,-0.654 c -0.118,0.058 -0.242,0.102 -0.37,0.131 0.02,0.067 0.035,0.134 0.044,0.204 0.021,0.159 0.009,0.318 -0.013,0.475 m -0.041,1.06 c 0.043,0.223 0.123,0.452 0.24,0.685 0.376,0.008 0.753,-0.076 1.087,-0.254 L -1.706,0.374 c -0.289,0.147 -0.613,0.222 -0.939,0.225 -0.03,0 -0.061,0 -0.091,-0.001 0.005,0.036 0.011,0.073 0.018,0.11 M -2.754,0.237 c -0.005,0.09 -10e-4,0.18 0.007,0.272 0.058,0.002 0.115,0.002 0.173,-0.001 0.29,-0.012 0.577,-0.085 0.834,-0.217 l -0.149,-0.362 -0.137,-0.332 -0.009,-0.023 c -0.023,0.011 -0.047,0.023 -0.071,0.033 -0.178,0.074 -0.373,0.116 -0.583,0.131 -0.006,0.033 -0.011,0.066 -0.016,0.099 -0.017,0.107 -0.034,0.216 -0.044,0.324 -0.002,0.026 -0.004,0.051 -0.005,0.076 m 1.748,-0.614 c -0.037,-0.044 -0.074,-0.087 -0.113,-0.128 -0.057,-0.062 -0.117,-0.123 -0.177,-0.183 -0.017,-0.017 -0.035,-0.035 -0.053,-0.053 -0.042,-0.042 -0.085,-0.084 -0.127,-0.127 -0.133,0.162 -0.296,0.3 -0.479,0.401 l 0.008,0.021 0.137,0.331 0.149,0.363 c 0.005,-0.003 0.01,-0.005 0.014,-0.008 0.257,-0.151 0.473,-0.365 0.639,-0.613 0.001,-0.001 0.001,-0.003 0.002,-0.004 M -1.054,1.722 C -1.014,1.699 -0.975,1.674 -0.935,1.648 -0.652,1.461 -0.42,1.234 -0.254,0.997 -0.271,0.943 -0.289,0.889 -0.308,0.835 -0.36,0.69 -0.42,0.548 -0.489,0.409 -0.686,0.723 -0.956,0.991 -1.278,1.178 l 0.173,0.42 z m 0.168,0.001 c -0.832,0.55 -1.79,0.572 -2.136,0.05 -0.085,-0.12 -0.177,-0.255 -0.261,-0.4 -0.245,-0.418 -0.435,-0.917 -0.278,-1.374 0.034,-0.1 0.079,-0.198 0.122,-0.293 0.095,-0.205 0.183,-0.399 0.174,-0.61 -0.007,-0.163 -0.073,-0.314 -0.142,-0.473 -0.051,-0.117 -0.104,-0.239 -0.133,-0.367 -0.089,-0.41 0.119,-0.859 0.475,-1.073 0.006,-0.022 0.01,-0.019 0.016,-0.018 0.005,0 0.02,-0.003 0.02,-0.003 0.036,-0.019 0.073,-0.037 0.111,-0.052 0.172,-0.065 0.359,-0.075 0.539,-0.038 0.282,0.057 0.544,0.23 0.686,0.484 0.053,0.095 0.09,0.199 0.126,0.298 0.04,0.113 0.078,0.219 0.137,0.315 0.133,0.217 0.361,0.359 0.581,0.497 0.046,0.029 0.092,0.058 0.137,0.087 0.163,0.107 0.396,0.406 0.569,0.701 0.115,0.196 0.205,0.39 0.23,0.525 l 0.001,0.006 C 0.24,0.519 -0.168,1.248 -0.886,1.723" /></g><g
+ transform="translate(10.1347,13.3765)"
+ id="g192"><path
+ id="path194"
+ style="fill:#222260;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 0,0 1.266,-0.746 3.119,-0.436 1.853,0.311 3.459,2.569 3.459,2.569 L 8.382,1.194 c 0,0 -2.988,-1.757 -4.465,-2.214 -1.478,-0.457 -3.249,0.579 -4.192,0.979 z" /></g><g
+ transform="translate(16.1919,14.6104)"
+ id="g196"><path
+ id="path198"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 1.069,-0.512 -0.243,-0.195 -1.06,0.502 z" /></g><g
+ transform="translate(10.3721,13.3692)"
+ id="g200"><path
+ id="path202"
+ style="fill:#222260;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 -3.67,-3.195 c 0,0 0.054,0.689 0.302,1.137 0,0 0.959,1.094 1.657,1.488 0.699,0.394 1.276,0.577 1.394,0.572 C -0.271,0.001 -0.243,0.01 -0.188,0.01 -0.105,0.01 0,0 0,0" /></g><g
+ transform="translate(15.1905,15.9486)"
+ id="g204"><path
+ id="path206"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 C 0,0 0.206,0.272 0.291,0.419 0.379,0.57 0.516,0.894 0.516,0.894" /></g><g
+ transform="translate(15.3113,15.8571)"
+ id="g208"><path
+ id="path210"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 -0.242,0.183 c 0.002,0.003 0.201,0.266 0.281,0.403 0.082,0.142 0.215,0.455 0.216,0.458 L 0.534,0.926 C 0.529,0.913 0.393,0.592 0.301,0.434 0.212,0.281 0.009,0.011 0,0" /></g></g></g></g></svg> \ No newline at end of file
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 7491068090..7027eafeab 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -19,8 +19,6 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.OptionalSubject.optionals;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.entities.Patch.COMMIT_MSG;
@@ -35,7 +33,6 @@ import static com.google.gerrit.server.project.testing.TestLabels.value;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static org.eclipse.jgit.lib.Constants.HEAD;
@@ -49,15 +46,13 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.primitives.Chars;
import com.google.common.testing.FakeTicker;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.PushOneCommit.Result;
-import com.google.gerrit.acceptance.config.ConfigAnnotationParser;
-import com.google.gerrit.acceptance.config.GerritSystemProperty;
import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
-import com.google.gerrit.acceptance.testsuite.request.SshSessionFactory;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.AccessSection;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
@@ -146,12 +141,11 @@ import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.restapi.change.Revisions;
import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.util.git.DelegateSystemReader;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.FakeEmailSender;
import com.google.gerrit.testing.FakeEmailSender.Message;
-import com.google.gerrit.testing.SshMode;
-import com.google.gerrit.testing.TestTimeUtil;
import com.google.gson.Gson;
import com.google.inject.Inject;
import com.google.inject.Module;
@@ -160,8 +154,6 @@ import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Modifier;
-import java.sql.Timestamp;
-import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -190,10 +182,7 @@ import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.transport.Transport;
-import org.eclipse.jgit.util.FS;
-import org.eclipse.jgit.util.SystemReader;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
@@ -207,20 +196,11 @@ import org.junit.runners.model.Statement;
@RunWith(ConfigSuite.class)
public abstract class AbstractDaemonTest {
- /**
- * Test methods without special annotations will use a common server for efficiency reasons. The
- * server is torn down after the test class is done.
- */
- private static GerritServer commonServer;
-
- private static Description firstTest;
-
@ClassRule public static TemporaryFolder temporaryFolder = new TemporaryFolder();
@ConfigSuite.Parameter public Config baseConfig;
- @ConfigSuite.Name private String configName;
+ @ConfigSuite.Name public String configName;
- @Rule
public TestRule testRunner =
new TestRule() {
@Override
@@ -228,18 +208,12 @@ public abstract class AbstractDaemonTest {
return new Statement() {
@Override
public void evaluate() throws Throwable {
- if (firstTest == null) {
- firstTest = description;
- }
beforeTest(description);
- ProjectResetter.Config input = requireNonNull(resetProjects());
-
try (ProjectResetter resetter =
- projectResetter != null ? projectResetter.builder().build(input) : null) {
- AbstractDaemonTest.this.resetter = resetter;
+ server.createProjectResetter(
+ (allProjects, allUsers) -> resetProjects(allProjects, allUsers))) {
base.evaluate();
} finally {
- AbstractDaemonTest.this.resetter = null;
afterTest();
}
}
@@ -247,11 +221,36 @@ public abstract class AbstractDaemonTest {
}
};
+ protected DaemonTestRule daemonTestRule;
+ protected TestConfigRule configRule;
+ protected ServerTestRule server;
+ protected TimeSettingsTestRule timeSettingsRule;
+
+ @Rule public TestRule topLevelTestRule = createTopLevelTestRule();
+
+ /**
+ * Creates test rules required for tests.
+ *
+ * <p>Creating all rules in a single method gives more flexibility in the order of creation and
+ * allows additional initialization steps if needed.
+ */
+ protected TestRule createTopLevelTestRule() {
+ daemonTestRule = createDaemonTestRule();
+ configRule = daemonTestRule.configRule();
+ timeSettingsRule = daemonTestRule.timeSettingsRule();
+ server = daemonTestRule.server();
+ return daemonTestRule;
+ }
+
+ @UsedAt(UsedAt.Project.GOOGLE)
+ protected DaemonTestRule createDaemonTestRule() {
+ return GerritServerDaemonTestRule.create(this);
+ }
+
@Inject @CanonicalWebUrl protected Provider<String> canonicalWebUrl;
@Inject @GerritPersonIdent protected Provider<PersonIdent> serverIdent;
@Inject @GerritServerConfig protected Config cfg;
@Inject @GerritInstanceId @Nullable protected String instanceId;
- @Inject protected AcceptanceTestRequestScope atrScope;
@Inject protected AccountCache accountCache;
@Inject protected AccountCreator accountCreator;
@Inject protected Accounts accounts;
@@ -273,7 +272,6 @@ public abstract class AbstractDaemonTest {
@Inject protected PatchSetUtil psUtil;
@Inject protected ProjectCache projectCache;
@Inject protected ProjectConfig.Factory projectConfigFactory;
- @Inject protected ProjectResetter.Builder.Factory projectResetter;
@Inject protected Provider<InternalChangeQuery> queryProvider;
@Inject protected PushOneCommit.Factory pushFactory;
@Inject protected PluginConfigFactory pluginConfig;
@@ -283,25 +281,22 @@ public abstract class AbstractDaemonTest {
@Inject protected BatchAbandon batchAbandon;
@Inject protected TestSshKeys sshKeys;
@Inject protected TestTicker testTicker;
+ @Inject protected ThreadLocalRequestContext localCtx;
- protected EventRecorder eventRecorder;
+ @Nullable public SshSession adminSshSession;
- protected GerritServer server;
+ @Nullable public SshSession userSshSession;
+
+ protected EventRecorder eventRecorder;
protected Project.NameKey project;
protected RestSession adminRestSession;
protected RestSession userRestSession;
protected RestSession anonymousRestSession;
- protected SshSession adminSshSession;
- protected SshSession userSshSession;
protected TestAccount admin;
protected TestAccount user;
protected TestRepository<InMemoryRepository> testRepo;
protected String resourcePrefix;
- protected Description description;
- protected GerritServer.Description testMethodDescription;
-
- protected boolean testRequiresSsh;
protected BlockStrategy noSleepBlockStrategy = t -> {}; // Don't sleep in tests.
@Inject private AbstractChangeNotes.Args changeNotesArgs;
@@ -314,54 +309,8 @@ public abstract class AbstractDaemonTest {
@Inject private SitePaths sitePaths;
@Inject private ProjectOperations projectOperations;
- private ProjectResetter resetter;
private List<Repository> toClose;
- private String systemTimeZone;
- private SystemReader oldSystemReader;
- /**
- * The Getters and Setters below are needed for tests that run on custom {@link GerritServer}
- * (that can be set up via {@link #initServer} and {@link #setUpDatabase} methods. Because tests
- * inherit directly from {@link AbstractDaemonTest}, the set up has to be delegated to some other
- * class that can share the set up logic across different test classes.
- *
- * <p>E.g, we need to be able to do something like:
- *
- * <pre>{@code
- * public class AccountIT extends AbstractDaemonTest {...}
- *
- * public class AbstractDaemonTestAdapter {
- *
- * protected void initServer() {...}
- *
- * ...
- *
- * }
- *
- * public class CustomAccountIT extends AccountIT {
- *
- * AbstractDaemonTestAdapter testAdapter;
- *
- * {@literal @Override}
- * protected void initServer() {
- * testAdapter.initServer();
- * }
- * ...
- * }
- *
- * public class CustomChangeIT extends ChangeIT {
- *
- * AbstractDaemonTestAdapter testAdapter;
- *
- * {@literal @Override}
- * protected void initServer() {
- * testAdapter.initServer();
- * }
- * ...
- * }
- *
- * }</pre>
- */
public String getResourcePrefix() {
return resourcePrefix;
}
@@ -370,10 +319,6 @@ public abstract class AbstractDaemonTest {
this.resourcePrefix = resourcePrefix;
}
- public Description getDescription() {
- return description;
- }
-
public TestRepository<InMemoryRepository> getTestRepo() {
return testRepo;
}
@@ -406,14 +351,6 @@ public abstract class AbstractDaemonTest {
this.project = project;
}
- public GerritServer getServer() {
- return server;
- }
-
- public void setServer(GerritServer server) {
- this.server = server;
- }
-
@Before
public void clearSender() {
if (sender != null) {
@@ -428,19 +365,9 @@ public abstract class AbstractDaemonTest {
}
}
- @Before
- public void assumeSshIfRequired() {
- if (testRequiresSsh) {
- // If the test uses ssh, we use assume() to make sure ssh is enabled on
- // the test suite. JUnit will skip tests annotated with @UseSsh if we
- // disable them using the command line flag.
- assume().that(SshMode.useSsh()).isTrue();
- }
- }
-
@After
public void verifyNoPiiInChangeNotes() throws RestApiException, IOException {
- if (testMethodDescription.verifyNoPiiInChangeNotes()) {
+ if (configRule.methodDescription().verifyNoPiiInChangeNotes()) {
verifyNoAccountDetailsInChangeNotes();
}
}
@@ -454,23 +381,18 @@ public abstract class AbstractDaemonTest {
@ConfigSuite.AfterConfig
public static void stopCommonServer() throws Exception {
- if (commonServer != null) {
- try {
- commonServer.close();
- } catch (Exception e) {
- throw new AssertionError(
- "Error stopping common server in "
- + (firstTest != null ? firstTest.getTestClass().getName() : "unknown test class"),
- e);
- } finally {
- commonServer = null;
- }
- }
+ GerritServerTestRule.afterConfigChanged();
}
- /** Controls which project and branches should be reset after each test case. */
- protected ProjectResetter.Config resetProjects() {
- return new ProjectResetter.Config()
+ /**
+ * Controls which project and branches should be reset in the commonServer after each test case.
+ *
+ * <p>Parameters allProjects and allUsers must refer to the commonServer names - if a test doesn't
+ * use commonServer then names in the test can be different from names in commonServer.
+ */
+ protected ProjectResetter.Config resetProjects(
+ AllProjectsName allProjects, AllUsersName allUsers) {
+ return new ProjectResetter.Config.Builder()
// Don't reset all refs so that refs/sequences/changes is not touched and change IDs are
// not reused.
.reset(allProjects, RefNames.REFS_CONFIG)
@@ -483,26 +405,20 @@ public abstract class AbstractDaemonTest {
RefNames.REFS_GROUPNAMES,
RefNames.REFS_GROUPS + "*",
RefNames.REFS_STARRED_CHANGES + "*",
- RefNames.REFS_DRAFT_COMMENTS + "*");
+ RefNames.REFS_DRAFT_COMMENTS + "*")
+ .build();
}
protected void restartAsSlave() throws Exception {
- closeSsh();
- server = GerritServer.restartAsSlave(server);
+ server.restartAsSlave();
server.getTestInjector().injectMembers(this);
- if (resetter != null) {
- server.getTestInjector().injectMembers(resetter);
- }
- initSsh();
+ updateSshSessions();
}
protected void restart() throws Exception {
- server = GerritServer.restart(server, createModule(), createSshModule());
+ server.restart();
server.getTestInjector().injectMembers(this);
- if (resetter != null) {
- server.getTestInjector().injectMembers(resetter);
- }
- initSsh();
+ updateSshSessions();
}
public void reindexAccount(Account.Id accountId) {
@@ -526,49 +442,58 @@ public abstract class AbstractDaemonTest {
protected void beforeTest(Description description) throws Exception {
// SystemReader must be overridden before creating any repos, since they read the user/system
// configs at initialization time, and are then stored in the RepositoryCache forever.
- oldSystemReader = setFakeSystemReader(temporaryFolder.getRoot());
-
- this.description = description;
- GerritServer.Description classDesc =
- GerritServer.Description.forTestClass(description, configName);
- GerritServer.Description methodDesc =
- GerritServer.Description.forTestMethod(description, configName);
- testMethodDescription = methodDesc;
-
- if (methodDesc.systemProperties() != null) {
- ConfigAnnotationParser.parse(methodDesc.systemProperties());
- }
-
- if (methodDesc.systemProperty() != null) {
- ConfigAnnotationParser.parse(methodDesc.systemProperty());
- }
- testRequiresSsh = classDesc.useSshAnnotation() || methodDesc.useSshAnnotation();
- if (!testRequiresSsh) {
- baseConfig.setString("sshd", null, "listenAddress", "off");
+ if (enableExperimentsRejectImplicitMergesOnMerge()) {
+ // When changes are merged/submitted - reject the operation if there is an implicit merge (
+ // even if rejectImplicitMerges is disabled in the project config).
+ baseConfig.setStringList(
+ "experiments",
+ null,
+ "enabled",
+ ImmutableList.of(
+ "GerritBackendFeature__check_implicit_merges_on_merge",
+ "GerritBackendFeature__reject_implicit_merges_on_merge",
+ "GerritBackendFeature__always_reject_implicit_merges_on_merge"));
}
- baseConfig.unset("gerrit", null, "canonicalWebUrl");
- baseConfig.unset("httpd", null, "listenUrl");
-
- baseConfig.setInt("index", null, "batchThreads", -1);
-
- initServer(classDesc, methodDesc);
-
+ server.initServer();
server.getTestInjector().injectMembers(this);
+
Transport.register(inProcessProtocol);
toClose = Collections.synchronizedList(new ArrayList<>());
- setUpDatabase(classDesc);
+ setUpDatabase();
+ server.initSsh();
+ updateSshSessions();
+ }
+
+ void updateSshSessions() throws Exception {
+ userSshSession = null;
+ adminSshSession = null;
+ if (server.sshInitialized()) {
+ try (ManualRequestContext ctx = requestScopeOperations.setNestedApiUser(user.id())) {
+ userSshSession = server.getOrCreateSshSessionForContext(ctx);
+ // The session doesn't store any reference to the context and it remains open after the ctx
+ // is closed.
+ userSshSession.open();
+ }
+
+ try (ManualRequestContext ctx = requestScopeOperations.setNestedApiUser(admin.id())) {
+ adminSshSession = server.getOrCreateSshSessionForContext(ctx);
+ // The session doesn't store any reference to the context and it remains open after the ctx
+ // is closed.
+ adminSshSession.open();
+ }
+ }
+ }
- // Set the clock step last, so that the test setup isn't consuming any timestamps after the
- // clock has been set.
- setTimeSettings(classDesc.useSystemTime(), classDesc.useClockStep(), classDesc.useTimezone());
- setTimeSettings(
- methodDesc.useSystemTime(), methodDesc.useClockStep(), methodDesc.useTimezone());
+ protected boolean enableExperimentsRejectImplicitMergesOnMerge() {
+ // By default any attempt to make an explicit merge is rejected. This allows to check
+ // that existing workflows continue to work even if gerrit rejects implicit merges on merge.
+ return true;
}
- protected void setUpDatabase(GerritServer.Description classDesc) throws Exception {
+ protected void setUpDatabase() throws Exception {
admin = accountCreator.admin();
user = accountCreator.user1();
@@ -576,90 +501,22 @@ public abstract class AbstractDaemonTest {
reindexAccount(admin.id());
reindexAccount(user.id());
- adminRestSession = new RestSession(server, admin);
- userRestSession = new RestSession(server, user);
- anonymousRestSession = new RestSession(server, null);
+ adminRestSession = server.createRestSession(admin);
+ userRestSession = server.createRestSession(user);
+ anonymousRestSession = server.createRestSession(null);
- initSsh();
-
- String testMethodName = description.getMethodName();
+ String testMethodName = configRule.description().getMethodName();
resourcePrefix =
UNSAFE_PROJECT_NAME
- .matcher(description.getClassName() + "_" + testMethodName + "_")
+ .matcher(configRule.description().getClassName() + "_" + testMethodName + "_")
.replaceAll("");
- setRequestScope(admin);
- ProjectInput in = projectInput(description);
+ requestScopeOperations.setApiUser(admin.id());
+ ProjectInput in = projectInput(configRule.description());
gApi.projects().create(in);
project = Project.nameKey(in.name);
- if (!classDesc.skipProjectClone()) {
- testRepo = cloneProject(project, getCloneAsAccount(description));
- }
- }
-
- protected void initServer(GerritServer.Description classDesc, GerritServer.Description methodDesc)
- throws Exception {
- Module module = createModule();
- Module auditModule = createAuditModule();
- Module sshModule = createSshModule();
- if (classDesc.equals(methodDesc) && !classDesc.sandboxed() && !methodDesc.sandboxed()) {
- if (commonServer == null) {
- commonServer =
- GerritServer.initAndStart(
- temporaryFolder, classDesc, baseConfig, module, auditModule, sshModule);
- }
- server = commonServer;
- } else {
- server =
- GerritServer.initAndStart(
- temporaryFolder, methodDesc, baseConfig, module, auditModule, sshModule);
- }
- }
-
- private static SystemReader setFakeSystemReader(File tempDir) {
- SystemReader oldSystemReader = SystemReader.getInstance();
- SystemReader.setInstance(
- new DelegateSystemReader(oldSystemReader) {
- @Override
- public FileBasedConfig openJGitConfig(Config parent, FS fs) {
- return new FileBasedConfig(parent, new File(tempDir, "jgit.config"), FS.detect());
- }
-
- @Override
- public FileBasedConfig openUserConfig(Config parent, FS fs) {
- return new FileBasedConfig(parent, new File(tempDir, "user.config"), FS.detect());
- }
-
- @Override
- public FileBasedConfig openSystemConfig(Config parent, FS fs) {
- return new FileBasedConfig(parent, new File(tempDir, "system.config"), FS.detect());
- }
- });
- return oldSystemReader;
- }
-
- private void setTimeSettings(
- boolean useSystemTime,
- @Nullable UseClockStep useClockStep,
- @Nullable UseTimezone useTimezone) {
- if (useSystemTime) {
- TestTimeUtil.useSystemTime();
- } else if (useClockStep != null) {
- TestTimeUtil.resetWithClockStep(useClockStep.clockStep(), useClockStep.clockStepUnit());
- if (useClockStep.startAtEpoch()) {
- TestTimeUtil.setClock(Timestamp.from(Instant.EPOCH));
- }
- }
- if (useTimezone != null) {
- systemTimeZone = System.setProperty("user.timezone", useTimezone.timezone());
- }
- }
-
- private void resetTimeSettings() {
- TestTimeUtil.useSystemTime();
- if (systemTimeZone != null) {
- System.setProperty("user.timezone", systemTimeZone);
- systemTimeZone = null;
+ if (!configRule.classDescription().skipProjectClone()) {
+ testRepo = cloneProject(project, getCloneAsAccount(configRule.description()));
}
}
@@ -678,23 +535,6 @@ public abstract class AbstractDaemonTest {
return null;
}
- protected void initSsh() throws Exception {
- if (testRequiresSsh
- && SshMode.useSsh()
- && (adminSshSession == null || userSshSession == null)) {
- // Create Ssh sessions
- SshSessionFactory.initSsh();
- Context ctx = newRequestContext(user);
- atrScope.set(ctx);
- userSshSession = ctx.getSession();
- userSshSession.open();
- ctx = newRequestContext(admin);
- atrScope.set(ctx);
- adminSshSession = ctx.getSession();
- adminSshSession.open();
- }
- }
-
protected TestAccount getCloneAsAccount(Description description) {
TestProjectInput ann = description.getAnnotation(TestProjectInput.class);
return accountCreator.get(ann != null ? ann.cloneAs() : "admin");
@@ -754,9 +594,10 @@ public abstract class AbstractDaemonTest {
* @return name prefixed by a string unique to this test method.
*/
protected String name(String name) {
- return resourcePrefix + name;
+ return daemonTestRule.name(name);
}
+ @CanIgnoreReturnValue
protected Project.NameKey createProjectOverAPI(
String nameSuffix,
@Nullable Project.NameKey parent,
@@ -778,7 +619,7 @@ public abstract class AbstractDaemonTest {
protected TestRepository<InMemoryRepository> cloneProject(
Project.NameKey p, TestAccount testAccount) throws Exception {
- return GitUtil.cloneProject(p, registerRepoConnection(p, testAccount));
+ return daemonTestRule.cloneProject(p, testAccount);
}
/**
@@ -786,8 +627,7 @@ public abstract class AbstractDaemonTest {
*
* @return a URI string that can be used to connect to this repository for both fetch and push.
*/
- protected String registerRepoConnection(Project.NameKey p, TestAccount testAccount)
- throws Exception {
+ String registerRepoConnection(Project.NameKey p, TestAccount testAccount) throws Exception {
InProcessProtocol.Context ctx =
new InProcessProtocol.Context(identifiedUserFactory, testAccount.id(), p);
Repository repo = repoManager.openRepository(p);
@@ -800,42 +640,11 @@ public abstract class AbstractDaemonTest {
for (Repository repo : toClose) {
repo.close();
}
- closeSsh();
- resetTimeSettings();
- if (server != commonServer) {
- server.close();
- server = null;
- }
- GerritServer.Description methodDesc =
- GerritServer.Description.forTestMethod(description, configName);
- if (methodDesc.systemProperties() != null) {
- for (GerritSystemProperty sysProp : methodDesc.systemProperties().value()) {
- System.clearProperty(sysProp.name());
- }
- }
-
- if (methodDesc.systemProperty() != null) {
- System.clearProperty(methodDesc.systemProperty().name());
- }
-
- SystemReader.setInstance(oldSystemReader);
- oldSystemReader = null;
// Set useDefaultTicker in afterTest, so the next beforeTest will use the default ticker
testTicker.useDefaultTicker();
}
- protected void closeSsh() {
- if (adminSshSession != null) {
- adminSshSession.close();
- adminSshSession = null;
- }
- if (userSshSession != null) {
- userSshSession.close();
- userSshSession = null;
- }
- }
-
/**
* Verify that NoteDB commits do not persist user-sensitive information, by running checks for all
* commits in {@link RefNames#changeMetaRef} for all changes, created during the test.
@@ -874,7 +683,7 @@ public abstract class AbstractDaemonTest {
if (accountState.userName().isPresent()) {
assertThat(fullMessage).doesNotContain(accountState.userName().get());
}
- List<String> allEmails =
+ ImmutableList<String> allEmails =
accountState.externalIds().stream()
.map(ExternalId::email)
.filter(Objects::nonNull)
@@ -906,10 +715,12 @@ public abstract class AbstractDaemonTest {
return b;
}
+ @CanIgnoreReturnValue
protected PushOneCommit.Result createChange() throws Exception {
return createChange("refs/for/master");
}
+ @CanIgnoreReturnValue
protected PushOneCommit.Result createChange(String ref) throws Exception {
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
PushOneCommit.Result result = push.to(ref);
@@ -917,6 +728,7 @@ public abstract class AbstractDaemonTest {
return result;
}
+ @CanIgnoreReturnValue
protected PushOneCommit.Result createChange(TestRepository<InMemoryRepository> repo)
throws Exception {
PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
@@ -925,10 +737,12 @@ public abstract class AbstractDaemonTest {
return result;
}
+ @CanIgnoreReturnValue
protected PushOneCommit.Result createMergeCommitChange(String ref) throws Exception {
return createMergeCommitChange(ref, "foo");
}
+ @CanIgnoreReturnValue
protected PushOneCommit.Result createMergeCommitChange(String ref, String file) throws Exception {
ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
@@ -962,6 +776,7 @@ public abstract class AbstractDaemonTest {
return result;
}
+ @CanIgnoreReturnValue
protected PushOneCommit.Result createNParentsMergeCommitChange(String ref, List<String> fileNames)
throws Exception {
// This method creates n different commits and creates a merge commit pointing to all n parents.
@@ -1013,6 +828,7 @@ public abstract class AbstractDaemonTest {
return result;
}
+ @CanIgnoreReturnValue
protected PushOneCommit.Result createCommitAndPush(
TestRepository<InMemoryRepository> repo,
String ref,
@@ -1026,6 +842,7 @@ public abstract class AbstractDaemonTest {
return result;
}
+ @CanIgnoreReturnValue
protected PushOneCommit.Result createChangeWithTopic(
TestRepository<InMemoryRepository> repo,
String topic,
@@ -1038,12 +855,14 @@ public abstract class AbstractDaemonTest {
repo, "refs/for/master%topic=" + name(topic), commitMsg, fileName, content);
}
+ @CanIgnoreReturnValue
protected PushOneCommit.Result createChange(String subject, String fileName, String content)
throws Exception {
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content);
return push.to("refs/for/master");
}
+ @CanIgnoreReturnValue
protected PushOneCommit.Result createChange(
TestRepository<?> repo,
String branch,
@@ -1057,6 +876,7 @@ public abstract class AbstractDaemonTest {
"refs/for/" + branch + (Strings.isNullOrEmpty(topic) ? "" : "%topic=" + name(topic)));
}
+ @CanIgnoreReturnValue
protected BranchApi createBranch(BranchNameKey branch) throws Exception {
return gApi.projects()
.name(branch.project().get())
@@ -1064,6 +884,7 @@ public abstract class AbstractDaemonTest {
.create(new BranchInput());
}
+ @CanIgnoreReturnValue
protected BranchApi createBranchWithRevision(BranchNameKey branch, String revision)
throws Exception {
BranchInput in = new BranchInput();
@@ -1074,6 +895,7 @@ public abstract class AbstractDaemonTest {
private static final List<Character> RANDOM =
Chars.asList('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h');
+ @CanIgnoreReturnValue
protected PushOneCommit.Result amendChangeWithUploader(
PushOneCommit.Result change, Project.NameKey projectName, TestAccount account)
throws Exception {
@@ -1092,10 +914,12 @@ public abstract class AbstractDaemonTest {
return result;
}
+ @CanIgnoreReturnValue
protected PushOneCommit.Result amendChange(String changeId) throws Exception {
return amendChange(changeId, "refs/for/master", admin, testRepo);
}
+ @CanIgnoreReturnValue
protected PushOneCommit.Result amendChange(
String changeId, String ref, TestAccount testAccount, TestRepository<?> repo)
throws Exception {
@@ -1110,11 +934,13 @@ public abstract class AbstractDaemonTest {
new String(Chars.toArray(RANDOM)));
}
+ @CanIgnoreReturnValue
protected PushOneCommit.Result amendChange(
String changeId, String subject, String fileName, String content) throws Exception {
return amendChange(changeId, "refs/for/master", admin, testRepo, subject, fileName, content);
}
+ @CanIgnoreReturnValue
protected PushOneCommit.Result amendChange(
String changeId,
String ref,
@@ -1158,17 +984,6 @@ public abstract class AbstractDaemonTest {
return gApi.changes().query(q).get();
}
- /** Sets up {@code account} as a caller in tests. */
- public void setRequestScope(TestAccount account) {
- Context ctx = newRequestContext(account);
- atrScope.set(ctx);
- }
-
- protected Context newRequestContext(TestAccount account) {
- requestScopeOperations.setApiUser(account.id());
- return atrScope.get();
- }
-
protected Account getAccount(Account.Id accountId) {
return getAccountState(accountId).account();
}
@@ -1184,10 +999,8 @@ public abstract class AbstractDaemonTest {
protected AutoCloseable disableNoteDb() {
changeNotesArgs.failOnLoadForTest.set(true);
- Context oldContext = atrScope.disableNoteDb();
return () -> {
changeNotesArgs.failOnLoadForTest.set(false);
- atrScope.set(oldContext);
};
}
@@ -1240,6 +1053,7 @@ public abstract class AbstractDaemonTest {
.update();
}
+ @CanIgnoreReturnValue
protected PushOneCommit.Result pushTo(String ref) throws Exception {
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
return push.to(ref);
@@ -1358,7 +1172,7 @@ public abstract class AbstractDaemonTest {
protected ChangeResource parseChangeResource(String changeId) throws Exception {
List<ChangeNotes> notes = changeFinder.find(changeId);
assertThat(notes).hasSize(1);
- return changeResourceFactory.create(notes.get(0), atrScope.get().getUser());
+ return changeResourceFactory.create(notes.get(0), localCtx.getContext().getUser());
}
protected RevCommit getHead(Repository repo, String name) throws Exception {
@@ -1736,6 +1550,7 @@ public abstract class AbstractDaemonTest {
assertThat(res).isEqualTo(expectedContent);
}
+ @CanIgnoreReturnValue
protected RevCommit createNewCommitWithoutChangeId(String branch, String file, String content)
throws Exception {
try (Repository repo = repoManager.openRepository(project);
@@ -1831,7 +1646,7 @@ public abstract class AbstractDaemonTest {
return new ProjectConfigUpdate(projectName);
}
- protected class ProjectConfigUpdate implements AutoCloseable {
+ public class ProjectConfigUpdate implements AutoCloseable {
private final ProjectConfig projectConfig;
private MetaDataUpdate metaDataUpdate;
@@ -1955,12 +1770,14 @@ public abstract class AbstractDaemonTest {
}
/** Switches to system ticker */
+ @CanIgnoreReturnValue
public Ticker useDefaultTicker() {
this.actualTicker = Ticker.systemTicker();
return actualTicker;
}
/** Switches to {@link FakeTicker} */
+ @CanIgnoreReturnValue
public FakeTicker useFakeTicker() {
if (!(this.actualTicker instanceof FakeTicker)) {
this.actualTicker = new FakeTicker();
diff --git a/java/com/google/gerrit/acceptance/AbstractDynamicOptionsTest.java b/java/com/google/gerrit/acceptance/AbstractDynamicOptionsTest.java
index 4e8d20d6f7..11f2a41db2 100644
--- a/java/com/google/gerrit/acceptance/AbstractDynamicOptionsTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDynamicOptionsTest.java
@@ -82,6 +82,10 @@ public class AbstractDynamicOptionsTest extends AbstractDaemonTest {
}
public static class PluginOneSshModule extends CommandModule {
+ public PluginOneSshModule() {
+ super(/* slaveMode= */ false);
+ }
+
@Override
public void configure() {
command(LS_SAMPLES).to(ListSamplesCommand.class);
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index 8a9e56a911..7681734f6a 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -26,6 +26,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.Subject;
import com.google.common.truth.Truth;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Address;
@@ -41,6 +42,8 @@ import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.testing.FakeEmailSender;
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.inject.Inject;
@@ -67,10 +70,11 @@ public abstract class AbstractNotificationTest extends AbstractDaemonTest {
}
@Override
- protected ProjectResetter.Config resetProjects() {
+ protected ProjectResetter.Config resetProjects(
+ AllProjectsName allProjects, AllUsersName allUsers) {
// Don't reset anything so that stagedUsers can be cached across all tests.
// Without this caching these tests become much too slow.
- return new ProjectResetter.Config();
+ return new ProjectResetter.Config.Builder().build();
}
protected static FakeEmailSenderSubject assertThat(FakeEmailSender sender) {
@@ -109,14 +113,14 @@ public abstract class AbstractNotificationTest extends AbstractDaemonTest {
fakeEmailSender = target;
}
- public FakeEmailSenderSubject didNotSend() {
+ public void didNotSend() {
Message message = fakeEmailSender.peekMessage();
if (message != null) {
failWithoutActual(fact("expected no message", message));
}
- return this;
}
+ @CanIgnoreReturnValue
public FakeEmailSenderSubject sent(String messageType, StagedUsers users) {
message = fakeEmailSender.nextMessage();
if (message == null) {
@@ -183,18 +187,22 @@ public abstract class AbstractNotificationTest extends AbstractDaemonTest {
return addrList.getAddressList().stream().map(Address::email).collect(toList());
}
+ @CanIgnoreReturnValue
public FakeEmailSenderSubject to(String... emails) {
return rcpt(users.supportReviewersByEmail ? TO : null, emails);
}
+ @CanIgnoreReturnValue
public FakeEmailSenderSubject cc(String... emails) {
return rcpt(users.supportReviewersByEmail ? CC : null, emails);
}
+ @CanIgnoreReturnValue
public FakeEmailSenderSubject bcc(String... emails) {
return rcpt(users.supportReviewersByEmail ? BCC : null, emails);
}
+ @CanIgnoreReturnValue
public FakeEmailSenderSubject title(String expectedEmailTitle) {
if (!emailTitle.equals(expectedEmailTitle)) {
failWithoutActual(
@@ -204,6 +212,7 @@ public abstract class AbstractNotificationTest extends AbstractDaemonTest {
return this;
}
+ @CanIgnoreReturnValue
private FakeEmailSenderSubject rcpt(@Nullable RecipientType type, String[] emails) {
for (String email : emails) {
rcpt(type, email);
@@ -230,6 +239,7 @@ public abstract class AbstractNotificationTest extends AbstractDaemonTest {
}
}
+ @CanIgnoreReturnValue
public FakeEmailSenderSubject noOneElse() {
for (Map.Entry<NotifyType, TestAccount> watchEntry : users.watchers.entrySet()) {
if (!accountedFor.contains(watchEntry.getValue().email())) {
@@ -257,22 +267,27 @@ public abstract class AbstractNotificationTest extends AbstractDaemonTest {
return this;
}
+ @CanIgnoreReturnValue
public FakeEmailSenderSubject notTo(String... emails) {
return rcpt(null, emails);
}
+ @CanIgnoreReturnValue
public FakeEmailSenderSubject to(TestAccount... accounts) {
return rcpt(TO, accounts);
}
+ @CanIgnoreReturnValue
public FakeEmailSenderSubject cc(TestAccount... accounts) {
return rcpt(CC, accounts);
}
+ @CanIgnoreReturnValue
public FakeEmailSenderSubject bcc(TestAccount... accounts) {
return rcpt(BCC, accounts);
}
+ @CanIgnoreReturnValue
public FakeEmailSenderSubject notTo(TestAccount... accounts) {
return rcpt(null, accounts);
}
@@ -288,18 +303,22 @@ public abstract class AbstractNotificationTest extends AbstractDaemonTest {
rcpt(type, account.email());
}
+ @CanIgnoreReturnValue
public FakeEmailSenderSubject to(NotifyType... watches) {
return rcpt(TO, watches);
}
+ @CanIgnoreReturnValue
public FakeEmailSenderSubject cc(NotifyType... watches) {
return rcpt(CC, watches);
}
+ @CanIgnoreReturnValue
public FakeEmailSenderSubject bcc(NotifyType... watches) {
return rcpt(BCC, watches);
}
+ @CanIgnoreReturnValue
public FakeEmailSenderSubject notTo(NotifyType... watches) {
return rcpt(null, watches);
}
@@ -350,7 +369,7 @@ public abstract class AbstractNotificationTest extends AbstractDaemonTest {
public boolean supportReviewersByEmail;
private String usersCacheKey() {
- return description.getClassName();
+ return configRule.description().getClassName();
}
private TestAccount reindexAndCopy(TestAccount account) {
@@ -491,10 +510,12 @@ public abstract class AbstractNotificationTest extends AbstractDaemonTest {
}
}
+ @CanIgnoreReturnValue
protected StagedPreChange stagePreChange(String ref) throws Exception {
return new StagedPreChange(ref);
}
+ @CanIgnoreReturnValue
protected StagedPreChange stagePreChange(
String ref, @Nullable PushOptionGenerator pushOptionGenerator) throws Exception {
return new StagedPreChange(ref, pushOptionGenerator);
diff --git a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
index fe845c0612..3dced645e3 100644
--- a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
@@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat;
import static java.util.Objects.requireNonNull;
import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.common.Nullable;
@@ -297,14 +298,15 @@ public class AbstractPluginFieldsTest extends AbstractDaemonTest {
assertThat(getter.call(id).get(id)).isNull();
}
- protected static List<PluginDefinedInfo> pluginInfoFromSingletonList(
+ protected static ImmutableList<PluginDefinedInfo> pluginInfoFromSingletonList(
List<ChangeInfo> changeInfos) {
assertThat(changeInfos).hasSize(1);
return pluginInfoFromChangeInfo(changeInfos.get(0));
}
@Nullable
- protected static List<PluginDefinedInfo> pluginInfoFromChangeInfo(ChangeInfo changeInfo) {
+ protected static ImmutableList<PluginDefinedInfo> pluginInfoFromChangeInfo(
+ ChangeInfo changeInfo) {
List<PluginDefinedInfo> pluginInfo = changeInfo.plugins;
if (pluginInfo == null) {
return null;
diff --git a/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java b/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
deleted file mode 100644
index c4bf20c08a..0000000000
--- a/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
+++ /dev/null
@@ -1,183 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// 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.
-
-package com.google.gerrit.acceptance;
-
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.RequestCleanup;
-import com.google.gerrit.server.util.RequestContext;
-import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
-import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.inject.Inject;
-import com.google.inject.Key;
-import com.google.inject.OutOfScopeException;
-import com.google.inject.Provider;
-import com.google.inject.Scope;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Guice scopes for state during an Acceptance Test connection. */
-public class AcceptanceTestRequestScope {
- private static final Key<RequestCleanup> RC_KEY = Key.get(RequestCleanup.class);
-
- public static class Context implements RequestContext {
- private final RequestCleanup cleanup = new RequestCleanup();
- private final Map<Key<?>, Object> map = new HashMap<>();
- private final SshSession session;
- private final CurrentUser user;
-
- final long created;
- volatile long started;
- volatile long finished;
-
- private Context(SshSession s, CurrentUser u, long at) {
- session = s;
- user = u;
- created = started = finished = at;
- map.put(RC_KEY, cleanup);
- }
-
- private Context(Context p, SshSession s, CurrentUser c) {
- this(s, c, p.created);
- started = p.started;
- finished = p.finished;
- }
-
- public SshSession getSession() {
- return session;
- }
-
- @Override
- public CurrentUser getUser() {
- if (user == null) {
- throw new IllegalStateException("user == null, forgot to set it?");
- }
- return user;
- }
-
- synchronized <T> T get(Key<T> key, Provider<T> creator) {
- @SuppressWarnings("unchecked")
- T t = (T) map.get(key);
- if (t == null) {
- t = creator.get();
- map.put(key, t);
- }
- return t;
- }
- }
-
- static class ContextProvider implements Provider<Context> {
- @Override
- public Context get() {
- return requireContext();
- }
- }
-
- static class SshSessionProvider implements Provider<SshSession> {
- @Override
- public SshSession get() {
- return requireContext().getSession();
- }
- }
-
- static class Propagator extends ThreadLocalRequestScopePropagator<Context> {
- private final AcceptanceTestRequestScope atrScope;
-
- @Inject
- Propagator(AcceptanceTestRequestScope atrScope, ThreadLocalRequestContext local) {
- super(REQUEST, current, local);
- this.atrScope = atrScope;
- }
-
- @Override
- protected Context continuingContext(Context ctx) {
- // The cleanup is not chained, since the RequestScopePropagator executors
- // the Context's cleanup when finished executing.
- return atrScope.newContinuingContext(ctx);
- }
- }
-
- private static final ThreadLocal<Context> current = new ThreadLocal<>();
-
- private static Context requireContext() {
- final Context ctx = current.get();
- if (ctx == null) {
- throw new OutOfScopeException("Not in command/request");
- }
- return ctx;
- }
-
- private final ThreadLocalRequestContext local;
-
- @Inject
- AcceptanceTestRequestScope(ThreadLocalRequestContext local) {
- this.local = local;
- }
-
- public Context newContext(SshSession s, CurrentUser user) {
- return new Context(s, user, TimeUtil.nowMs());
- }
-
- private Context newContinuingContext(Context ctx) {
- return new Context(ctx, ctx.getSession(), ctx.getUser());
- }
-
- public Context set(Context ctx) {
- Context old = current.get();
- current.set(ctx);
- local.setContext(ctx);
- return old;
- }
-
- public Context get() {
- return current.get();
- }
-
- /**
- * Disables read and write access to NoteDb and returns the context prior to that modification.
- */
- public Context disableNoteDb() {
- Context old = current.get();
- Context ctx = new Context(old.session, old.user, old.created);
-
- current.set(ctx);
- local.setContext(ctx);
- return old;
- }
-
- /** Returns exactly one instance per command executed. */
- static final Scope REQUEST =
- new Scope() {
- @Override
- public <T> Provider<T> scope(Key<T> key, Provider<T> creator) {
- return new Provider<>() {
- @Override
- public T get() {
- return requireContext().get(key, creator);
- }
-
- @Override
- public String toString() {
- return String.format("%s[%s]", creator, REQUEST);
- }
- };
- }
-
- @Override
- public String toString() {
- return "Acceptance Test Scope.REQUEST";
- }
- };
-}
diff --git a/java/com/google/gerrit/acceptance/AccountCreator.java b/java/com/google/gerrit/acceptance/AccountCreator.java
index f3881f21d2..8654f82827 100644
--- a/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -14,12 +14,14 @@
package com.google.gerrit.acceptance;
+import static com.google.gerrit.common.UsedAt.Project.GOOGLE;
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.InternalGroup;
@@ -56,7 +58,8 @@ public class AccountCreator {
private final ExternalIdFactory externalIdFactory;
@Inject
- AccountCreator(
+ @UsedAt(GOOGLE)
+ protected AccountCreator(
Sequences sequences,
@ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider,
GroupCache groupCache,
@@ -87,7 +90,7 @@ public class AccountCreator {
List<ExternalId> extIds = new ArrayList<>(2);
String httpPass = null;
if (username != null) {
- httpPass = "http-pass";
+ httpPass = externalIdFactory.arePasswordsAllowed() ? "http-pass" : null;
extIds.add(externalIdFactory.createUsername(username, id, httpPass));
}
@@ -107,14 +110,9 @@ public class AccountCreator {
.addExternalIds(extIds));
ImmutableList.Builder<String> tags = ImmutableList.builder();
+ addUserToGroups(id, groupNames);
if (groupNames != null) {
for (String n : groupNames) {
- AccountGroup.NameKey k = AccountGroup.nameKey(n);
- Optional<InternalGroup> group = groupCache.get(k);
- if (!group.isPresent()) {
- throw new NoSuchGroupException(n);
- }
- addGroupMember(group.get().getGroupUUID(), id);
if (ServiceUserClassifier.SERVICE_USERS.equals(n)) {
tags.add("SERVICE_USER");
}
@@ -129,6 +127,19 @@ public class AccountCreator {
return account;
}
+ protected void addUserToGroups(Account.Id id, String... groupNames) throws Exception {
+ if (groupNames != null) {
+ for (String n : groupNames) {
+ AccountGroup.NameKey k = AccountGroup.nameKey(n);
+ Optional<InternalGroup> group = groupCache.get(k);
+ if (!group.isPresent()) {
+ throw new NoSuchGroupException(n);
+ }
+ addGroupMember(group.get().getGroupUUID(), id);
+ }
+ }
+ }
+
public TestAccount create(@Nullable String username, String group) throws Exception {
return create(username, null, username, null, group);
}
@@ -178,7 +189,7 @@ public class AccountCreator {
return ImmutableList.copyOf(accounts.values());
}
- private void addGroupMember(AccountGroup.UUID groupUuid, Account.Id accountId)
+ protected void addGroupMember(AccountGroup.UUID groupUuid, Account.Id accountId)
throws IOException, NoSuchGroupException, ConfigInvalidException {
GroupDelta groupDelta =
GroupDelta.builder()
diff --git a/java/com/google/gerrit/acceptance/AccountIndexedCounter.java b/java/com/google/gerrit/acceptance/AccountIndexedCounter.java
index 88b97c70a5..17e05591cf 100644
--- a/java/com/google/gerrit/acceptance/AccountIndexedCounter.java
+++ b/java/com/google/gerrit/acceptance/AccountIndexedCounter.java
@@ -52,6 +52,11 @@ public class AccountIndexedCounter implements AccountIndexedListener {
countsByAccount.remove(accountId.get());
}
+ public void assertReindexAtLeastOnceOf(Account.Id accountId) {
+ assertThat(countsByAccount.asMap().getOrDefault(accountId.get(), 0L)).isAtLeast(1);
+ countsByAccount.remove(accountId.get());
+ }
+
public void assertNoReindex() {
assertThat(countsByAccount.asMap()).isEmpty();
}
diff --git a/java/com/google/gerrit/acceptance/ChangeIndexedCounter.java b/java/com/google/gerrit/acceptance/ChangeIndexedCounter.java
index 5991646891..98287c8066 100644
--- a/java/com/google/gerrit/acceptance/ChangeIndexedCounter.java
+++ b/java/com/google/gerrit/acceptance/ChangeIndexedCounter.java
@@ -41,6 +41,10 @@ public class ChangeIndexedCounter implements ChangeIndexedListener {
return countsByChange.get(info._number);
}
+ public long getTotalCount() {
+ return countsByChange.asMap().values().stream().reduce(0L, Long::sum);
+ }
+
public void assertReindexOf(ChangeInfo info) {
assertReindexOf(info, 1);
}
diff --git a/java/com/google/gerrit/acceptance/DaemonTestRule.java b/java/com/google/gerrit/acceptance/DaemonTestRule.java
new file mode 100644
index 0000000000..fc2a0cc5e7
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/DaemonTestRule.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.common.UsedAt.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.junit.rules.TestRule;
+
+/**
+ * Test rule required to run {@link AbstractDaemonTest}.
+ *
+ * <p>The Google internal implementation uses own infrastructure instead of the {@link
+ * GerritServer}.
+ */
+@UsedAt(Project.GOOGLE)
+public interface DaemonTestRule extends TestRule {
+ TestConfigRule configRule();
+
+ ServerTestRule server();
+
+ TimeSettingsTestRule timeSettingsRule();
+
+ TestRepository<InMemoryRepository> cloneProject(NameKey p, TestAccount testAccount)
+ throws Exception;
+
+ String name(String name);
+}
diff --git a/java/com/google/gerrit/acceptance/EventRecorder.java b/java/com/google/gerrit/acceptance/EventRecorder.java
index 161857308d..702b3b4165 100644
--- a/java/com/google/gerrit/acceptance/EventRecorder.java
+++ b/java/com/google/gerrit/acceptance/EventRecorder.java
@@ -172,7 +172,8 @@ public class EventRecorder {
}
public void assertNoRefUpdatedEvents(String project, String branch) throws Exception {
- getRefUpdatedEvents(project, branch, 0);
+ @SuppressWarnings("unused")
+ var unused = getRefUpdatedEvents(project, branch, 0);
}
public void assertRefUpdatedEvents(String project, String branch, String... expected)
diff --git a/java/com/google/gerrit/acceptance/ExtensionRegistry.java b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
index f3527f0950..d2051d5f3e 100644
--- a/java/com/google/gerrit/acceptance/ExtensionRegistry.java
+++ b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
@@ -14,6 +14,7 @@
package com.google.gerrit.acceptance;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.extensions.api.changes.ActionVisitor;
import com.google.gerrit.extensions.config.CapabilityDefinition;
@@ -45,6 +46,7 @@ import com.google.gerrit.extensions.webui.ResolveConflictsWebLink;
import com.google.gerrit.server.ExceptionHook;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.change.ChangeETagComputation;
+import com.google.gerrit.server.change.FilterIncludedIn;
import com.google.gerrit.server.change.ReviewerSuggestion;
import com.google.gerrit.server.config.ProjectConfigEntry;
import com.google.gerrit.server.git.ChangeMessageModifier;
@@ -87,6 +89,7 @@ public class ExtensionRegistry {
private final DynamicSet<GitReferenceUpdatedListener> refUpdatedListeners;
private final DynamicSet<GitBatchRefUpdateListener> batchRefUpdateListeners;
private final DynamicSet<FileHistoryWebLink> fileHistoryWebLinks;
+ private final DynamicSet<FilterIncludedIn> filterIncludedIns;
private final DynamicSet<PatchSetWebLink> patchSetWebLinks;
private final DynamicSet<ResolveConflictsWebLink> resolveConflictsWebLinks;
private final DynamicSet<EditWebLink> editWebLinks;
@@ -133,6 +136,7 @@ public class ExtensionRegistry {
DynamicSet<GitReferenceUpdatedListener> refUpdatedListeners,
DynamicSet<GitBatchRefUpdateListener> batchRefUpdateListeners,
DynamicSet<FileHistoryWebLink> fileHistoryWebLinks,
+ DynamicSet<FilterIncludedIn> filterIncludedIns,
DynamicSet<PatchSetWebLink> patchSetWebLinks,
DynamicSet<ResolveConflictsWebLink> resolveConflictsWebLinks,
DynamicSet<EditWebLink> editWebLinks,
@@ -174,6 +178,7 @@ public class ExtensionRegistry {
this.refUpdatedListeners = refUpdatedListeners;
this.batchRefUpdateListeners = batchRefUpdateListeners;
this.fileHistoryWebLinks = fileHistoryWebLinks;
+ this.filterIncludedIns = filterIncludedIns;
this.patchSetWebLinks = patchSetWebLinks;
this.editWebLinks = editWebLinks;
this.fileWebLinks = fileWebLinks;
@@ -205,172 +210,219 @@ public class ExtensionRegistry {
public class Registration implements AutoCloseable {
private final List<RegistrationHandle> registrationHandles = new ArrayList<>();
+ @CanIgnoreReturnValue
public Registration add(AccountIndexedListener accountIndexedListener) {
return add(accountIndexedListeners, accountIndexedListener);
}
+ @CanIgnoreReturnValue
public Registration add(ChangeIndexedListener changeIndexedListener) {
return add(changeIndexedListeners, changeIndexedListener);
}
+ @CanIgnoreReturnValue
public Registration add(GroupIndexedListener groupIndexedListener) {
return add(groupIndexedListeners, groupIndexedListener);
}
+ @CanIgnoreReturnValue
public Registration add(ProjectIndexedListener projectIndexedListener) {
return add(projectIndexedListeners, projectIndexedListener);
}
+ @CanIgnoreReturnValue
public Registration add(CommitValidationListener commitValidationListener) {
return add(commitValidationListeners, commitValidationListener);
}
+ @CanIgnoreReturnValue
public Registration add(TopicEditedListener topicEditedListener) {
return add(topicEditedListeners, topicEditedListener);
}
+ @CanIgnoreReturnValue
public Registration add(ExceptionHook exceptionHook) {
return add(exceptionHooks, exceptionHook);
}
+ @CanIgnoreReturnValue
public Registration add(PerformanceLogger performanceLogger) {
return add(performanceLoggers, performanceLogger);
}
+ @CanIgnoreReturnValue
public Registration add(ProjectCreationValidationListener projectCreationListener) {
return add(projectCreationValidationListeners, projectCreationListener);
}
+ @CanIgnoreReturnValue
public Registration add(SubmitRule submitRule) {
return add(submitRules, submitRule);
}
+ @CanIgnoreReturnValue
public Registration add(SubmitRequirement submitRequirement) {
return add(submitRequirements, submitRequirement);
}
+ @CanIgnoreReturnValue
public Registration add(ChangeHasOperandFactory hasOperand, String exportName) {
return add(hasOperands, hasOperand, exportName);
}
+ @CanIgnoreReturnValue
public Registration add(ChangeIsOperandFactory isOperand, String exportName) {
return add(isOperands, isOperand, exportName);
}
+ @CanIgnoreReturnValue
public Registration add(ChangeMessageModifier changeMessageModifier) {
return add(changeMessageModifiers, changeMessageModifier);
}
+ @CanIgnoreReturnValue
public Registration add(ChangeMessageModifier changeMessageModifier, String exportName) {
return add(changeMessageModifiers, changeMessageModifier, exportName);
}
+ @CanIgnoreReturnValue
public Registration add(ChangeETagComputation changeETagComputation) {
return add(changeETagComputations, changeETagComputation);
}
+ @CanIgnoreReturnValue
public Registration add(ActionVisitor actionVisitor) {
return add(actionVisitors, actionVisitor);
}
+ @CanIgnoreReturnValue
public Registration add(DownloadScheme downloadScheme, String exportName) {
return add(downloadSchemes, downloadScheme, exportName);
}
+ @CanIgnoreReturnValue
public Registration add(RefOperationValidationListener refOperationValidationListener) {
return add(refOperationValidationListeners, refOperationValidationListener);
}
+ @CanIgnoreReturnValue
public Registration add(CommentAddedListener commentAddedListener) {
return add(commentAddedListeners, commentAddedListener);
}
+ @CanIgnoreReturnValue
public Registration add(GitReferenceUpdatedListener refUpdatedListener) {
return add(refUpdatedListeners, refUpdatedListener);
}
+ @CanIgnoreReturnValue
public Registration add(GitBatchRefUpdateListener batchRefUpdateListener) {
return add(batchRefUpdateListeners, batchRefUpdateListener);
}
+ @CanIgnoreReturnValue
public Registration add(FileHistoryWebLink fileHistoryWebLink) {
return add(fileHistoryWebLinks, fileHistoryWebLink);
}
+ @CanIgnoreReturnValue
+ public Registration add(FilterIncludedIn filterIncludedIn) {
+ return add(filterIncludedIns, filterIncludedIn);
+ }
+
+ @CanIgnoreReturnValue
public Registration add(PatchSetWebLink patchSetWebLink) {
return add(patchSetWebLinks, patchSetWebLink);
}
+ @CanIgnoreReturnValue
public Registration add(ResolveConflictsWebLink resolveConflictsWebLink) {
return add(resolveConflictsWebLinks, resolveConflictsWebLink);
}
+ @CanIgnoreReturnValue
public Registration add(EditWebLink editWebLink) {
return add(editWebLinks, editWebLink);
}
+ @CanIgnoreReturnValue
public Registration add(FileWebLink fileWebLink) {
return add(fileWebLinks, fileWebLink);
}
+ @CanIgnoreReturnValue
public Registration add(RevisionCreatedListener revisionCreatedListener) {
return add(revisionCreatedListeners, revisionCreatedListener);
}
+ @CanIgnoreReturnValue
public Registration add(GroupBackend groupBackend) {
return add(groupBackends, groupBackend);
}
+ @CanIgnoreReturnValue
public Registration add(
AccountActivationValidationListener accountActivationValidationListener) {
return add(accountActivationValidationListeners, accountActivationValidationListener);
}
+ @CanIgnoreReturnValue
public Registration add(AccountActivationListener accountDeactivatedListener) {
return add(accountActivationListeners, accountDeactivatedListener);
}
+ @CanIgnoreReturnValue
public Registration add(OnSubmitValidationListener onSubmitValidationListener) {
return add(onSubmitValidationListeners, onSubmitValidationListener);
}
+ @CanIgnoreReturnValue
public Registration add(WorkInProgressStateChangedListener workInProgressStateChangedListener) {
return add(workInProgressStateChangedListeners, workInProgressStateChangedListener);
}
+ @CanIgnoreReturnValue
public Registration add(AttentionSetListener attentionSetListener) {
return add(attentionSetListeners, attentionSetListener);
}
+ @CanIgnoreReturnValue
public Registration add(CapabilityDefinition capabilityDefinition, String exportName) {
return add(capabilityDefinitions, capabilityDefinition, exportName);
}
+ @CanIgnoreReturnValue
public Registration add(
PluginProjectPermissionDefinition pluginProjectPermissionDefinition, String exportName) {
return add(pluginProjectPermissionDefinitions, pluginProjectPermissionDefinition, exportName);
}
+ @CanIgnoreReturnValue
public Registration add(ProjectConfigEntry pluginConfigEntry, String exportName) {
return add(pluginConfigEntries, pluginConfigEntry, exportName);
}
+ @CanIgnoreReturnValue
public Registration add(PluginPushOption pluginPushOption) {
return add(pluginPushOptions, pluginPushOption);
}
+ @CanIgnoreReturnValue
public Registration add(OnPostReview onPostReview) {
return add(onPostReviews, onPostReview);
}
+ @CanIgnoreReturnValue
public Registration add(ReviewerAddedListener reviewerAddedListener) {
return add(reviewerAddedListeners, reviewerAddedListener);
}
+ @CanIgnoreReturnValue
public Registration add(ReviewerDeletedListener reviewerDeletedListener) {
return add(reviewerDeletedListeners, reviewerDeletedListener);
}
+ @CanIgnoreReturnValue
public Registration add(ReviewerSuggestion reviewerSuggestion, String exportName) {
return add(reviewerSuggestions, reviewerSuggestion, exportName);
}
diff --git a/java/com/google/gerrit/acceptance/FakeGroupAuditService.java b/java/com/google/gerrit/acceptance/FakeGroupAuditService.java
index a1c28b943b..433c14935f 100644
--- a/java/com/google/gerrit/acceptance/FakeGroupAuditService.java
+++ b/java/com/google/gerrit/acceptance/FakeGroupAuditService.java
@@ -19,6 +19,7 @@ import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.httpd.GitOverHttpServlet;
import com.google.gerrit.server.AuditEvent;
@@ -77,6 +78,7 @@ public class FakeGroupAuditService extends AuditService {
}
}
+ @CanIgnoreReturnValue
public ImmutableList<HttpAuditEvent> drainHttpAuditEvents() throws Exception {
// Assumes that all HttpAuditEvents are produced by GitOverHttpServlet.
int expectedSize = Ints.checkedCast(httpMetrics.getRequestsStarted() - drainedSoFar.get());
diff --git a/java/com/google/gerrit/acceptance/GerritServerDaemonTestRule.java b/java/com/google/gerrit/acceptance/GerritServerDaemonTestRule.java
new file mode 100644
index 0000000000..4f4d67aa60
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/GerritServerDaemonTestRule.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gerrit.entities.Project.NameKey;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.junit.rules.RuleChain;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/** Implements {@link DaemonTestRule} using {@link GerritServer}. */
+public class GerritServerDaemonTestRule implements DaemonTestRule {
+ public static GerritServerDaemonTestRule create(AbstractDaemonTest test) {
+ TestConfigRule configRule = new TestConfigRule(AbstractDaemonTest.temporaryFolder, test);
+ GerritServerTestRule server =
+ new GerritServerTestRule(
+ configRule,
+ AbstractDaemonTest.temporaryFolder,
+ () -> test.createModule(),
+ () -> test.createAuditModule(),
+ () -> test.createSshModule());
+ TimeSettingsTestRule timeSettingsRule = new TimeSettingsTestRule(configRule);
+ return new GerritServerDaemonTestRule(test, configRule, server, timeSettingsRule);
+ }
+
+ private final RuleChain ruleChain;
+ private final TestConfigRule configRule;
+ private final ServerTestRule server;
+
+ private final TimeSettingsTestRule timeSettingsRule;
+
+ private final AbstractDaemonTest test;
+
+ private GerritServerDaemonTestRule(
+ AbstractDaemonTest test,
+ TestConfigRule configRule,
+ ServerTestRule server,
+ TimeSettingsTestRule timeSettingsRule) {
+ this.configRule = configRule;
+ this.server = server;
+ this.timeSettingsRule = timeSettingsRule;
+ this.test = test;
+ // Set the clock step as almost the last step, so that the test setup isn't consuming any
+ // timestamps after the
+ // clock has been set.
+ ruleChain =
+ RuleChain.outerRule(configRule)
+ .around(server)
+ .around(test.testRunner)
+ .around(timeSettingsRule);
+ }
+
+ @Override
+ public TestConfigRule configRule() {
+ return configRule;
+ }
+
+ @Override
+ public ServerTestRule server() {
+ return server;
+ }
+
+ @Override
+ public TimeSettingsTestRule timeSettingsRule() {
+ return timeSettingsRule;
+ }
+
+ @Override
+ public TestRepository<InMemoryRepository> cloneProject(NameKey p, TestAccount testAccount)
+ throws Exception {
+ return GitUtil.cloneProject(p, test.registerRepoConnection(p, testAccount));
+ }
+
+ @Override
+ public String name(String name) {
+ return test.resourcePrefix + name;
+ }
+
+ @Override
+ public Statement apply(Statement statement, Description description) {
+ return ruleChain.apply(statement, description);
+ }
+}
diff --git a/java/com/google/gerrit/acceptance/GerritServerRestSession.java b/java/com/google/gerrit/acceptance/GerritServerRestSession.java
new file mode 100644
index 0000000000..c2c77fe922
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/GerritServerRestSession.java
@@ -0,0 +1,148 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.acceptance;
+
+import static com.google.common.net.HttpHeaders.ACCEPT;
+import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
+import static com.google.gerrit.json.OutputFormat.JSON_COMPACT;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.restapi.RawInput;
+import java.io.IOException;
+import org.apache.http.Header;
+import org.apache.http.client.fluent.Request;
+import org.apache.http.entity.BufferedHttpEntity;
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.message.BasicHeader;
+
+/** Sends requests to {@link GerritServer} as a specified user. */
+public class GerritServerRestSession extends HttpSession implements RestSession {
+
+ public GerritServerRestSession(GerritServer server, @Nullable TestAccount account) {
+ super(server, account);
+ }
+
+ @Override
+ public RestResponse get(String endPoint) throws IOException {
+ return getWithHeaders(endPoint);
+ }
+
+ @Override
+ public RestResponse getJsonAccept(String endPoint) throws IOException {
+ return getWithHeaders(endPoint, new BasicHeader(ACCEPT, "application/json"));
+ }
+
+ @Override
+ public RestResponse getWithHeaders(String endPoint, Header... headers) throws IOException {
+ Request get = Request.Get(getUrl(endPoint));
+ if (headers != null) {
+ get.setHeaders(headers);
+ }
+ return execute(get);
+ }
+
+ @Override
+ public RestResponse head(String endPoint) throws IOException {
+ return execute(Request.Head(getUrl(endPoint)));
+ }
+
+ @Override
+ public RestResponse put(String endPoint) throws IOException {
+ return put(endPoint, /* content = */ null);
+ }
+
+ @Override
+ public RestResponse put(String endPoint, Object content) throws IOException {
+ return putWithHeaders(endPoint, content);
+ }
+
+ @Override
+ public RestResponse putWithHeaders(String endPoint, Header... headers) throws IOException {
+ return putWithHeaders(endPoint, /* content= */ null, headers);
+ }
+
+ @Override
+ public RestResponse putWithHeaders(String endPoint, Object content, Header... headers)
+ throws IOException {
+ Request put = Request.Put(getUrl(endPoint));
+ if (headers != null) {
+ put.setHeaders(headers);
+ }
+ if (content != null) {
+ addContentToRequest(put, content);
+ }
+ return execute(put);
+ }
+
+ @Override
+ public RestResponse putRaw(String endPoint, RawInput stream) throws IOException {
+ requireNonNull(stream);
+ Request put = Request.Put(getUrl(endPoint));
+ put.addHeader(new BasicHeader(CONTENT_TYPE, stream.getContentType()));
+ put.body(
+ new BufferedHttpEntity(
+ new InputStreamEntity(stream.getInputStream(), stream.getContentLength())));
+ return execute(put);
+ }
+
+ @Override
+ public RestResponse post(String endPoint) throws IOException {
+ return post(endPoint, /* content = */ null);
+ }
+
+ @Override
+ public RestResponse post(String endPoint, Object content) throws IOException {
+ return postWithHeaders(endPoint, content);
+ }
+
+ @Override
+ public RestResponse postWithHeaders(String endPoint, Object content, Header... headers)
+ throws IOException {
+ Request post = Request.Post(getUrl(endPoint));
+ if (headers != null) {
+ post.setHeaders(headers);
+ }
+ if (content != null) {
+ addContentToRequest(post, content);
+ }
+ return execute(post);
+ }
+
+ private static void addContentToRequest(Request request, Object content) {
+ request.addHeader(new BasicHeader(CONTENT_TYPE, "application/json"));
+ request.body(new StringEntity(JSON_COMPACT.newGson().toJson(content), UTF_8));
+ }
+
+ @Override
+ public RestResponse delete(String endPoint) throws IOException {
+ return execute(Request.Delete(getUrl(endPoint)));
+ }
+
+ @Override
+ public RestResponse deleteWithHeaders(String endPoint, Header... headers) throws IOException {
+ Request delete = Request.Delete(getUrl(endPoint));
+ if (headers != null) {
+ delete.setHeaders(headers);
+ }
+ return execute(delete);
+ }
+
+ private String getUrl(String endPoint) {
+ return url + (account != null ? "/a" : "") + endPoint;
+ }
+}
diff --git a/java/com/google/gerrit/acceptance/GerritServerTestRule.java b/java/com/google/gerrit/acceptance/GerritServerTestRule.java
new file mode 100644
index 0000000000..d3bc008314
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/GerritServerTestRule.java
@@ -0,0 +1,285 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.acceptance;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.truth.TruthJUnit.assume;
+import static java.util.Objects.requireNonNull;
+
+import com.google.gerrit.acceptance.GerritServer.TestSshServerAddress;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
+import com.google.gerrit.acceptance.testsuite.request.SshSessionFactory;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.testing.SshMode;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import java.net.InetSocketAddress;
+import java.util.HashMap;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.function.Supplier;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+public class GerritServerTestRule implements ServerTestRule {
+ /**
+ * Test methods without special annotations will use a common server for efficiency reasons. The
+ * server is torn down after the test class is done or when the config is changed.
+ */
+ private static GerritServer commonServer;
+
+ private static Description firstTest;
+
+ private final TemporaryFolder temporaryFolder;
+ @Nullable private final Supplier<Module> testSysModule;
+ @Nullable private final Supplier<Module> testAuditModule;
+ @Nullable private final Supplier<Module> testSshModule;
+ private final TestConfigRule config;
+
+ @Inject private TestSshKeys sshKeys;
+ @Inject @Nullable @TestSshServerAddress private InetSocketAddress sshAddress;
+
+ @Inject private AccountOperations accountOperations;
+
+ private boolean sshInitialized;
+
+ private final HashMap<RequestContext, SshSession> sshSessionByContext = new HashMap<>();
+
+ public GerritServer server;
+
+ public GerritServerTestRule(
+ TestConfigRule config,
+ TemporaryFolder temporaryFolder,
+ @Nullable Supplier<Module> testSysModule,
+ @Nullable Supplier<Module> testAuditModule,
+ @Nullable Supplier<Module> testSshModule) {
+ this.config = config;
+ this.testSysModule = testSysModule;
+ this.testAuditModule = testAuditModule;
+ this.testSshModule = testSshModule;
+ this.temporaryFolder = temporaryFolder;
+ }
+
+ @Override
+ public Statement apply(Statement statement, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ if (firstTest == null) {
+ firstTest = description;
+ }
+ if (config.testRequiresSsh()) {
+ // If the test uses ssh, we use assume() to make sure ssh is enabled on
+ // the test suite. JUnit will skip tests annotated with @UseSsh if we
+ // disable them using the command line flag.
+ assume().that(SshMode.useSsh()).isTrue();
+ }
+ statement.evaluate();
+ afterTest();
+ }
+ };
+ }
+
+ public void afterTest() throws Exception {
+ closeSsh();
+ if (server != commonServer) {
+ server.close();
+ server = null;
+ }
+ }
+
+ @Override
+ public void initServer() throws Exception {
+
+ if (config.classDescription().equals(config.methodDescription())
+ && !config.classDescription().sandboxed()
+ && !config.methodDescription().sandboxed()) {
+ if (commonServer == null) {
+ commonServer =
+ GerritServer.initAndStart(
+ temporaryFolder,
+ config.classDescription(),
+ config.baseConfig(),
+ testSysModule.get(),
+ testAuditModule.get(),
+ testSshModule.get());
+ }
+ server = commonServer;
+ } else {
+ server =
+ GerritServer.initAndStart(
+ temporaryFolder,
+ config.methodDescription(),
+ config.baseConfig(),
+ testSysModule.get(),
+ testAuditModule.get(),
+ testSshModule.get());
+ }
+ getTestInjector().injectMembers(this);
+ }
+
+ @Override
+ public void initSsh() throws Exception {
+ if (config.testRequiresSsh() && SshMode.useSsh()) {
+ checkState(!sshInitialized, "The ssh has been alread initialized. Call closeSsh first.");
+ // Create Ssh sessions
+ SshSessionFactory.initSsh();
+ sshInitialized = true;
+ }
+ }
+
+ @Override
+ public boolean sshInitialized() {
+ return sshInitialized;
+ }
+
+ @Override
+ public SshSession getOrCreateSshSessionForContext(RequestContext ctx) {
+ checkState(
+ config.testRequiresSsh(),
+ "The test or the test class must be annotated with @UseSsh to use this method.");
+ return sshSessionByContext.computeIfAbsent(
+ ctx,
+ (c) ->
+ SshSessionFactory.createSession(
+ sshKeys,
+ sshAddress,
+ accountOperations.account(ctx.getUser().getAccountId()).get()));
+ }
+
+ /**
+ * This method is only required for some tests and is not a part of interface.
+ *
+ * <p>After restarting the server with this method, the caller can still get exit value of the
+ * last command executed before restarting (using non-closed sessions). This is used in
+ * SshDaemonIT tests.
+ */
+ public void restartKeepSessionOpen() throws Exception {
+ checkState(
+ server != commonServer,
+ "The commonServer can't be restarted; to use this method, the test must be @Sandboxed");
+ server = GerritServer.restart(server, testSysModule.get(), testSshModule.get());
+ getTestInjector().injectMembers(this);
+ }
+
+ @Override
+ public void restart() throws Exception {
+ checkState(
+ server != commonServer,
+ "The commonServer can't be restarted; to use this method, the test must be @Sandboxed");
+ closeSsh();
+ server = GerritServer.restart(server, testSysModule.get(), testSshModule.get());
+ getTestInjector().injectMembers(this);
+ initSsh();
+ }
+
+ @Override
+ public void restartAsSlave() throws Exception {
+ checkState(
+ server != commonServer,
+ "The commonServer can't be restarted; to use this method, the test must be @Sandboxed");
+ closeSsh();
+ server = GerritServer.restartAsSlave(server);
+ getTestInjector().injectMembers(this);
+ initSsh();
+ }
+
+ protected void closeSsh() {
+ sshSessionByContext.values().forEach(SshSession::close);
+ sshSessionByContext.clear();
+ sshInitialized = false;
+ }
+
+ @Override
+ public Injector getTestInjector() {
+ return server.getTestInjector();
+ }
+
+ @Override
+ public Optional<Injector> getHttpdInjector() {
+ return server.getHttpdInjector();
+ }
+
+ @Override
+ public RestSession createRestSession(@Nullable TestAccount account) {
+ return new GerritServerRestSession(server, account);
+ }
+
+ @Nullable
+ @Override
+ public ProjectResetter createProjectResetter(
+ BiFunction<AllProjectsName, AllUsersName, ProjectResetter.Config> resetConfigSupplier)
+ throws Exception {
+ // Only commonServer can be shared between tests and should be restored after each
+ // test. It can be that the commonServer is started, but a test actually don't use
+ // it and instead the test uses a separate server instance.
+ // In this case, the separate server is stopped after each test and so doesn't require
+ // cleanup (for simplicity, the commonServer is always cleaned up even if it is not
+ // used in a test).
+ if (commonServer == null) {
+ return null;
+ }
+ Injector testInjector = commonServer.testInjector;
+ ProjectResetter.Config config =
+ requireNonNull(
+ resetConfigSupplier.apply(
+ testInjector.getInstance(AllProjectsName.class),
+ testInjector.getInstance(AllUsersName.class)));
+ ProjectResetter.Builder.Factory projectResetterFactory =
+ testInjector.getInstance(ProjectResetter.Builder.Factory.class);
+ return projectResetterFactory != null ? projectResetterFactory.builder().build(config) : null;
+ }
+
+ @Override
+ public boolean isReplica() {
+ return server.isReplica();
+ }
+
+ @Override
+ public Optional<InetSocketAddress> getHttpAddress() {
+ return server.getHttpAddress();
+ }
+
+ @Override
+ public String getGitUrl() {
+ return server.getUrl();
+ }
+
+ @Override
+ public boolean isUsernameSupported() {
+ return true;
+ }
+
+ public static void afterConfigChanged() {
+ if (commonServer != null) {
+ try {
+ commonServer.close();
+ } catch (Exception e) {
+ throw new AssertionError(
+ "Error stopping common server in "
+ + (firstTest != null ? firstTest.getTestClass().getName() : "unknown test class"),
+ e);
+ } finally {
+ commonServer = null;
+ }
+ }
+ }
+}
diff --git a/java/com/google/gerrit/acceptance/GitUtil.java b/java/com/google/gerrit/acceptance/GitUtil.java
index 94d329da5b..335e97c891 100644
--- a/java/com/google/gerrit/acceptance/GitUtil.java
+++ b/java/com/google/gerrit/acceptance/GitUtil.java
@@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertWithMessage;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.entities.Project;
import java.io.IOException;
@@ -98,6 +99,7 @@ public class GitUtil {
return testRepo;
}
+ @CanIgnoreReturnValue
public static Ref createAnnotatedTag(TestRepository<?> testRepo, String name, PersonIdent tagger)
throws GitAPIException {
TagCommand cmd =
@@ -105,6 +107,7 @@ public class GitUtil {
return cmd.call();
}
+ @CanIgnoreReturnValue
public static Ref updateAnnotatedTag(TestRepository<?> testRepo, String name, PersonIdent tagger)
throws GitAPIException {
TagCommand tc = testRepo.git().tag().setName(name);
@@ -117,21 +120,25 @@ public class GitUtil {
fetch.call();
}
+ @CanIgnoreReturnValue
public static PushResult pushHead(TestRepository<?> testRepo, String ref) throws GitAPIException {
return pushHead(testRepo, ref, false);
}
+ @CanIgnoreReturnValue
public static PushResult pushHead(TestRepository<?> testRepo, String ref, boolean pushTags)
throws GitAPIException {
return pushHead(testRepo, ref, pushTags, false);
}
+ @CanIgnoreReturnValue
public static PushResult pushHead(
TestRepository<?> testRepo, String ref, boolean pushTags, boolean force)
throws GitAPIException {
return pushOne(testRepo, "HEAD", ref, pushTags, force, null);
}
+ @CanIgnoreReturnValue
public static PushResult pushHead(
TestRepository<?> testRepo,
String ref,
@@ -142,11 +149,13 @@ public class GitUtil {
return pushOne(testRepo, "HEAD", ref, pushTags, force, pushOptions);
}
+ @CanIgnoreReturnValue
public static PushResult deleteRef(TestRepository<?> testRepo, String ref)
throws GitAPIException {
return pushOne(testRepo, "", ref, false, true, null);
}
+ @CanIgnoreReturnValue
public static PushResult pushOne(
TestRepository<?> testRepo,
String source,
diff --git a/java/com/google/gerrit/acceptance/HttpSession.java b/java/com/google/gerrit/acceptance/HttpSession.java
index 833c53b4ff..ef14a3d444 100644
--- a/java/com/google/gerrit/acceptance/HttpSession.java
+++ b/java/com/google/gerrit/acceptance/HttpSession.java
@@ -25,7 +25,7 @@ import org.apache.http.client.fluent.Request;
import org.apache.http.impl.client.HttpClientBuilder;
public class HttpSession {
- protected TestAccount account;
+ protected @Nullable TestAccount account;
protected final String url;
private final Executor executor;
diff --git a/java/com/google/gerrit/acceptance/InProcessProtocol.java b/java/com/google/gerrit/acceptance/InProcessProtocol.java
index 17ce595936..abcc108e4c 100644
--- a/java/com/google/gerrit/acceptance/InProcessProtocol.java
+++ b/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -230,7 +230,9 @@ class InProcessProtocol extends TestProtocol<Context> {
// have an easy way to run code when this instance is done being used.
// Each operation is run in its own thread, so we don't need to recover
// its original context anyway.
- threadContext.setContext(req);
+ @SuppressWarnings("unused")
+ var unused = threadContext.setContext(req);
+
current.set(req);
PermissionBackend.ForProject perm = permissionBackend.currentUser().project(req.project);
@@ -300,7 +302,9 @@ class InProcessProtocol extends TestProtocol<Context> {
// have an easy way to run code when this instance is done being used.
// Each operation is run in its own thread, so we don't need to recover
// its original context anyway.
- threadContext.setContext(req);
+ @SuppressWarnings("unused")
+ var unused = threadContext.setContext(req);
+
current.set(req);
try {
permissionBackend
diff --git a/java/com/google/gerrit/acceptance/ProjectResetter.java b/java/com/google/gerrit/acceptance/ProjectResetter.java
index c8ab1a9ed0..c71641b92a 100644
--- a/java/com/google/gerrit/acceptance/ProjectResetter.java
+++ b/java/com/google/gerrit/acceptance/ProjectResetter.java
@@ -20,6 +20,8 @@ import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction
import static java.util.stream.Collectors.toSet;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Sets;
@@ -137,19 +139,37 @@ public class ProjectResetter implements AutoCloseable {
}
public static class Config {
- private final Multimap<Project.NameKey, String> refsByProject;
+ private final ImmutableMultimap<Project.NameKey, String> refsByProject;
- public Config() {
- this.refsByProject = MultimapBuilder.hashKeys().arrayListValues().build();
+ private Config(ImmutableMultimap<Project.NameKey, String> refsByProject) {
+ this.refsByProject = refsByProject;
}
- public Config reset(Project.NameKey project, String... refPatterns) {
- List<String> refPatternList = Arrays.asList(refPatterns);
- if (refPatternList.isEmpty()) {
- refPatternList = ImmutableList.of(RefNames.REFS + "*");
+ public Builder toBuilder() {
+ Builder builder = new Builder();
+ builder.refsByProject.putAll(refsByProject);
+ return builder;
+ }
+
+ public static class Builder {
+ private final ListMultimap<Project.NameKey, String> refsByProject;
+
+ public Builder() {
+ this.refsByProject = MultimapBuilder.hashKeys().arrayListValues().build();
+ }
+
+ public Builder reset(Project.NameKey project, String... refPatterns) {
+ List<String> refPatternList = Arrays.asList(refPatterns);
+ if (refPatternList.isEmpty()) {
+ refPatternList = ImmutableList.of(RefNames.REFS + "*");
+ }
+ refsByProject.putAll(project, refPatternList);
+ return this;
+ }
+
+ public Config build() {
+ return new Config(ImmutableMultimap.copyOf(refsByProject));
}
- refsByProject.putAll(project, refPatternList);
- return this;
}
}
@@ -166,12 +186,12 @@ public class ProjectResetter implements AutoCloseable {
private final Multimap<Project.NameKey, String> refsPatternByProject;
// State to which to reset to.
- private final Multimap<Project.NameKey, RefState> savedRefStatesByProject;
+ private final ListMultimap<Project.NameKey, RefState> savedRefStatesByProject;
// Results of the resetting
- private Multimap<Project.NameKey, String> keptRefsByProject;
- private Multimap<Project.NameKey, String> restoredRefsByProject;
- private Multimap<Project.NameKey, String> deletedRefsByProject;
+ private ListMultimap<Project.NameKey, String> keptRefsByProject;
+ private ListMultimap<Project.NameKey, String> restoredRefsByProject;
+ private ListMultimap<Project.NameKey, String> deletedRefsByProject;
private ProjectResetter(
GitRepositoryManager repoManager,
@@ -212,13 +232,13 @@ public class ProjectResetter implements AutoCloseable {
}
/** Read the states of all matching refs. */
- private Multimap<Project.NameKey, RefState> readRefStates() throws IOException {
- Multimap<Project.NameKey, RefState> refStatesByProject =
+ private ListMultimap<Project.NameKey, RefState> readRefStates() throws IOException {
+ ListMultimap<Project.NameKey, RefState> refStatesByProject =
MultimapBuilder.hashKeys().arrayListValues().build();
for (Map.Entry<Project.NameKey, Collection<String>> e :
refsPatternByProject.asMap().entrySet()) {
try (Repository repo = repoManager.openRepository(e.getKey())) {
- Collection<Ref> refs = repo.getRefDatabase().getRefs();
+ List<Ref> refs = repo.getRefDatabase().getRefs();
for (String refPattern : e.getValue()) {
RefPatternMatcher matcher = RefPatternMatcher.getMatcher(refPattern);
for (Ref ref : refs) {
@@ -262,7 +282,7 @@ public class ProjectResetter implements AutoCloseable {
for (Map.Entry<Project.NameKey, Collection<String>> e :
refsPatternByProject.asMap().entrySet()) {
try (Repository repo = repoManager.openRepository(e.getKey())) {
- Collection<Ref> nonRestoredRefs =
+ Set<Ref> nonRestoredRefs =
repo.getRefDatabase().getRefs().stream()
.filter(
r ->
diff --git a/java/com/google/gerrit/acceptance/PushOneCommit.java b/java/com/google/gerrit/acceptance/PushOneCommit.java
index a61fa46353..2d5353389c 100644
--- a/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -22,6 +22,7 @@ import static org.junit.Assert.assertEquals;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
@@ -95,6 +96,9 @@ public class PushOneCommit {
PushOneCommit create(PersonIdent i, TestRepository<?> testRepo);
PushOneCommit create(
+ PersonIdent i, TestRepository<?> testRepo, boolean insertChangeIdIfNotExist);
+
+ PushOneCommit create(
PersonIdent i, TestRepository<?> testRepo, @Assisted("changeId") String changeId);
PushOneCommit create(
@@ -172,7 +176,7 @@ public class PushOneCommit {
private final TestRepository<?>.CommitBuilder commitBuilder;
@AssistedInject
- PushOneCommit(
+ public PushOneCommit(
Result.Factory pushResultFactory,
@Assisted PersonIdent i,
@Assisted TestRepository<?> testRepo)
@@ -181,7 +185,24 @@ public class PushOneCommit {
}
@AssistedInject
- PushOneCommit(
+ public PushOneCommit(
+ Result.Factory pushResultFactory,
+ @Assisted PersonIdent i,
+ @Assisted TestRepository<?> testRepo,
+ @Assisted boolean insertChangeIdIfNotExist)
+ throws Exception {
+ this(
+ pushResultFactory,
+ i,
+ testRepo,
+ SUBJECT,
+ ImmutableMap.of(FILE_NAME, FILE_CONTENT),
+ /* changeId= */ null,
+ insertChangeIdIfNotExist);
+ }
+
+ @AssistedInject
+ public PushOneCommit(
Result.Factory pushResultFactory,
@Assisted PersonIdent i,
@Assisted TestRepository<?> testRepo,
@@ -191,7 +212,7 @@ public class PushOneCommit {
}
@AssistedInject
- PushOneCommit(
+ public PushOneCommit(
Result.Factory pushResultFactory,
@Assisted PersonIdent i,
@Assisted TestRepository<?> testRepo,
@@ -203,18 +224,19 @@ public class PushOneCommit {
}
@AssistedInject
- PushOneCommit(
+ public PushOneCommit(
Result.Factory pushResultFactory,
@Assisted PersonIdent i,
@Assisted TestRepository<?> testRepo,
@Assisted String subject,
@Assisted Map<String, String> files)
throws Exception {
- this(pushResultFactory, i, testRepo, subject, files, null);
+ this(
+ pushResultFactory, i, testRepo, subject, files, null, /* insertChangeIdIfNotExist= */ true);
}
@AssistedInject
- PushOneCommit(
+ public PushOneCommit(
Result.Factory pushResultFactory,
@Assisted PersonIdent i,
@Assisted TestRepository<?> testRepo,
@@ -223,11 +245,18 @@ public class PushOneCommit {
@Assisted("content") String content,
@Nullable @Assisted("changeId") String changeId)
throws Exception {
- this(pushResultFactory, i, testRepo, subject, ImmutableMap.of(fileName, content), changeId);
+ this(
+ pushResultFactory,
+ i,
+ testRepo,
+ subject,
+ ImmutableMap.of(fileName, content),
+ changeId,
+ /* insertChangeIdIfNotExist= */ true);
}
@AssistedInject
- PushOneCommit(
+ public PushOneCommit(
Result.Factory pushResultFactory,
@Assisted PersonIdent i,
@Assisted TestRepository<?> testRepo,
@@ -235,6 +264,26 @@ public class PushOneCommit {
@Assisted Map<String, String> files,
@Nullable @Assisted("changeId") String changeId)
throws Exception {
+ this(
+ pushResultFactory,
+ i,
+ testRepo,
+ subject,
+ files,
+ changeId,
+ /* insertChangeIdIfNotExist= */ true);
+ }
+
+ @AssistedInject
+ public PushOneCommit(
+ Result.Factory pushResultFactory,
+ @Assisted PersonIdent i,
+ @Assisted TestRepository<?> testRepo,
+ @Assisted("subject") String subject,
+ @Assisted Map<String, String> files,
+ @Nullable @Assisted("changeId") String changeId,
+ @Assisted boolean insertChangeIdIfNotExist)
+ throws Exception {
this.testRepo = testRepo;
this.subject = subject;
this.files = files;
@@ -242,16 +291,24 @@ public class PushOneCommit {
this.pushResultFactory = pushResultFactory;
if (changeId != null) {
commitBuilder = testRepo.amendRef("HEAD").insertChangeId(changeId.substring(1));
- } else {
+ } else if (insertChangeIdIfNotExist) {
if (subject.contains("\nChange-Id: ")) {
commitBuilder = testRepo.amendRef("HEAD");
} else {
commitBuilder = testRepo.branch("HEAD").commit().insertChangeId(nextChangeId());
}
+ } else {
+ commitBuilder = testRepo.amendRef("HEAD");
}
commitBuilder.message(subject).author(i).committer(new PersonIdent(i, testRepo.getDate()));
}
+ @UsedAt(Project.GOOGLE)
+ protected TestRepository<?> testRepository() {
+ return testRepo;
+ }
+
+ @CanIgnoreReturnValue
public PushOneCommit setParents(List<RevCommit> parents) throws Exception {
commitBuilder.noParents();
for (RevCommit p : parents) {
@@ -266,17 +323,20 @@ public class PushOneCommit {
return this;
}
+ @CanIgnoreReturnValue
public PushOneCommit setParent(RevCommit parent) throws Exception {
commitBuilder.noParents();
commitBuilder.parent(parent);
return this;
}
+ @CanIgnoreReturnValue
public PushOneCommit noParent() {
commitBuilder.noParents();
return this;
}
+ @CanIgnoreReturnValue
public PushOneCommit addFile(String path, String content, int fileMode) throws Exception {
RevBlob blobId = testRepo.blob(content);
commitBuilder.edit(
@@ -290,6 +350,7 @@ public class PushOneCommit {
return this;
}
+ @CanIgnoreReturnValue
public PushOneCommit addSymlink(String path, String target) throws Exception {
RevBlob blobId = testRepo.blob(target);
commitBuilder.edit(
@@ -303,6 +364,7 @@ public class PushOneCommit {
return this;
}
+ @CanIgnoreReturnValue
public PushOneCommit addGitSubmodule(String modulePath, ObjectId commitId) {
commitBuilder.edit(
new PathEdit(modulePath) {
@@ -315,11 +377,13 @@ public class PushOneCommit {
return this;
}
+ @CanIgnoreReturnValue
public PushOneCommit rmFile(String filename) {
commitBuilder.rm(filename);
return this;
}
+ @CanIgnoreReturnValue
public Result to(String ref) throws Exception {
for (Map.Entry<String, String> e : files.entrySet()) {
commitBuilder.add(e.getKey(), e.getValue());
@@ -327,6 +391,7 @@ public class PushOneCommit {
return execute(ref);
}
+ @CanIgnoreReturnValue
public Result rm(String ref) throws Exception {
for (String fileName : files.keySet()) {
commitBuilder.rm(fileName);
@@ -334,10 +399,11 @@ public class PushOneCommit {
return execute(ref);
}
+ @CanIgnoreReturnValue
public Result execute(String ref) throws Exception {
RevCommit c = commitBuilder.create();
if (changeId == null) {
- changeId = GitUtil.getChangeId(testRepo, c).get();
+ changeId = GitUtil.getChangeId(testRepo, c).orElse(null);
}
if (tag != null) {
TagCommand tagCommand = testRepo.git().tag().setName(tag.name);
@@ -387,7 +453,7 @@ public class PushOneCommit {
Result create(
@Assisted("ref") String ref,
@Assisted("subject") String subject,
- @Assisted("changeId") String changeId,
+ @Nullable @Assisted("changeId") String changeId,
@Nullable PushResult resSubj,
@Nullable RevCommit commit,
@Nullable List<String> pushOptions);
@@ -413,7 +479,7 @@ public class PushOneCommit {
Provider<InternalChangeQuery> queryProvider,
@Assisted("ref") String ref,
@Assisted("subject") String subject,
- @Assisted("changeId") String changeId,
+ @Assisted("changeId") @Nullable String changeId,
@Assisted @Nullable PushResult resSubj,
@Assisted @Nullable RevCommit commit,
@Assisted @Nullable List<String> pushOptions) {
@@ -473,7 +539,7 @@ public class PushOneCommit {
private void assertReviewers(
Change c, ReviewerStateInternal state, List<TestAccount> expectedReviewers) {
- Iterable<Account.Id> actualIds =
+ ImmutableSet<Account.Id> actualIds =
approvalsUtil.getReviewers(notesFactory.createChecked(c)).byState(state);
assertThat(actualIds)
.containsExactlyElementsIn(Sets.newHashSet(TestAccount.ids(expectedReviewers)));
diff --git a/java/com/google/gerrit/acceptance/RestResponse.java b/java/com/google/gerrit/acceptance/RestResponse.java
index a045d803db..a9c14aafca 100644
--- a/java/com/google/gerrit/acceptance/RestResponse.java
+++ b/java/com/google/gerrit/acceptance/RestResponse.java
@@ -30,6 +30,8 @@ import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.common.UsedAt.Project;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
@@ -37,7 +39,8 @@ import java.net.URI;
public class RestResponse extends HttpResponse {
- RestResponse(org.apache.http.HttpResponse response) {
+ @UsedAt(Project.GOOGLE)
+ public RestResponse(org.apache.http.HttpResponse response) {
super(response);
}
diff --git a/java/com/google/gerrit/acceptance/RestSession.java b/java/com/google/gerrit/acceptance/RestSession.java
index 342cbd066f..0865e31147 100644
--- a/java/com/google/gerrit/acceptance/RestSession.java
+++ b/java/com/google/gerrit/acceptance/RestSession.java
@@ -11,123 +11,45 @@
// 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.
-
package com.google.gerrit.acceptance;
-import static com.google.common.net.HttpHeaders.ACCEPT;
-import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
-import static com.google.gerrit.json.OutputFormat.JSON_COMPACT;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Objects.requireNonNull;
-
-import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
import com.google.gerrit.extensions.restapi.RawInput;
-import java.io.IOException;
import org.apache.http.Header;
import org.apache.http.client.fluent.Request;
-import org.apache.http.entity.BufferedHttpEntity;
-import org.apache.http.entity.InputStreamEntity;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.message.BasicHeader;
-
-public class RestSession extends HttpSession {
-
- public RestSession(GerritServer server, @Nullable TestAccount account) {
- super(server, account);
- }
-
- public RestResponse get(String endPoint) throws IOException {
- return getWithHeaders(endPoint);
- }
-
- public RestResponse getJsonAccept(String endPoint) throws IOException {
- return getWithHeaders(endPoint, new BasicHeader(ACCEPT, "application/json"));
- }
-
- public RestResponse getWithHeaders(String endPoint, Header... headers) throws IOException {
- Request get = Request.Get(getUrl(endPoint));
- if (headers != null) {
- get.setHeaders(headers);
- }
- return execute(get);
- }
-
- public RestResponse head(String endPoint) throws IOException {
- return execute(Request.Head(getUrl(endPoint)));
- }
-
- public RestResponse put(String endPoint) throws IOException {
- return put(endPoint, /* content = */ null);
- }
-
- public RestResponse put(String endPoint, Object content) throws IOException {
- return putWithHeaders(endPoint, content);
- }
-
- public RestResponse putWithHeaders(String endPoint, Header... headers) throws IOException {
- return putWithHeaders(endPoint, /* content= */ null, headers);
- }
-
- public RestResponse putWithHeaders(String endPoint, Object content, Header... headers)
- throws IOException {
- Request put = Request.Put(getUrl(endPoint));
- if (headers != null) {
- put.setHeaders(headers);
- }
- if (content != null) {
- addContentToRequest(put, content);
- }
- return execute(put);
- }
-
- public RestResponse putRaw(String endPoint, RawInput stream) throws IOException {
- requireNonNull(stream);
- Request put = Request.Put(getUrl(endPoint));
- put.addHeader(new BasicHeader(CONTENT_TYPE, stream.getContentType()));
- put.body(
- new BufferedHttpEntity(
- new InputStreamEntity(stream.getInputStream(), stream.getContentLength())));
- return execute(put);
- }
-
- public RestResponse post(String endPoint) throws IOException {
- return post(endPoint, /* content = */ null);
- }
-
- public RestResponse post(String endPoint, Object content) throws IOException {
- return postWithHeaders(endPoint, content);
- }
-
- public RestResponse postWithHeaders(String endPoint, Object content, Header... headers)
- throws IOException {
- Request post = Request.Post(getUrl(endPoint));
- if (headers != null) {
- post.setHeaders(headers);
- }
- if (content != null) {
- addContentToRequest(post, content);
- }
- return execute(post);
- }
-
- private static void addContentToRequest(Request request, Object content) {
- request.addHeader(new BasicHeader(CONTENT_TYPE, "application/json"));
- request.body(new StringEntity(JSON_COMPACT.newGson().toJson(content), UTF_8));
- }
-
- public RestResponse delete(String endPoint) throws IOException {
- return execute(Request.Delete(getUrl(endPoint)));
- }
-
- public RestResponse deleteWithHeaders(String endPoint, Header... headers) throws IOException {
- Request delete = Request.Delete(getUrl(endPoint));
- if (headers != null) {
- delete.setHeaders(headers);
- }
- return execute(delete);
- }
-
- private String getUrl(String endPoint) {
- return url + (account != null ? "/a" : "") + endPoint;
- }
+
+/** Makes rest requests to gerrit backend. */
+@UsedAt(UsedAt.Project.GOOGLE) // Google has own implementation of this interface in tests.
+public interface RestSession {
+ String url();
+
+ RestResponse execute(Request request) throws Exception;
+
+ RestResponse get(String endPoint) throws Exception;
+
+ RestResponse getJsonAccept(String endPoint) throws Exception;
+
+ RestResponse getWithHeaders(String endPoint, Header... headers) throws Exception;
+
+ RestResponse head(String endPoint) throws Exception;
+
+ RestResponse put(String endPoint) throws Exception;
+
+ RestResponse put(String endPoint, Object content) throws Exception;
+
+ RestResponse putWithHeaders(String endPoint, Header... headers) throws Exception;
+
+ RestResponse putWithHeaders(String endPoint, Object content, Header... headers) throws Exception;
+
+ RestResponse putRaw(String endPoint, RawInput stream) throws Exception;
+
+ RestResponse post(String endPoint) throws Exception;
+
+ RestResponse post(String endPoint, Object content) throws Exception;
+
+ RestResponse postWithHeaders(String endPoint, Object content, Header... headers) throws Exception;
+
+ RestResponse delete(String endPoint) throws Exception;
+
+ RestResponse deleteWithHeaders(String endPoint, Header... headers) throws Exception;
}
diff --git a/java/com/google/gerrit/acceptance/ServerTestRule.java b/java/com/google/gerrit/acceptance/ServerTestRule.java
new file mode 100644
index 0000000000..a057a6e11b
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/ServerTestRule.java
@@ -0,0 +1,104 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gerrit.acceptance.ProjectResetter.Config;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.inject.Injector;
+import java.net.InetSocketAddress;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import org.junit.rules.TestRule;
+
+public interface ServerTestRule extends TestRule {
+ /**
+ * Initialize a server.
+ *
+ * <p>All other methods must be called after this method is executed.
+ */
+ void initServer() throws Exception;
+
+ @Nullable
+ ProjectResetter createProjectResetter(
+ BiFunction<AllProjectsName, AllUsersName, Config> resetConfigSupplier) throws Exception;
+
+ Injector getTestInjector();
+
+ Optional<Injector> getHttpdInjector();
+
+ /**
+ * Initializes Ssh if a test requires it.
+ *
+ * <p>The method shouldn't throw an exception if the test doesn't require Ssh. If the test
+ * requires ssh and ssh is not supported (e.g. in internal google tests) the method throws {@link
+ * UnsupportedOperationException}.
+ */
+ void initSsh() throws Exception;
+
+ /**
+ * Restart backend as a replica and re-init Ssh if a test requires ssh.
+ *
+ * <p>The method throws {@link UnsupportedOperationException} if restarting is not supported (e.g.
+ * in internal google tests).
+ */
+ void restartAsSlave() throws Exception;
+
+ /**
+ * Restart backend as a primary and re-init Ssh if a test requires ssh.
+ *
+ * <p>The method throws {@link UnsupportedOperationException} if restarting is not supported (e.g.
+ * in internal google tests).
+ */
+ void restart() throws Exception;
+
+ /**
+ * Creates {@link RestSession} which sends all requests as a specified account.
+ *
+ * <p>For sending anonymous requests pass null as the {@code account}.
+ */
+ RestSession createRestSession(@Nullable TestAccount account);
+
+ /** Returns true if the started server is a replica. */
+ boolean isReplica();
+
+ /** Returns address to be used for http requests (if present). */
+ Optional<InetSocketAddress> getHttpAddress();
+
+ /**
+ * Gets or creates a session associated with the given context.
+ *
+ * <p>The method throws {@link UnsupportedOperationException} if ssh is not supported (e.g. in
+ * internal google tests). The method must be called only if a test or class is annotated with the
+ * UseSsh annotation.
+ */
+ SshSession getOrCreateSshSessionForContext(RequestContext ctx);
+
+ /** Returns url to be used for git operations. */
+ String getGitUrl();
+
+ /** Returns true if ssh has been initialized. */
+ boolean sshInitialized();
+
+ /**
+ * Returns true if username is supported.
+ *
+ * <p>If it is not supported tests must either skip username checks or use something else instead
+ * (e.g. email)
+ */
+ boolean isUsernameSupported();
+}
diff --git a/java/com/google/gerrit/acceptance/SshSession.java b/java/com/google/gerrit/acceptance/SshSession.java
index 23bfd0b1fa..ac1a73d61a 100644
--- a/java/com/google/gerrit/acceptance/SshSession.java
+++ b/java/com/google/gerrit/acceptance/SshSession.java
@@ -17,6 +17,7 @@ package com.google.gerrit.acceptance;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.testsuite.account.TestAccount;
import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
import java.io.Reader;
@@ -38,6 +39,7 @@ public abstract class SshSession {
public abstract void close();
+ @CanIgnoreReturnValue
public abstract String exec(String command) throws Exception;
public abstract int execAndReturnStatus(String command) throws Exception;
diff --git a/java/com/google/gerrit/acceptance/SshSessionMina.java b/java/com/google/gerrit/acceptance/SshSessionMina.java
index 89096e434f..bac4ed66ad 100644
--- a/java/com/google/gerrit/acceptance/SshSessionMina.java
+++ b/java/com/google/gerrit/acceptance/SshSessionMina.java
@@ -79,7 +79,8 @@ public class SshSessionMina extends SshSession {
@Override
public void open() throws Exception {
- getMinaSession();
+ @SuppressWarnings("unused")
+ var unused = getMinaSession();
}
@Override
diff --git a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
index dcb49a5943..01e705c883 100644
--- a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
+++ b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
@@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
import com.google.common.io.ByteStreams;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.groups.GroupInput;
@@ -75,7 +76,8 @@ public abstract class StandaloneSiteTest {
try {
// ServerContext ctor is called multiple times but the group can be only created once
- gApi.groups().id("Group");
+ @SuppressWarnings("unused")
+ var unused = gApi.groups().id("Group");
} catch (ResourceNotFoundException e) {
GroupInput in = new GroupInput();
in.members = Collections.singletonList("admin");
@@ -211,11 +213,13 @@ public abstract class StandaloneSiteTest {
runGerrit(Arrays.stream(multiArgs).flatMap(Streams::stream).toArray(String[]::new));
}
+ @CanIgnoreReturnValue
protected static String execute(
ImmutableList<String> cmd, File dir, ImmutableMap<String, String> env) throws IOException {
return execute(cmd, dir, env, null);
}
+ @CanIgnoreReturnValue
protected static String execute(
ImmutableList<String> cmd,
File dir,
diff --git a/java/com/google/gerrit/acceptance/TestAccount.java b/java/com/google/gerrit/acceptance/TestAccount.java
index 67d8a050d2..85c2ddca52 100644
--- a/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/java/com/google/gerrit/acceptance/TestAccount.java
@@ -78,7 +78,7 @@ public abstract class TestAccount {
return new PersonIdent(fullName(), email());
}
- public String getHttpUrl(GerritServer server) {
+ public String getHttpUrl(ServerTestRule server) {
checkState(server.getHttpAddress().isPresent(), "GerritServer must have httpAddress");
InetSocketAddress addr = server.getHttpAddress().get();
return new URIBuilder()
diff --git a/java/com/google/gerrit/acceptance/TestConfigRule.java b/java/com/google/gerrit/acceptance/TestConfigRule.java
new file mode 100644
index 0000000000..a7f051aa99
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/TestConfigRule.java
@@ -0,0 +1,142 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gerrit.acceptance.config.ConfigAnnotationParser;
+import com.google.gerrit.acceptance.config.GerritSystemProperty;
+import com.google.gerrit.server.util.git.DelegateSystemReader;
+import java.io.File;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.SystemReader;
+import org.junit.rules.TemporaryFolder;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+public class TestConfigRule implements TestRule {
+ private final TemporaryFolder temporaryFolder;
+ private final AbstractDaemonTest test;
+ private Description description;
+ private GerritServer.Description methodDescription;
+ GerritServer.Description classDescription;
+ private boolean testRequiresSsh;
+ private SystemReader oldSystemReader;
+
+ public TestConfigRule(TemporaryFolder temporaryFolder, AbstractDaemonTest test) {
+ this.temporaryFolder = temporaryFolder;
+ this.test = test;
+ }
+
+ @Override
+ public Statement apply(Statement statement, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ setTestConfigFromDescription(description);
+ statement.evaluate();
+ clear();
+ }
+ };
+ }
+
+ private void setTestConfigFromDescription(Description description) {
+ oldSystemReader = setFakeSystemReader(temporaryFolder.getRoot());
+
+ this.description = description;
+ classDescription = GerritServer.Description.forTestClass(description, test.configName);
+ methodDescription = GerritServer.Description.forTestMethod(description, test.configName);
+
+ if (methodDescription.systemProperties() != null) {
+ ConfigAnnotationParser.parse(methodDescription.systemProperties());
+ }
+
+ if (methodDescription.systemProperty() != null) {
+ ConfigAnnotationParser.parse(methodDescription.systemProperty());
+ }
+
+ test.baseConfig.unset("gerrit", null, "canonicalWebUrl");
+ test.baseConfig.unset("httpd", null, "listenUrl");
+
+ test.baseConfig.setInt("index", null, "batchThreads", -1);
+
+ testRequiresSsh = classDescription.useSshAnnotation() || methodDescription.useSshAnnotation();
+ if (!testRequiresSsh) {
+ test.baseConfig.setString("sshd", null, "listenAddress", "off");
+ }
+ }
+
+ private static SystemReader setFakeSystemReader(File tempDir) {
+ SystemReader oldSystemReader = SystemReader.getInstance();
+ SystemReader.setInstance(
+ new DelegateSystemReader(oldSystemReader) {
+ @Override
+ public FileBasedConfig openJGitConfig(Config parent, FS fs) {
+ return new FileBasedConfig(parent, new File(tempDir, "jgit.config"), FS.detect());
+ }
+
+ @Override
+ public FileBasedConfig openUserConfig(Config parent, FS fs) {
+ return new FileBasedConfig(parent, new File(tempDir, "user.config"), FS.detect());
+ }
+
+ @Override
+ public FileBasedConfig openSystemConfig(Config parent, FS fs) {
+ return new FileBasedConfig(parent, new File(tempDir, "system.config"), FS.detect());
+ }
+ });
+ return oldSystemReader;
+ }
+
+ private void clear() {
+ if (methodDescription.systemProperties() != null) {
+ for (GerritSystemProperty sysProp : methodDescription.systemProperties().value()) {
+ System.clearProperty(sysProp.name());
+ }
+ }
+
+ if (methodDescription.systemProperty() != null) {
+ System.clearProperty(methodDescription.systemProperty().name());
+ }
+ description = null;
+ methodDescription = null;
+ classDescription = null;
+ testRequiresSsh = false;
+
+ SystemReader.setInstance(oldSystemReader);
+ oldSystemReader = null;
+ }
+
+ public Description description() {
+ return description;
+ }
+
+ public GerritServer.Description methodDescription() {
+ return methodDescription;
+ }
+
+ public GerritServer.Description classDescription() {
+ return classDescription;
+ }
+
+ public boolean testRequiresSsh() {
+ return testRequiresSsh;
+ }
+
+ public Config baseConfig() {
+ return test.baseConfig;
+ }
+}
diff --git a/java/com/google/gerrit/acceptance/TimeSettingsTestRule.java b/java/com/google/gerrit/acceptance/TimeSettingsTestRule.java
new file mode 100644
index 0000000000..631dc062ad
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/TimeSettingsTestRule.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.testing.TestTimeUtil;
+import java.sql.Timestamp;
+import java.time.Instant;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+public class TimeSettingsTestRule implements TestRule {
+ private final TestConfigRule config;
+ private String systemTimeZone;
+
+ public TimeSettingsTestRule(TestConfigRule config) {
+ this.config = config;
+ }
+
+ @Override
+ public Statement apply(Statement statement, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ beforeTest();
+ statement.evaluate();
+ afterTest();
+ }
+ };
+ }
+
+ private void beforeTest() {
+ setTimeSettings(
+ config.classDescription().useSystemTime(),
+ config.classDescription().useClockStep(),
+ config.classDescription().useTimezone());
+ setTimeSettings(
+ config.methodDescription().useSystemTime(),
+ config.methodDescription().useClockStep(),
+ config.methodDescription().useTimezone());
+ }
+
+ private void afterTest() {
+ resetTimeSettings();
+ }
+
+ private void setTimeSettings(
+ boolean useSystemTime,
+ @Nullable UseClockStep useClockStep,
+ @Nullable UseTimezone useTimezone) {
+ if (useSystemTime) {
+ TestTimeUtil.useSystemTime();
+ } else if (useClockStep != null) {
+ TestTimeUtil.resetWithClockStep(useClockStep.clockStep(), useClockStep.clockStepUnit());
+ if (useClockStep.startAtEpoch()) {
+ TestTimeUtil.setClock(Timestamp.from(Instant.EPOCH));
+ }
+ }
+ if (useTimezone != null) {
+ systemTimeZone = System.setProperty("user.timezone", useTimezone.timezone());
+ }
+ }
+
+ private void resetTimeSettings() {
+ TestTimeUtil.useSystemTime();
+ if (systemTimeZone != null) {
+ System.setProperty("user.timezone", systemTimeZone);
+ systemTimeZone = null;
+ }
+ }
+}
diff --git a/java/com/google/gerrit/acceptance/config/BUILD b/java/com/google/gerrit/acceptance/config/BUILD
index 0da68b0f80..1da975f5d2 100644
--- a/java/com/google/gerrit/acceptance/config/BUILD
+++ b/java/com/google/gerrit/acceptance/config/BUILD
@@ -12,5 +12,6 @@ java_library(
"//lib:jgit",
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
+ "//lib/errorprone:annotations",
],
)
diff --git a/java/com/google/gerrit/acceptance/config/package-info.java b/java/com/google/gerrit/acceptance/config/package-info.java
new file mode 100644
index 0000000000..753b75a762
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/config/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.acceptance.config;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/acceptance/package-info.java b/java/com/google/gerrit/acceptance/package-info.java
new file mode 100644
index 0000000000..428b5fbe1a
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.acceptance;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/acceptance/rest/package-info.java b/java/com/google/gerrit/acceptance/rest/package-info.java
new file mode 100644
index 0000000000..923b0c5e5f
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/rest/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.acceptance.rest;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/acceptance/ssh/TestSshCommandModule.java b/java/com/google/gerrit/acceptance/ssh/TestSshCommandModule.java
index 626092bdbd..f20851c332 100644
--- a/java/com/google/gerrit/acceptance/ssh/TestSshCommandModule.java
+++ b/java/com/google/gerrit/acceptance/ssh/TestSshCommandModule.java
@@ -17,6 +17,10 @@ package com.google.gerrit.acceptance.ssh;
import com.google.gerrit.sshd.CommandModule;
public class TestSshCommandModule extends CommandModule {
+ public TestSshCommandModule() {
+ super(/* slaveMode= */ false);
+ }
+
@Override
protected void configure() {
command("graceful").to(GracefulCommand.class);
diff --git a/java/com/google/gerrit/acceptance/ssh/package-info.java b/java/com/google/gerrit/acceptance/ssh/package-info.java
new file mode 100644
index 0000000000..940b42e738
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/ssh/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.acceptance.ssh;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
index edbb1ee4cb..5b5895faaa 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
@@ -19,6 +19,7 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Account;
import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.ServerInitiated;
@@ -66,10 +67,11 @@ public class AccountOperationsImpl implements AccountOperations {
@Override
public TestAccountCreation.Builder newAccount() {
- return TestAccountCreation.builder(this::createAccount);
+ return TestAccountCreation.builder(
+ this::createAccount, externalIdFactory.arePasswordsAllowed());
}
- private Account.Id createAccount(TestAccountCreation testAccountCreation) throws Exception {
+ protected Account.Id createAccount(TestAccountCreation testAccountCreation) throws Exception {
Account.Id accountId = Account.id(seq.nextAccountId());
Consumer<AccountDelta.Builder> accountCreation =
deltaBuilder -> initAccountDelta(deltaBuilder, testAccountCreation, accountId);
@@ -159,6 +161,7 @@ public class AccountOperationsImpl implements AccountOperations {
checkState(updatedAccount.isPresent(), "Tried to update non-existing test account");
}
+ @CanIgnoreReturnValue
private Optional<AccountState> updateAccount(ConfigureDeltaFromState configureDeltaFromState)
throws IOException, ConfigInvalidException {
return accountsUpdate.update("Update Test Account", accountId, configureDeltaFromState);
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
index 042dc9a2af..5d4051720d 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkState;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
import com.google.gerrit.entities.Account;
import java.util.Optional;
@@ -41,50 +42,62 @@ public abstract class TestAccountCreation {
abstract ThrowingFunction<TestAccountCreation, Account.Id> accountCreator();
- public static Builder builder(ThrowingFunction<TestAccountCreation, Account.Id> accountCreator) {
- return new AutoValue_TestAccountCreation.Builder()
- .accountCreator(accountCreator)
- .httpPassword("http-pass");
+ public static Builder builder(
+ ThrowingFunction<TestAccountCreation, Account.Id> accountCreator,
+ boolean arePasswordsAllowed) {
+ TestAccountCreation.Builder builder =
+ new AutoValue_TestAccountCreation.Builder().accountCreator(accountCreator);
+ if (arePasswordsAllowed) {
+ builder.httpPassword("http-pass");
+ }
+ return builder;
}
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder fullname(String fullname);
+ @CanIgnoreReturnValue
public Builder clearFullname() {
return fullname("");
}
public abstract Builder httpPassword(String httpPassword);
+ @CanIgnoreReturnValue
public Builder clearHttpPassword() {
return httpPassword("");
}
public abstract Builder preferredEmail(String preferredEmail);
+ @CanIgnoreReturnValue
public Builder clearPreferredEmail() {
return preferredEmail("");
}
public abstract Builder username(String username);
+ @CanIgnoreReturnValue
public Builder clearUsername() {
return username("");
}
public abstract Builder status(String status);
+ @CanIgnoreReturnValue
public Builder clearStatus() {
return status("");
}
abstract Builder active(boolean active);
+ @CanIgnoreReturnValue
public Builder active() {
return active(true);
}
+ @CanIgnoreReturnValue
public Builder inactive() {
return active(false);
}
@@ -93,6 +106,7 @@ public abstract class TestAccountCreation {
abstract ImmutableSet.Builder<String> secondaryEmailsBuilder();
+ @CanIgnoreReturnValue
public Builder addSecondaryEmail(String secondaryEmail) {
secondaryEmailsBuilder().add(secondaryEmail);
return this;
@@ -103,6 +117,7 @@ public abstract class TestAccountCreation {
abstract TestAccountCreation autoBuild();
+ @CanIgnoreReturnValue
public Account.Id create() {
TestAccountCreation accountCreation = autoBuild();
if (accountCreation.preferredEmail().isPresent()) {
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/package-info.java b/java/com/google/gerrit/acceptance/testsuite/account/package-info.java
new file mode 100644
index 0000000000..e211ecdbb3
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/account/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.acceptance.testsuite.account;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
index 3bd355b3d0..1fd780f27d 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
@@ -181,7 +181,8 @@ public class PerPatchsetOperationsImpl implements PerPatchsetOperations {
side,
message,
unresolved,
- parentUuid);
+ parentUuid,
+ null);
// For draft comments, only the tag set on the HumanComment (and not on the ChangeUpdate)
// matters.
commentCreation.tag().ifPresent(tag -> newComment.tag = tag);
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java b/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java
index a0746e2156..eb714d459e 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java
@@ -19,7 +19,9 @@ import static com.google.common.base.Preconditions.checkState;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
+import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
@@ -32,6 +34,9 @@ import org.eclipse.jgit.merge.MergeStrategy;
/** Initial attributes of the change. If not provided, arbitrary values will be used. */
@AutoValue
public abstract class TestChangeCreation {
+ @UsedAt(UsedAt.Project.GOOGLE)
+ public abstract Optional<String> host();
+
public abstract Optional<Project.NameKey> project();
public abstract String branch();
@@ -72,6 +77,10 @@ public abstract class TestChangeCreation {
@AutoValue.Builder
public abstract static class Builder {
+ /** Host name in a multi-tenant deployment. */
+ @UsedAt(UsedAt.Project.GOOGLE)
+ public abstract Builder host(String host);
+
/** Target project/Repository of the change. Must be an existing project. */
public abstract Builder project(Project.NameKey project);
@@ -238,6 +247,7 @@ public abstract class TestChangeCreation {
*
* @return the {@code Change.Id} of the created change
*/
+ @CanIgnoreReturnValue
public Change.Id create() {
TestChangeCreation changeUpdate = build();
return changeUpdate.changeCreator().applyAndThrowSilently(changeUpdate);
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestCommentCreation.java b/java/com/google/gerrit/acceptance/testsuite/change/TestCommentCreation.java
index 2031bde7c0..9828e6c34b 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/TestCommentCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestCommentCreation.java
@@ -15,6 +15,7 @@
package com.google.gerrit.acceptance.testsuite.change;
import com.google.auto.value.AutoValue;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
import com.google.gerrit.acceptance.testsuite.change.TestRange.Position;
import com.google.gerrit.common.Nullable;
@@ -68,6 +69,7 @@ public abstract class TestCommentCreation {
@AutoValue.Builder
public abstract static class Builder {
+ @CanIgnoreReturnValue
public Builder noMessage() {
return message("");
}
@@ -76,11 +78,13 @@ public abstract class TestCommentCreation {
public abstract Builder message(String message);
/** Indicates a patchset-level comment. */
+ @CanIgnoreReturnValue
public Builder onPatchsetLevel() {
return file(Patch.PATCHSET_LEVEL);
}
/** Indicates a file comment. The comment will be on the specified file. */
+ @CanIgnoreReturnValue
public Builder onFileLevelOf(String filePath) {
return file(filePath).line(null).range(null);
}
@@ -122,6 +126,7 @@ public abstract class TestCommentCreation {
* <p>On the UI, such comments are shown on the right side of a diff view when a diff against
* base is selected. See {@link #onParentCommit()} for comments shown on the left side.
*/
+ @CanIgnoreReturnValue
public Builder onPatchsetCommit() {
return side(CommentSide.PATCHSET_COMMIT);
}
@@ -135,11 +140,13 @@ public abstract class TestCommentCreation {
*
* <p>For merge commits, this indicates the first parent commit.
*/
+ @CanIgnoreReturnValue
public Builder onParentCommit() {
return side(CommentSide.PARENT_COMMIT);
}
/** Like {@link #onParentCommit()} but for the second parent of a merge commit. */
+ @CanIgnoreReturnValue
public Builder onSecondParentCommit() {
return side(CommentSide.SECOND_PARENT_COMMIT);
}
@@ -148,6 +155,7 @@ public abstract class TestCommentCreation {
* Like {@link #onParentCommit()} but for the AutoMerge commit created from the parents of a
* merge commit.
*/
+ @CanIgnoreReturnValue
public Builder onAutoMergeCommit() {
return side(CommentSide.AUTO_MERGE_COMMIT);
}
@@ -155,11 +163,13 @@ public abstract class TestCommentCreation {
abstract Builder side(CommentSide side);
/** Indicates a resolved comment. */
+ @CanIgnoreReturnValue
public Builder resolved() {
return unresolved(false);
}
/** Indicates an unresolved comment. */
+ @CanIgnoreReturnValue
public Builder unresolved() {
return unresolved(true);
}
@@ -211,6 +221,7 @@ public abstract class TestCommentCreation {
*
* @return the UUID of the created comment
*/
+ @CanIgnoreReturnValue
public String create() {
TestCommentCreation commentCreation = autoBuild();
return commentCreation.commentCreator().applyAndThrowSilently(commentCreation);
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestPatchsetCreation.java b/java/com/google/gerrit/acceptance/testsuite/change/TestPatchsetCreation.java
index f8ca977294..d9ac4904ed 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/TestPatchsetCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestPatchsetCreation.java
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkState;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.PatchSet;
@@ -181,6 +182,7 @@ public abstract class TestPatchsetCreation {
*
* @return the {@code PatchSet.Id} of the created patchset
*/
+ @CanIgnoreReturnValue
public PatchSet.Id create() {
TestPatchsetCreation patchsetCreation = build();
return patchsetCreation.patchsetCreator().applyAndThrowSilently(patchsetCreation);
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/package-info.java b/java/com/google/gerrit/acceptance/testsuite/change/package-info.java
new file mode 100644
index 0000000000..3863f72e2b
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.acceptance.testsuite.change;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/BUILD b/java/com/google/gerrit/acceptance/testsuite/group/BUILD
index 2052105a41..2bc2b62273 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/BUILD
+++ b/java/com/google/gerrit/acceptance/testsuite/group/BUILD
@@ -21,6 +21,7 @@ java_library(
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
"//lib/commons:lang3",
+ "//lib/errorprone:annotations",
"//lib/guice",
],
)
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java
index 99899cf34d..7549c84f5b 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java
@@ -19,6 +19,7 @@ import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
@@ -54,6 +55,7 @@ public abstract class TestGroupCreation {
public abstract Builder description(String description);
+ @CanIgnoreReturnValue
public Builder clearDescription() {
return description("");
}
@@ -62,10 +64,12 @@ public abstract class TestGroupCreation {
public abstract Builder visibleToAll(boolean visibleToAll);
+ @CanIgnoreReturnValue
public Builder clearMembers() {
return members(ImmutableSet.of());
}
+ @CanIgnoreReturnValue
public Builder members(Account.Id member1, Account.Id... otherMembers) {
return members(Sets.union(ImmutableSet.of(member1), ImmutableSet.copyOf(otherMembers)));
}
@@ -74,15 +78,18 @@ public abstract class TestGroupCreation {
abstract ImmutableSet.Builder<Account.Id> membersBuilder();
+ @CanIgnoreReturnValue
public Builder addMember(Account.Id member) {
membersBuilder().add(member);
return this;
}
+ @CanIgnoreReturnValue
public Builder clearSubgroups() {
return subgroups(ImmutableSet.of());
}
+ @CanIgnoreReturnValue
public Builder subgroups(AccountGroup.UUID subgroup1, AccountGroup.UUID... otherSubgroups) {
return subgroups(Sets.union(ImmutableSet.of(subgroup1), ImmutableSet.copyOf(otherSubgroups)));
}
@@ -91,6 +98,7 @@ public abstract class TestGroupCreation {
abstract ImmutableSet.Builder<AccountGroup.UUID> subgroupsBuilder();
+ @CanIgnoreReturnValue
public Builder addSubgroup(AccountGroup.UUID subgroup) {
subgroupsBuilder().add(subgroup);
return this;
@@ -106,6 +114,7 @@ public abstract class TestGroupCreation {
*
* @return the UUID of the created group
*/
+ @CanIgnoreReturnValue
public AccountGroup.UUID create() {
TestGroupCreation groupCreation = autoBuild();
return testRefAction(() -> groupCreation.groupCreator().applyAndThrowSilently(groupCreation));
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java
index 47c7117efc..e375760ed8 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java
+++ b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java
@@ -17,6 +17,7 @@ package com.google.gerrit.acceptance.testsuite.group;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
@@ -56,6 +57,7 @@ public abstract class TestGroupUpdate {
public abstract Builder description(String description);
+ @CanIgnoreReturnValue
public Builder clearDescription() {
return description("");
}
@@ -69,10 +71,12 @@ public abstract class TestGroupUpdate {
abstract Function<ImmutableSet<Account.Id>, Set<Account.Id>> memberModification();
+ @CanIgnoreReturnValue
public Builder clearMembers() {
return memberModification(originalMembers -> ImmutableSet.of());
}
+ @CanIgnoreReturnValue
public Builder addMember(Account.Id member) {
Function<ImmutableSet<Account.Id>, Set<Account.Id>> previousModification =
memberModification();
@@ -82,6 +86,7 @@ public abstract class TestGroupUpdate {
return this;
}
+ @CanIgnoreReturnValue
public Builder removeMember(Account.Id member) {
Function<ImmutableSet<Account.Id>, Set<Account.Id>> previousModification =
memberModification();
@@ -98,10 +103,12 @@ public abstract class TestGroupUpdate {
abstract Function<ImmutableSet<AccountGroup.UUID>, Set<AccountGroup.UUID>>
subgroupModification();
+ @CanIgnoreReturnValue
public Builder clearSubgroups() {
return subgroupModification(originalMembers -> ImmutableSet.of());
}
+ @CanIgnoreReturnValue
public Builder addSubgroup(AccountGroup.UUID subgroup) {
Function<ImmutableSet<AccountGroup.UUID>, Set<AccountGroup.UUID>> previousModification =
subgroupModification();
@@ -111,6 +118,7 @@ public abstract class TestGroupUpdate {
return this;
}
+ @CanIgnoreReturnValue
public Builder removeSubgroup(AccountGroup.UUID subgroup) {
Function<ImmutableSet<AccountGroup.UUID>, Set<AccountGroup.UUID>> previousModification =
subgroupModification();
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/package-info.java b/java/com/google/gerrit/acceptance/testsuite/group/package-info.java
new file mode 100644
index 0000000000..78778e1406
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/group/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.acceptance.testsuite.group;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/acceptance/testsuite/package-info.java b/java/com/google/gerrit/acceptance/testsuite/package-info.java
new file mode 100644
index 0000000000..2f43e09ff7
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.acceptance.testsuite;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/BUILD b/java/com/google/gerrit/acceptance/testsuite/project/BUILD
index 4ac2705fb0..be51a64355 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/BUILD
+++ b/java/com/google/gerrit/acceptance/testsuite/project/BUILD
@@ -21,6 +21,7 @@ java_library(
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
"//lib/commons:lang3",
+ "//lib/errorprone:annotations",
"//lib/guice",
],
)
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
index 3337fc33be..e8df7ed071 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
@@ -19,6 +19,7 @@ import static java.util.Objects.requireNonNull;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Project;
@@ -66,6 +67,7 @@ public abstract class TestProjectCreation {
* "refs/heads/" prefix of the branch name can be omitted. The specified branches are ignored if
* {@link #noEmptyCommit()} is used.
*/
+ @CanIgnoreReturnValue
public TestProjectCreation.Builder branches(String branch1, String... otherBranches) {
return branches(Sets.union(ImmutableSet.of(branch1), ImmutableSet.copyOf(otherBranches)));
}
@@ -77,10 +79,12 @@ public abstract class TestProjectCreation {
public abstract TestProjectCreation.Builder permissionOnly(boolean value);
/** Skips the empty commit on creation. This means that project's branches will not exist. */
+ @CanIgnoreReturnValue
public TestProjectCreation.Builder noEmptyCommit() {
return createEmptyCommit(false);
}
+ @CanIgnoreReturnValue
public TestProjectCreation.Builder addOwner(AccountGroup.UUID owner) {
ownersBuilder().add(requireNonNull(owner, "owner"));
return this;
@@ -98,6 +102,7 @@ public abstract class TestProjectCreation {
*
* @return the name of the created project
*/
+ @CanIgnoreReturnValue
public Project.NameKey create() {
TestProjectCreation creation = autoBuild();
return creation.projectCreator().applyAndThrowSilently(creation);
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
index 5634c780ba..cc57ba677d 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
@@ -21,6 +21,7 @@ import static java.util.Objects.requireNonNull;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.entities.AccountGroup;
@@ -75,6 +76,7 @@ public abstract class TestProjectUpdate {
abstract Optional<Integer> max();
/** Sets the minimum and maximum values for the capability. */
+ @CanIgnoreReturnValue
public Builder range(int min, int max) {
checkNonInvertedRange(min, max);
return min(min).max(max);
@@ -353,61 +355,72 @@ public abstract class TestProjectUpdate {
* Removes all access sections. Useful when testing against a specific set of access sections or
* permissions.
*/
+ @CanIgnoreReturnValue
public Builder removeAllAccessSections() {
return removeAllAccessSections(true);
}
/** Adds a permission to be included in this update. */
+ @CanIgnoreReturnValue
public Builder add(TestPermission testPermission) {
addedPermissionsBuilder().add(testPermission);
return this;
}
/** Adds a permission to be included in this update. */
+ @CanIgnoreReturnValue
public Builder add(TestPermission.Builder testPermissionBuilder) {
return add(testPermissionBuilder.build());
}
/** Adds a label permission to be included in this update. */
+ @CanIgnoreReturnValue
public Builder add(TestLabelPermission testLabelPermission) {
addedLabelPermissionsBuilder().add(testLabelPermission);
return this;
}
/** Adds a label permission to be included in this update. */
+ @CanIgnoreReturnValue
public Builder add(TestLabelPermission.Builder testLabelPermissionBuilder) {
return add(testLabelPermissionBuilder.build());
}
/** Adds a capability to be included in this update. */
+ @CanIgnoreReturnValue
public Builder add(TestCapability testCapability) {
addedCapabilitiesBuilder().add(testCapability);
return this;
}
/** Adds a capability to be included in this update. */
+ @CanIgnoreReturnValue
public Builder add(TestCapability.Builder testCapabilityBuilder) {
return add(testCapabilityBuilder.build());
}
/** Removes a permission, label permission, or capability as part of this update. */
+ @CanIgnoreReturnValue
public Builder remove(TestPermissionKey testPermissionKey) {
removedPermissionsBuilder().add(testPermissionKey);
return this;
}
/** Removes a permission, label permission, or capability as part of this update. */
+ @CanIgnoreReturnValue
public Builder remove(TestPermissionKey.Builder testPermissionKeyBuilder) {
return remove(testPermissionKeyBuilder.build());
}
/** Sets the exclusive bit bit for the given permission key. */
+ @CanIgnoreReturnValue
public Builder setExclusiveGroup(
TestPermissionKey.Builder testPermissionKeyBuilder, boolean exclusive) {
return setExclusiveGroup(testPermissionKeyBuilder.build(), exclusive);
}
/** Sets the exclusive bit bit for the given permission key. */
+ @CanIgnoreReturnValue
public Builder setExclusiveGroup(TestPermissionKey testPermissionKey, boolean exclusive) {
checkArgument(
!testPermissionKey.group().isPresent(),
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/package-info.java b/java/com/google/gerrit/acceptance/testsuite/project/package-info.java
new file mode 100644
index 0000000000..dfe56d60fb
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/project/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.acceptance.testsuite.project;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
index a9914b3d74..8bad32c964 100644
--- a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
+++ b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
@@ -14,9 +14,10 @@
package com.google.gerrit.acceptance.testsuite.request;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
import com.google.gerrit.acceptance.testsuite.account.TestAccount;
import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
/**
* An aggregation of operations on Guice request scopes for test purposes.
@@ -25,54 +26,68 @@ import com.google.gerrit.entities.Account;
*/
public interface RequestScopeOperations {
/**
+ * Sets the Guice request scope to the given account without closing the existing context.
+ *
+ * <p>Returns newly created context. To restore previous context call {@link
+ * ManualRequestContext#close()} method of the returned context.
+ *
+ * <p>In order to create and use the SSH session for the newly set context, SSH must be enabled in
+ * the test and the account must have a username set.
+ *
+ * <p>The session associated with the returned context can be obtained by calling {@link
+ * com.google.gerrit.acceptance.AbstractDaemonTest#getOrCreateSshSessionForContext}.
+ *
+ * @param accountId account ID. Must exist; throws an unchecked exception otherwise.
+ */
+ ManualRequestContext setNestedApiUser(Account.Id accountId) throws Exception;
+
+ /**
* Sets the Guice request scope to the given account.
*
- * <p>The resulting context has an SSH session attached. In order to use the SSH session returned
- * by {@link com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context#getSession()}, SSH
- * must be enabled in the test and the account must have a username set. However, these are not
- * requirements simply to call this method.
+ * <p>After calling this method, the new context can be obtained using the {@link
+ * ThreadLocalRequestContext#getContext()} method.
+ *
+ * <p>If the previous context is a {@link ManualRequestContext}. the method closes it before
+ * setting the new context. This prevents stacking of contexts.
+ *
+ * <p>In order to create and use the SSH session for the new context, SSH must be enabled in the
+ * test and the account must have a username set. To get the session associated with the newly set
+ * context use the {@link
+ * com.google.gerrit.acceptance.AbstractDaemonTest#getOrCreateSshSessionForContext} method.
*
* @param accountId account ID. Must exist; throws an unchecked exception otherwise.
- * @return the previous request scope.
*/
- AcceptanceTestRequestScope.Context setApiUser(Account.Id accountId);
+ void setApiUser(Account.Id accountId) throws Exception;
/**
* Sets the Guice request scope to the given account.
*
- * <p>The resulting context has an SSH session attached. In order to use the SSH session returned
- * by {@link com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context#getSession()}, SSH
- * must be enabled in the test and the account must have a username set. However, these are not
- * requirements simply to call this method.
+ * <p>See {@link #setApiUser(Account.Id)} for details.
*
* @param testAccount test account from {@code AccountOperations}.
- * @return the previous request scope.
*/
- AcceptanceTestRequestScope.Context setApiUser(TestAccount testAccount);
+ void setApiUser(TestAccount testAccount) throws Exception;
/**
* Enforces a new request context for the current API user.
*
- * <p>This recreates the {@code IdentifiedUser}, hence everything which is cached in the {@code
- * IdentifiedUser} is reloaded (e.g. the email addresses of the user).
- *
- * <p>The current user must be an identified user.
+ * <p>See {@link #setApiUser(Account.Id)} for details.
*
- * @return the previous request scope.
+ * <p>The current user (i.e. a user set before calling this method) must be an identified user.
*/
- AcceptanceTestRequestScope.Context resetCurrentApiUser();
+ void resetCurrentApiUser() throws Exception;
/**
* Sets the Guice request scope to the anonymous user.
*
- * @return the previous request scope.
+ * <p>See {@link #setApiUser(Account.Id)} for details.
*/
- AcceptanceTestRequestScope.Context setApiUserAnonymous();
+ void setApiUserAnonymous() throws Exception;
/**
* Sets the Guice request scope to the internal server user.
*
- * @return the previous request scope.
+ * <p>See {@link #setApiUser(Account.Id)} for details.
*/
- AcceptanceTestRequestScope.Context setApiUserInternal();
+ void setApiUserInternal() throws Exception;
}
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
index 895c7a0cf1..03336caa4b 100644
--- a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
@@ -17,12 +17,8 @@ package com.google.gerrit.acceptance.testsuite.request;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
-import com.google.gerrit.acceptance.GerritServer.TestSshServerAddress;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.account.TestAccount;
-import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
@@ -30,10 +26,12 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.IdentifiedUser.GenericFactory;
import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
-import java.net.InetSocketAddress;
/**
* The implementation of {@code RequestScopeOperations}.
@@ -43,64 +41,76 @@ import java.net.InetSocketAddress;
*/
@Singleton
public class RequestScopeOperationsImpl implements RequestScopeOperations {
- private final AcceptanceTestRequestScope atrScope;
+ private final ThreadLocalRequestContext localContext;
private final AccountCache accountCache;
private final AccountOperations accountOperations;
private final IdentifiedUser.GenericFactory userFactory;
private final Provider<AnonymousUser> anonymousUserProvider;
private final InternalUser.Factory internalUserFactory;
- private final InetSocketAddress sshAddress;
- private final TestSshKeys testSshKeys;
@Inject
RequestScopeOperationsImpl(
- AcceptanceTestRequestScope atrScope,
+ ThreadLocalRequestContext localContext,
AccountCache accountCache,
AccountOperations accountOperations,
GenericFactory userFactory,
Provider<AnonymousUser> anonymousUserProvider,
- InternalUser.Factory internalUserFactory,
- @Nullable @TestSshServerAddress InetSocketAddress sshAddress,
- TestSshKeys testSshKeys) {
- this.atrScope = atrScope;
+ InternalUser.Factory internalUserFactory) {
+ this.localContext = localContext;
this.accountCache = accountCache;
this.accountOperations = accountOperations;
this.userFactory = userFactory;
this.anonymousUserProvider = anonymousUserProvider;
this.internalUserFactory = internalUserFactory;
- this.sshAddress = sshAddress;
- this.testSshKeys = testSshKeys;
}
@Override
- public AcceptanceTestRequestScope.Context setApiUser(Account.Id accountId) {
- return setApiUser(accountOperations.account(accountId).get());
+ public ManualRequestContext setNestedApiUser(Account.Id accountId) {
+ return new ManualRequestContext(createIdentifiedUser(accountId), localContext);
}
@Override
- public AcceptanceTestRequestScope.Context setApiUser(TestAccount testAccount) {
- return atrScope.set(
- atrScope.newContext(
- SshSessionFactory.createSession(testSshKeys, sshAddress, testAccount),
- createIdentifiedUser(testAccount.accountId())));
+ public void setApiUser(Account.Id accountId) {
+ setApiUser(accountOperations.account(accountId).get());
}
@Override
- public AcceptanceTestRequestScope.Context resetCurrentApiUser() {
- CurrentUser user = atrScope.get().getUser();
+ public void setApiUser(TestAccount testAccount) {
+ setApiUser(createIdentifiedUser(testAccount.accountId()));
+ }
+
+ @Override
+ public void resetCurrentApiUser() {
+ RequestContext currentContext = localContext.getContext();
+ checkState(
+ currentContext != null, "can only reset IdentifiedUser, but the RequestContext is null");
+ CurrentUser user = localContext.getContext().getUser();
// More special cases for anonymous users etc. can be added as needed.
checkState(user.isIdentifiedUser(), "can only reset IdentifiedUser, not %s", user);
- return setApiUser(user.getAccountId());
+ setApiUser(user.getAccountId());
}
@Override
- public AcceptanceTestRequestScope.Context setApiUserAnonymous() {
- return atrScope.set(atrScope.newContext(null, anonymousUserProvider.get()));
+ public void setApiUserAnonymous() {
+ setApiUser(anonymousUserProvider.get());
}
@Override
- public AcceptanceTestRequestScope.Context setApiUserInternal() {
- return atrScope.set(atrScope.newContext(null, internalUserFactory.create()));
+ public void setApiUserInternal() {
+ setApiUser(internalUserFactory.create());
+ }
+
+ private void setApiUser(CurrentUser newUser) {
+ // The ManualRequestContext stores the previous context. When the close() method is called,
+ // the old context is restored.
+ RequestContext oldContext = localContext.getContext();
+ if (oldContext instanceof ManualRequestContext) {
+ ((ManualRequestContext) oldContext).close();
+ }
+ // The created object is not used, because the constructor of the ManualRequestContext sets the
+ // active context to itself. It is not needed to explicitly call localContext.setContext after
+ // an instance is created.
+ var unused = new ManualRequestContext(newUser, localContext);
}
private IdentifiedUser createIdentifiedUser(Account.Id accountId) {
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/package-info.java b/java/com/google/gerrit/acceptance/testsuite/request/package-info.java
new file mode 100644
index 0000000000..943bd21d4f
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/request/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.acceptance.testsuite.request;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/asciidoctor/AsciiDoctor.java b/java/com/google/gerrit/asciidoctor/AsciiDoctor.java
index 9d0a28e14c..25ed8132f2 100644
--- a/java/com/google/gerrit/asciidoctor/AsciiDoctor.java
+++ b/java/com/google/gerrit/asciidoctor/AsciiDoctor.java
@@ -22,7 +22,7 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
-import java.nio.file.Paths;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -164,7 +164,7 @@ public class AsciiDoctor {
if (bazel) {
renderFiles(inputFiles, null);
} else {
- try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(Paths.get(zipFile)))) {
+ try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(Path.of(zipFile)))) {
renderFiles(inputFiles, zip);
File[] cssFiles = tmpdir.listFiles((dir, name) -> name.endsWith(".css"));
diff --git a/java/com/google/gerrit/asciidoctor/BUILD b/java/com/google/gerrit/asciidoctor/BUILD
index 94ec20db7d..a132095c6f 100644
--- a/java/com/google/gerrit/asciidoctor/BUILD
+++ b/java/com/google/gerrit/asciidoctor/BUILD
@@ -35,6 +35,6 @@ java_library(
"//lib:args4j",
"//lib:guava",
"//lib/lucene:lucene-analyzers-common",
- "//lib/lucene:lucene-core-and-backward-codecs",
+ "//lib/lucene:lucene-core",
],
)
diff --git a/java/com/google/gerrit/asciidoctor/DocIndexer.java b/java/com/google/gerrit/asciidoctor/DocIndexer.java
index acd6aad7af..fd8161be0f 100644
--- a/java/com/google/gerrit/asciidoctor/DocIndexer.java
+++ b/java/com/google/gerrit/asciidoctor/DocIndexer.java
@@ -24,7 +24,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
-import java.nio.file.Paths;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.JarEntry;
@@ -82,7 +82,7 @@ public class DocIndexer {
return;
}
- try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(Paths.get(outFile)))) {
+ try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(Path.of(outFile)))) {
byte[] compressedIndex = zip(index());
JarEntry entry = new JarEntry(String.format("%s/%s", Constants.PACKAGE, Constants.INDEX_ZIP));
entry.setSize(compressedIndex.length);
diff --git a/java/com/google/gerrit/asciidoctor/package-info.java b/java/com/google/gerrit/asciidoctor/package-info.java
new file mode 100644
index 0000000000..504c3f5f2d
--- /dev/null
+++ b/java/com/google/gerrit/asciidoctor/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.asciidoctor;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/auth/ldap/LdapRealm.java b/java/com/google/gerrit/auth/ldap/LdapRealm.java
index 7dc2b1bca4..e33e5cb2a8 100644
--- a/java/com/google/gerrit/auth/ldap/LdapRealm.java
+++ b/java/com/google/gerrit/auth/ldap/LdapRealm.java
@@ -331,7 +331,10 @@ class LdapRealm extends AbstractRealm {
final DirContext ctx = helper.open();
try {
Helper.LdapSchema schema = helper.getSchema(ctx);
- helper.findAccount(schema, ctx, username, false);
+
+ @SuppressWarnings("unused")
+ var unused = helper.findAccount(schema, ctx, username, false);
+
return true;
} catch (NoSuchUserException e) {
return false;
diff --git a/java/com/google/gerrit/auth/ldap/package-info.java b/java/com/google/gerrit/auth/ldap/package-info.java
new file mode 100644
index 0000000000..45e1bc2315
--- /dev/null
+++ b/java/com/google/gerrit/auth/ldap/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.auth.ldap;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/auth/oauth/package-info.java b/java/com/google/gerrit/auth/oauth/package-info.java
new file mode 100644
index 0000000000..53c6829878
--- /dev/null
+++ b/java/com/google/gerrit/auth/oauth/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.auth.oauth;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/auth/openid/package-info.java b/java/com/google/gerrit/auth/openid/package-info.java
new file mode 100644
index 0000000000..2b6f77d61e
--- /dev/null
+++ b/java/com/google/gerrit/auth/openid/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.auth.openid;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/auth/package-info.java b/java/com/google/gerrit/auth/package-info.java
new file mode 100644
index 0000000000..6538de41c8
--- /dev/null
+++ b/java/com/google/gerrit/auth/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.auth;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/common/BUILD b/java/com/google/gerrit/common/BUILD
index 8f930bb7e8..8f85311e9d 100644
--- a/java/com/google/gerrit/common/BUILD
+++ b/java/com/google/gerrit/common/BUILD
@@ -30,6 +30,7 @@ java_library(
"//lib:servlet-api",
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
],
)
diff --git a/java/com/google/gerrit/common/FileUtil.java b/java/com/google/gerrit/common/FileUtil.java
index 5b0925e70c..35cf8484bc 100644
--- a/java/com/google/gerrit/common/FileUtil.java
+++ b/java/com/google/gerrit/common/FileUtil.java
@@ -14,6 +14,7 @@
package com.google.gerrit.common;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -84,6 +85,7 @@ public class FileUtil {
}
}
+ @CanIgnoreReturnValue
public static Path mkdirsOrDie(Path p, String errMsg) {
try {
if (!Files.isDirectory(p)) {
diff --git a/java/com/google/gerrit/common/UsedAt.java b/java/com/google/gerrit/common/UsedAt.java
index 5ea5177dcb..83af551bc2 100644
--- a/java/com/google/gerrit/common/UsedAt.java
+++ b/java/com/google/gerrit/common/UsedAt.java
@@ -41,6 +41,7 @@ public @interface UsedAt {
PLUGIN_CHECKS,
PLUGIN_CODE_OWNERS,
PLUGIN_DELETE_PROJECT,
+ PLUGIN_GITHUB,
PLUGIN_HIGH_AVAILABILITY,
PLUGIN_MULTI_SITE,
PLUGIN_PULL_REPLICATION,
diff --git a/java/com/google/gerrit/common/auth/openid/package-info.java b/java/com/google/gerrit/common/auth/openid/package-info.java
new file mode 100644
index 0000000000..931ed56874
--- /dev/null
+++ b/java/com/google/gerrit/common/auth/openid/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.common.auth.openid;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/common/data/package-info.java b/java/com/google/gerrit/common/data/package-info.java
new file mode 100644
index 0000000000..9f3539c491
--- /dev/null
+++ b/java/com/google/gerrit/common/data/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.common.data;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/common/data/testing/BUILD b/java/com/google/gerrit/common/data/testing/BUILD
index d39d05c82e..a580c773ef 100644
--- a/java/com/google/gerrit/common/data/testing/BUILD
+++ b/java/com/google/gerrit/common/data/testing/BUILD
@@ -7,6 +7,7 @@ java_library(
visibility = ["//visibility:public"],
deps = [
"//java/com/google/gerrit/entities",
+ "//lib/errorprone:annotations",
"//lib/truth",
],
)
diff --git a/java/com/google/gerrit/common/data/testing/package-info.java b/java/com/google/gerrit/common/data/testing/package-info.java
new file mode 100644
index 0000000000..24c37086f6
--- /dev/null
+++ b/java/com/google/gerrit/common/data/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.common.data.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/common/package-info.java b/java/com/google/gerrit/common/package-info.java
new file mode 100644
index 0000000000..5f97d56b45
--- /dev/null
+++ b/java/com/google/gerrit/common/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.common;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/entities/AccessSection.java b/java/com/google/gerrit/entities/AccessSection.java
index 8ae0a5dbed..7dca72ed9d 100644
--- a/java/com/google/gerrit/entities/AccessSection.java
+++ b/java/com/google/gerrit/entities/AccessSection.java
@@ -20,6 +20,7 @@ import static java.util.Objects.requireNonNull;
import com.google.auto.value.AutoValue;
import com.google.auto.value.extension.memoized.Memoized;
import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import java.util.ArrayList;
import java.util.List;
@@ -121,27 +122,32 @@ public abstract class AccessSection implements Comparable<AccessSection> {
public abstract String getName();
+ @CanIgnoreReturnValue
public Builder modifyPermissions(Consumer<List<Permission.Builder>> modification) {
modification.accept(permissionBuilders);
return this;
}
+ @CanIgnoreReturnValue
public Builder addPermission(Permission.Builder permission) {
requireNonNull(permission, "permission must be non-null");
return modifyPermissions(p -> p.add(permission));
}
+ @CanIgnoreReturnValue
public Builder remove(Permission.Builder permission) {
requireNonNull(permission, "permission must be non-null");
return removePermission(permission.getName());
}
+ @CanIgnoreReturnValue
public Builder removePermission(String name) {
requireNonNull(name, "name must be non-null");
return modifyPermissions(
p -> p.removeIf(permissionBuilder -> name.equalsIgnoreCase(permissionBuilder.getName())));
}
+ @CanIgnoreReturnValue
public Permission.Builder upsertPermission(String permissionName) {
requireNonNull(permissionName, "permissionName must be non-null");
diff --git a/java/com/google/gerrit/entities/Account.java b/java/com/google/gerrit/entities/Account.java
index 52ad0a95c8..efbac973e2 100644
--- a/java/com/google/gerrit/entities/Account.java
+++ b/java/com/google/gerrit/entities/Account.java
@@ -19,7 +19,9 @@ import static com.google.gerrit.entities.RefNames.REFS_STARRED_CHANGES;
import static com.google.gerrit.entities.RefNames.REFS_USERS;
import com.google.auto.value.AutoValue;
+import com.google.common.base.MoreObjects;
import com.google.common.primitives.Ints;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import java.time.Instant;
@@ -160,6 +162,17 @@ public abstract class Account {
public abstract String metaId();
/**
+ * A unique tag which identifies the current version of the account.
+ *
+ * <p>It can be any non-empty string. For open-source gerrit it is the same as metaId; internally
+ * in google a different value is assigned.
+ *
+ * <p>The value can be null only during account updating/creation.
+ */
+ @Nullable
+ public abstract String uniqueTag();
+
+ /**
* Create a new account.
*
* @param newId unique id, see Sequences#nextAccountId().
@@ -261,6 +274,7 @@ public abstract class Account {
public abstract Builder setInactive(boolean inactive);
+ @CanIgnoreReturnValue
public Builder setActive(boolean active) {
return setInactive(!active);
}
@@ -275,6 +289,11 @@ public abstract class Account {
public abstract Builder setMetaId(@Nullable String metaId);
+ @Nullable
+ public abstract String uniqueTag();
+
+ public abstract Builder setUniqueTag(@Nullable String uniqueTag);
+
public abstract Account build();
}
@@ -282,4 +301,18 @@ public abstract class Account {
public final String toString() {
return getName();
}
+
+ public final String debugString() {
+ return MoreObjects.toStringHelper(this)
+ .add("id", id())
+ .add("registeredOn", registeredOn())
+ .add("fullName", fullName())
+ .add("displayName", displayName())
+ .add("preferredEmail", preferredEmail())
+ .add("inactive", inactive())
+ .add("status", status())
+ .add("metaId", metaId())
+ .add("uniqueTag", uniqueTag())
+ .toString();
+ }
}
diff --git a/java/com/google/gerrit/entities/AccountGroupByIdAudit.java b/java/com/google/gerrit/entities/AccountGroupByIdAudit.java
index 0ef51e500b..5db5af5744 100644
--- a/java/com/google/gerrit/entities/AccountGroupByIdAudit.java
+++ b/java/com/google/gerrit/entities/AccountGroupByIdAudit.java
@@ -15,6 +15,7 @@
package com.google.gerrit.entities;
import com.google.auto.value.AutoValue;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.time.Instant;
import java.util.Optional;
@@ -39,6 +40,7 @@ public abstract class AccountGroupByIdAudit {
abstract Builder removedOn(Instant removedOn);
+ @CanIgnoreReturnValue
public Builder removed(Account.Id removedBy, Instant removedOn) {
return removedBy(removedBy).removedOn(removedOn);
}
diff --git a/java/com/google/gerrit/entities/AccountGroupMemberAudit.java b/java/com/google/gerrit/entities/AccountGroupMemberAudit.java
index 913956eb40..0cb1c625ac 100644
--- a/java/com/google/gerrit/entities/AccountGroupMemberAudit.java
+++ b/java/com/google/gerrit/entities/AccountGroupMemberAudit.java
@@ -15,6 +15,7 @@
package com.google.gerrit.entities;
import com.google.auto.value.AutoValue;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.time.Instant;
import java.util.Optional;
@@ -43,10 +44,12 @@ public abstract class AccountGroupMemberAudit {
abstract Builder removedOn(Instant removedOn);
+ @CanIgnoreReturnValue
public Builder removed(Account.Id removedBy, Instant removedOn) {
return removedBy(removedBy).removedOn(removedOn);
}
+ @CanIgnoreReturnValue
public Builder removedLegacy() {
return removed(addedBy(), addedOn());
}
diff --git a/java/com/google/gerrit/entities/CachedProjectConfig.java b/java/com/google/gerrit/entities/CachedProjectConfig.java
index be4a1cf475..6dc9e32e62 100644
--- a/java/com/google/gerrit/entities/CachedProjectConfig.java
+++ b/java/com/google/gerrit/entities/CachedProjectConfig.java
@@ -20,6 +20,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -148,31 +149,37 @@ public abstract class CachedProjectConfig {
public abstract Builder setBranchOrderSection(Optional<BranchOrderSection> value);
+ @CanIgnoreReturnValue
public Builder addGroup(GroupReference groupReference) {
groupsBuilder().put(groupReference.getUUID(), groupReference);
return this;
}
+ @CanIgnoreReturnValue
public Builder addAccessSection(AccessSection accessSection) {
accessSectionsBuilder().put(accessSection.getName(), accessSection);
return this;
}
+ @CanIgnoreReturnValue
public Builder addContributorAgreement(ContributorAgreement contributorAgreement) {
contributorAgreementsBuilder().put(contributorAgreement.getName(), contributorAgreement);
return this;
}
+ @CanIgnoreReturnValue
public Builder addNotifySection(NotifyConfig notifyConfig) {
notifySectionsBuilder().put(notifyConfig.getName(), notifyConfig);
return this;
}
+ @CanIgnoreReturnValue
public Builder addLabelSection(LabelType labelType) {
labelSectionsBuilder().put(labelType.getName(), labelType);
return this;
}
+ @CanIgnoreReturnValue
public Builder addSubmitRequirementSection(SubmitRequirement submitRequirement) {
submitRequirementSectionsBuilder().put(submitRequirement.name(), submitRequirement);
return this;
@@ -180,11 +187,13 @@ public abstract class CachedProjectConfig {
public abstract Builder setMimeTypes(ConfiguredMimeTypes value);
+ @CanIgnoreReturnValue
public Builder addSubscribeSection(SubscribeSection subscribeSection) {
subscribeSectionsBuilder().put(subscribeSection.project(), subscribeSection);
return this;
}
+ @CanIgnoreReturnValue
public Builder addCommentLinkSection(StoredCommentLinkInfo storedCommentLinkInfo) {
commentLinkSectionsBuilder().put(storedCommentLinkInfo.getName(), storedCommentLinkInfo);
return this;
@@ -213,6 +222,7 @@ public abstract class CachedProjectConfig {
abstract ImmutableMap.Builder<String, String> pluginConfigsBuilder();
+ @CanIgnoreReturnValue
public Builder addPluginConfig(String pluginName, String pluginConfig) {
pluginConfigsBuilder().put(pluginName, pluginConfig);
return this;
@@ -222,6 +232,7 @@ public abstract class CachedProjectConfig {
abstract ImmutableMap.Builder<String, ImmutableConfig> parsedProjectLevelConfigsBuilder();
+ @CanIgnoreReturnValue
public Builder addProjectLevelConfig(String configFileName, String config) {
projectLevelConfigsBuilder().put(configFileName, config);
try {
diff --git a/java/com/google/gerrit/entities/Comment.java b/java/com/google/gerrit/entities/Comment.java
index e1e143cf91..35a60eb4f1 100644
--- a/java/com/google/gerrit/entities/Comment.java
+++ b/java/com/google/gerrit/entities/Comment.java
@@ -20,6 +20,7 @@ import com.google.gerrit.common.Nullable;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.Comparator;
+import java.util.List;
import java.util.Objects;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ObjectId;
@@ -216,7 +217,7 @@ public abstract class Comment {
public int lineNbr;
public Identity author;
- protected Identity realAuthor;
+ public Identity realAuthor;
// TODO(issue-15525): Migrate this field from Timestamp to Instant
public Timestamp writtenOn;
@@ -227,6 +228,8 @@ public abstract class Comment {
public Range range;
public String tag;
+ @Nullable public List<FixSuggestion> fixSuggestions;
+
/**
* Hex commit SHA1 of the commit of the patchset to which this comment applies. Other classes call
* this "commitId", but this class uses the old ReviewDb term "revId", and this field name is
@@ -300,7 +303,14 @@ public abstract class Comment {
+ (key != null ? nullableLength(key.filename, key.uuid) : 0);
}
- public abstract int getApproximateSize();
+ public int getApproximateSize() {
+ int approximateSize = getCommentFieldApproximateSize();
+ approximateSize +=
+ fixSuggestions != null
+ ? fixSuggestions.stream().mapToInt(FixSuggestion::getApproximateSize).sum()
+ : 0;
+ return approximateSize;
+ }
static int nullableLength(String... strings) {
int length = 0;
@@ -327,7 +337,8 @@ public abstract class Comment {
&& Objects.equals(range, c.range)
&& Objects.equals(tag, c.tag)
&& Objects.equals(revId, c.revId)
- && Objects.equals(serverId, c.serverId);
+ && Objects.equals(serverId, c.serverId)
+ && Objects.equals(fixSuggestions, c.fixSuggestions);
}
@Override
@@ -344,7 +355,8 @@ public abstract class Comment {
range,
tag,
revId,
- serverId);
+ serverId,
+ fixSuggestions);
}
@Override
@@ -364,6 +376,7 @@ public abstract class Comment {
.add("parentUuid", Objects.toString(parentUuid, ""))
.add("range", Objects.toString(range, ""))
.add("revId", Objects.toString(revId, ""))
- .add("tag", Objects.toString(tag, ""));
+ .add("tag", Objects.toString(tag, ""))
+ .add("fixSuggestions", Objects.toString(fixSuggestions, ""));
}
}
diff --git a/java/com/google/gerrit/entities/FixReplacement.java b/java/com/google/gerrit/entities/FixReplacement.java
index fbbf746718..aa15ffcbed 100644
--- a/java/com/google/gerrit/entities/FixReplacement.java
+++ b/java/com/google/gerrit/entities/FixReplacement.java
@@ -14,6 +14,8 @@
package com.google.gerrit.entities;
+import java.util.Objects;
+
public final class FixReplacement {
public final String path;
public final Comment.Range range;
@@ -43,4 +45,20 @@ public final class FixReplacement {
int getApproximateSize() {
return path.length() + replacement.length();
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof FixReplacement)) {
+ return false;
+ }
+ FixReplacement f = (FixReplacement) o;
+ return Objects.equals(path, f.path)
+ && Objects.equals(range, f.range)
+ && Objects.equals(replacement, f.replacement);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(path, range, replacement);
+ }
}
diff --git a/java/com/google/gerrit/entities/FixSuggestion.java b/java/com/google/gerrit/entities/FixSuggestion.java
index 892e324423..737c23e1cd 100644
--- a/java/com/google/gerrit/entities/FixSuggestion.java
+++ b/java/com/google/gerrit/entities/FixSuggestion.java
@@ -15,6 +15,7 @@
package com.google.gerrit.entities;
import java.util.List;
+import java.util.Objects;
public final class FixSuggestion {
public final String fixId;
@@ -47,4 +48,20 @@ public final class FixSuggestion {
+ description.length()
+ replacements.stream().mapToInt(FixReplacement::getApproximateSize).sum();
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof FixSuggestion)) {
+ return false;
+ }
+ FixSuggestion fs = (FixSuggestion) o;
+ return Objects.equals(fixId, fs.fixId)
+ && Objects.equals(description, fs.description)
+ && Objects.equals(replacements, fs.replacements);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(fixId, description, replacements);
+ }
}
diff --git a/java/com/google/gerrit/entities/HumanComment.java b/java/com/google/gerrit/entities/HumanComment.java
index d287fa0e68..1e48f111e8 100644
--- a/java/com/google/gerrit/entities/HumanComment.java
+++ b/java/com/google/gerrit/entities/HumanComment.java
@@ -47,11 +47,6 @@ public class HumanComment extends Comment {
}
@Override
- public int getApproximateSize() {
- return super.getCommentFieldApproximateSize();
- }
-
- @Override
public String toString() {
return toStringHelper().add("unresolved", unresolved).toString();
}
diff --git a/java/com/google/gerrit/entities/LabelType.java b/java/com/google/gerrit/entities/LabelType.java
index 7a3266ecc3..ff4b8f9f32 100644
--- a/java/com/google/gerrit/entities/LabelType.java
+++ b/java/com/google/gerrit/entities/LabelType.java
@@ -20,6 +20,7 @@ import static java.util.stream.Collectors.toList;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import java.util.ArrayList;
import java.util.List;
@@ -39,6 +40,7 @@ public abstract class LabelType {
return create(name, values);
}
+ @CanIgnoreReturnValue
public static String checkName(String name) throws IllegalArgumentException {
checkNameInternal(name);
if ("SUBM".equals(name)) {
@@ -47,6 +49,7 @@ public abstract class LabelType {
return name;
}
+ @CanIgnoreReturnValue
public static String checkNameInternal(String name) throws IllegalArgumentException {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Empty label name");
@@ -246,7 +249,7 @@ public abstract class LabelType {
setRefPatterns(null);
}
- List<LabelValue> valueList = sortValues(getValues());
+ ImmutableList<LabelValue> valueList = sortValues(getValues());
setValues(valueList);
if (!valueList.isEmpty()) {
if (valueList.get(0).getValue() < 0) {
diff --git a/java/com/google/gerrit/entities/NotifyConfig.java b/java/com/google/gerrit/entities/NotifyConfig.java
index 5c0a3dbc51..d3123c4422 100644
--- a/java/com/google/gerrit/entities/NotifyConfig.java
+++ b/java/com/google/gerrit/entities/NotifyConfig.java
@@ -18,6 +18,7 @@ import com.google.auto.value.AutoValue;
import com.google.auto.value.extension.memoized.Memoized;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import java.util.EnumSet;
import java.util.Set;
@@ -74,11 +75,13 @@ public abstract class NotifyConfig implements Comparable<NotifyConfig> {
public abstract Builder setHeader(Header hdr);
+ @CanIgnoreReturnValue
public Builder addGroup(GroupReference group) {
groupsBuilder().add(group);
return this;
}
+ @CanIgnoreReturnValue
public Builder addAddress(Address address) {
addressesBuilder().add(address);
return this;
diff --git a/java/com/google/gerrit/entities/Permission.java b/java/com/google/gerrit/entities/Permission.java
index 0e959e7201..1f2f1510e2 100644
--- a/java/com/google/gerrit/entities/Permission.java
+++ b/java/com/google/gerrit/entities/Permission.java
@@ -18,6 +18,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import java.util.ArrayList;
import java.util.Iterator;
@@ -274,15 +275,18 @@ public abstract class Permission implements Comparable<Permission> {
public abstract Builder setExclusiveGroup(boolean value);
+ @CanIgnoreReturnValue
public Builder modifyRules(Consumer<List<PermissionRule.Builder>> modification) {
modification.accept(rulesBuilders);
return this;
}
+ @CanIgnoreReturnValue
public Builder add(PermissionRule.Builder rule) {
return modifyRules(r -> r.add(rule));
}
+ @CanIgnoreReturnValue
public Builder remove(PermissionRule rule) {
if (rule != null) {
return removeRule(rule.getGroup());
@@ -290,10 +294,12 @@ public abstract class Permission implements Comparable<Permission> {
return this;
}
+ @CanIgnoreReturnValue
public Builder removeRule(GroupReference group) {
return modifyRules(rules -> rules.removeIf(rule -> sameGroup(rule.build(), group)));
}
+ @CanIgnoreReturnValue
public Builder clearRules() {
return modifyRules(r -> r.clear());
}
diff --git a/java/com/google/gerrit/entities/PermissionRule.java b/java/com/google/gerrit/entities/PermissionRule.java
index 1665c1c7cf..706091f258 100644
--- a/java/com/google/gerrit/entities/PermissionRule.java
+++ b/java/com/google/gerrit/entities/PermissionRule.java
@@ -15,6 +15,7 @@
package com.google.gerrit.entities;
import com.google.auto.value.AutoValue;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
@AutoValue
public abstract class PermissionRule implements Comparable<PermissionRule> {
@@ -239,14 +240,17 @@ public abstract class PermissionRule implements Comparable<PermissionRule> {
@AutoValue.Builder
public abstract static class Builder {
+ @CanIgnoreReturnValue
public Builder setDeny() {
return setAction(Action.DENY);
}
+ @CanIgnoreReturnValue
public Builder setBlock() {
return setAction(Action.BLOCK);
}
+ @CanIgnoreReturnValue
public Builder setRange(int newMin, int newMax) {
if (newMax < newMin) {
setMin(newMax);
diff --git a/java/com/google/gerrit/entities/Project.java b/java/com/google/gerrit/entities/Project.java
index 72ca6a9ef8..9c2866cd48 100644
--- a/java/com/google/gerrit/entities/Project.java
+++ b/java/com/google/gerrit/entities/Project.java
@@ -18,6 +18,7 @@ import static java.util.Objects.requireNonNull;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.Immutable;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.InheritableBoolean;
@@ -195,6 +196,7 @@ public abstract class Project {
public abstract static class Builder {
public abstract Builder setDescription(String description);
+ @CanIgnoreReturnValue
public Builder setBooleanConfig(BooleanProjectConfig config, InheritableBoolean val) {
Map<BooleanProjectConfig, InheritableBoolean> map = new HashMap<>(getBooleanConfigs());
map.replace(config, val);
@@ -214,6 +216,7 @@ public abstract class Project {
public abstract Builder setParent(NameKey n);
+ @CanIgnoreReturnValue
public Builder setParent(String n) {
return setParent(n != null ? nameKey(n) : null);
}
diff --git a/java/com/google/gerrit/entities/RobotComment.java b/java/com/google/gerrit/entities/RobotComment.java
index 1d46d3bcf4..a4288cab14 100644
--- a/java/com/google/gerrit/entities/RobotComment.java
+++ b/java/com/google/gerrit/entities/RobotComment.java
@@ -15,16 +15,15 @@
package com.google.gerrit.entities;
import java.time.Instant;
-import java.util.List;
import java.util.Map;
import java.util.Objects;
+@Deprecated
public final class RobotComment extends Comment {
public String robotId;
public String robotRunId;
public String url;
public Map<String, String> properties;
- public List<FixSuggestion> fixSuggestions;
public RobotComment(
Key key,
@@ -64,7 +63,6 @@ public final class RobotComment extends Comment {
.add("robotRunId", robotRunId)
.add("url", url)
.add("properties", Objects.toString(properties, ""))
- .add("fixSuggestions", Objects.toString(fixSuggestions, ""))
.toString();
}
@@ -78,12 +76,11 @@ public final class RobotComment extends Comment {
&& Objects.equals(robotId, c.robotId)
&& Objects.equals(robotRunId, c.robotRunId)
&& Objects.equals(url, c.url)
- && Objects.equals(properties, c.properties)
- && Objects.equals(fixSuggestions, c.fixSuggestions);
+ && Objects.equals(properties, c.properties);
}
@Override
public int hashCode() {
- return Objects.hash(super.hashCode(), robotId, robotRunId, url, properties, fixSuggestions);
+ return Objects.hash(super.hashCode(), robotId, robotRunId, url, properties);
}
}
diff --git a/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java b/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
index fbb2fd7cbb..f9a5aeb292 100644
--- a/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
+++ b/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
@@ -16,6 +16,7 @@ package com.google.gerrit.entities;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import java.util.Optional;
@@ -203,6 +204,7 @@ public abstract class SubmitRequirementExpressionResult {
public abstract Builder status(boolean value);
+ @CanIgnoreReturnValue
public Builder addChildPredicateResult(PredicateResult result) {
childPredicateResultsBuilder().add(result);
return this;
diff --git a/java/com/google/gerrit/entities/SubscribeSection.java b/java/com/google/gerrit/entities/SubscribeSection.java
index 574cae8118..775190744b 100644
--- a/java/com/google/gerrit/entities/SubscribeSection.java
+++ b/java/com/google/gerrit/entities/SubscribeSection.java
@@ -20,6 +20,7 @@ import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@@ -51,12 +52,14 @@ public abstract class SubscribeSection {
abstract ImmutableList.Builder<RefSpec> multiMatchRefSpecsBuilder();
+ @CanIgnoreReturnValue
public Builder addMatchingRefSpec(String matchingRefSpec) {
matchingRefSpecsBuilder()
.add(new RefSpec(matchingRefSpec, RefSpec.WildcardMode.REQUIRE_MATCH));
return this;
}
+ @CanIgnoreReturnValue
public Builder addMultiMatchRefSpec(String multiMatchRefSpec) {
multiMatchRefSpecsBuilder()
.add(new RefSpec(multiMatchRefSpec, RefSpec.WildcardMode.ALLOW_MISMATCH));
diff --git a/java/com/google/gerrit/entities/converter/AccountInputProtoConverter.java b/java/com/google/gerrit/entities/converter/AccountInputProtoConverter.java
new file mode 100644
index 0000000000..c073a5fd7e
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/AccountInputProtoConverter.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.google.gerrit.entities.converter;
+
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.extensions.api.accounts.AccountInput;
+import com.google.gerrit.proto.Entities;
+import com.google.protobuf.Parser;
+
+/**
+ * Proto converter between {@link AccountInput} and {@link
+ * com.google.gerrit.proto.Entities.AccountInput}.
+ */
+@Immutable
+public enum AccountInputProtoConverter
+ implements ProtoConverter<Entities.AccountInput, AccountInput> {
+ INSTANCE;
+
+ @Override
+ public Entities.AccountInput toProto(AccountInput accountInput) {
+ Entities.AccountInput.Builder builder = Entities.AccountInput.newBuilder();
+ if (accountInput.username != null) {
+ builder.setUsername(accountInput.username);
+ }
+ if (accountInput.name != null) {
+ builder.setName(accountInput.name);
+ }
+ if (accountInput.displayName != null) {
+ builder.setDisplayName(accountInput.displayName);
+ }
+ if (accountInput.email != null) {
+ builder.setEmail(accountInput.email);
+ }
+ if (accountInput.sshKey != null) {
+ builder.setSshKey(accountInput.sshKey);
+ }
+ if (accountInput.httpPassword != null) {
+ builder.setHttpPassword(accountInput.httpPassword);
+ }
+ if (accountInput.groups != null) {
+ builder.addAllGroups(accountInput.groups);
+ }
+
+ return builder.build();
+ }
+
+ @Override
+ public AccountInput fromProto(Entities.AccountInput proto) {
+ AccountInput accountInput = new AccountInput();
+ if (proto.hasUsername()) {
+ accountInput.username = proto.getUsername();
+ }
+ if (proto.hasName()) {
+ accountInput.name = proto.getName();
+ }
+ if (proto.hasDisplayName()) {
+ accountInput.displayName = proto.getDisplayName();
+ }
+ if (proto.hasEmail()) {
+ accountInput.email = proto.getEmail();
+ }
+ if (proto.hasSshKey()) {
+ accountInput.sshKey = proto.getSshKey();
+ }
+ if (proto.hasHttpPassword()) {
+ accountInput.httpPassword = proto.getHttpPassword();
+ }
+ if (proto.getGroupsCount() > 0) {
+ accountInput.groups = proto.getGroupsList();
+ }
+ return accountInput;
+ }
+
+ @Override
+ public Parser<Entities.AccountInput> getParser() {
+ return Entities.AccountInput.parser();
+ }
+}
diff --git a/java/com/google/gerrit/entities/converter/ApplyPatchInputProtoConverter.java b/java/com/google/gerrit/entities/converter/ApplyPatchInputProtoConverter.java
new file mode 100644
index 0000000000..fc862b06fb
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/ApplyPatchInputProtoConverter.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.google.gerrit.entities.converter;
+
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
+import com.google.gerrit.proto.Entities;
+import com.google.protobuf.Parser;
+
+/**
+ * Proto converter between {@link ApplyPatchInput} and {@link
+ * com.google.gerrit.proto.Entities.ApplyPatchInput}.
+ */
+@Immutable
+public enum ApplyPatchInputProtoConverter
+ implements ProtoConverter<Entities.ApplyPatchInput, ApplyPatchInput> {
+ INSTANCE;
+
+ @Override
+ public Entities.ApplyPatchInput toProto(ApplyPatchInput applyPatchInput) {
+ Entities.ApplyPatchInput.Builder builder = Entities.ApplyPatchInput.newBuilder();
+ if (applyPatchInput.patch != null) {
+ builder.setPatch(applyPatchInput.patch);
+ }
+ return builder.build();
+ }
+
+ @Override
+ public ApplyPatchInput fromProto(Entities.ApplyPatchInput proto) {
+ ApplyPatchInput applyPatchInput = new ApplyPatchInput();
+ if (proto.hasPatch()) {
+ applyPatchInput.patch = proto.getPatch();
+ }
+ return applyPatchInput;
+ }
+
+ @Override
+ public Parser<Entities.ApplyPatchInput> getParser() {
+ return Entities.ApplyPatchInput.parser();
+ }
+}
diff --git a/java/com/google/gerrit/entities/converter/ChangeInputProtoConverter.java b/java/com/google/gerrit/entities/converter/ChangeInputProtoConverter.java
new file mode 100644
index 0000000000..81c787893e
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/ChangeInputProtoConverter.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.google.gerrit.entities.converter;
+
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.extensions.api.accounts.AccountInput;
+import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyInfo;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.common.MergeInput;
+import com.google.gerrit.proto.Entities;
+import com.google.protobuf.Parser;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Proto converter between {@link ChangeInput} and {@link
+ * com.google.gerrit.proto.Entities.ChangeInput}.
+ */
+@Immutable
+public enum ChangeInputProtoConverter implements ProtoConverter<Entities.ChangeInput, ChangeInput> {
+ INSTANCE;
+
+ private final ProtoConverter<Entities.MergeInput, MergeInput> mergeInputConverter =
+ MergeInputProtoConverter.INSTANCE;
+ private final ProtoConverter<Entities.ApplyPatchInput, ApplyPatchInput> applyPatchInputConverter =
+ ApplyPatchInputProtoConverter.INSTANCE;
+ private final ProtoConverter<Entities.AccountInput, AccountInput> accountInputConverter =
+ AccountInputProtoConverter.INSTANCE;
+ private final ProtoConverter<Entities.NotifyInfo, NotifyInfo> notifyInfoConverter =
+ NotifyInfoProtoConverter.INSTANCE;
+
+ @Override
+ public Entities.ChangeInput toProto(ChangeInput changeInput) {
+ Entities.ChangeInput.Builder builder = Entities.ChangeInput.newBuilder();
+ if (changeInput.project != null) {
+ builder.setProject(changeInput.project);
+ }
+ if (changeInput.branch != null) {
+ builder.setBranch(changeInput.branch);
+ }
+ if (changeInput.subject != null) {
+ builder.setSubject(changeInput.subject);
+ }
+ if (changeInput.topic != null) {
+ builder.setTopic(changeInput.topic);
+ }
+ if (changeInput.status != null) {
+ builder.setStatus(Entities.ChangeStatus.forNumber(changeInput.status.getValue()));
+ }
+ if (changeInput.isPrivate != null) {
+ builder.setIsPrivate(changeInput.isPrivate);
+ }
+ if (changeInput.workInProgress != null) {
+ builder.setWorkInProgress(changeInput.workInProgress);
+ }
+ if (changeInput.baseChange != null) {
+ builder.setBaseChange(changeInput.baseChange);
+ }
+ if (changeInput.baseCommit != null) {
+ builder.setBaseCommit(changeInput.baseCommit);
+ }
+ if (changeInput.newBranch != null) {
+ builder.setNewBranch(changeInput.newBranch);
+ }
+ if (changeInput.validationOptions != null) {
+ builder.putAllValidationOptions(changeInput.validationOptions);
+ }
+ if (changeInput.customKeyedValues != null) {
+ builder.putAllCustomKeyedValues(changeInput.customKeyedValues);
+ }
+ if (changeInput.merge != null) {
+ builder.setMerge(mergeInputConverter.toProto(changeInput.merge));
+ }
+ if (changeInput.patch != null) {
+ builder.setPatch(applyPatchInputConverter.toProto(changeInput.patch));
+ }
+ if (changeInput.author != null) {
+ builder.setAuthor(accountInputConverter.toProto(changeInput.author));
+ }
+ builder.setNotify(Entities.NotifyHandling.forNumber(changeInput.notify.getValue()));
+
+ List<ListChangesOption> responseFormatOptions = changeInput.responseFormatOptions;
+ if (responseFormatOptions != null) {
+ for (ListChangesOption option : responseFormatOptions) {
+ builder.addResponseFormatOptions(Entities.ListChangesOption.forNumber(option.getValue()));
+ }
+ }
+
+ if (changeInput.notifyDetails != null) {
+ Map<RecipientType, NotifyInfo> notifyDetails = changeInput.notifyDetails;
+ for (Map.Entry<RecipientType, NotifyInfo> entry : notifyDetails.entrySet()) {
+ Entities.RecipientType recipientType =
+ Entities.RecipientType.forNumber(entry.getKey().getValue());
+ builder.putNotifyDetails(
+ recipientType.name(), notifyInfoConverter.toProto(entry.getValue()));
+ }
+ }
+ return builder.build();
+ }
+
+ @Override
+ public ChangeInput fromProto(Entities.ChangeInput proto) {
+ ChangeInput changeInput =
+ new ChangeInput(proto.getProject(), proto.getBranch(), proto.getSubject());
+ if (proto.hasTopic()) {
+ changeInput.topic = proto.getTopic();
+ }
+ if (proto.hasStatus()) {
+ changeInput.status = ChangeStatus.valueOf(proto.getStatus().name());
+ }
+ if (proto.hasIsPrivate()) {
+ changeInput.isPrivate = proto.getIsPrivate();
+ }
+ if (proto.hasWorkInProgress()) {
+ changeInput.workInProgress = proto.getWorkInProgress();
+ }
+ if (proto.hasBaseChange()) {
+ changeInput.baseChange = proto.getBaseChange();
+ }
+ if (proto.hasBaseCommit()) {
+ changeInput.baseCommit = proto.getBaseCommit();
+ }
+ if (proto.hasNewBranch()) {
+ changeInput.newBranch = proto.getNewBranch();
+ }
+ if (proto.getValidationOptionsCount() > 0) {
+ changeInput.validationOptions = proto.getValidationOptionsMap();
+ }
+ if (proto.getCustomKeyedValuesCount() > 0) {
+ changeInput.customKeyedValues = proto.getCustomKeyedValuesMap();
+ }
+ if (proto.hasMerge()) {
+ changeInput.merge = mergeInputConverter.fromProto(proto.getMerge());
+ }
+ if (proto.hasPatch()) {
+ changeInput.patch = applyPatchInputConverter.fromProto(proto.getPatch());
+ }
+ if (proto.hasAuthor()) {
+ changeInput.author = accountInputConverter.fromProto(proto.getAuthor());
+ }
+ if (proto.getResponseFormatOptionsCount() > 0) {
+ changeInput.responseFormatOptions = new ArrayList<ListChangesOption>();
+ for (Entities.ListChangesOption option : proto.getResponseFormatOptionsList()) {
+ changeInput.responseFormatOptions.add(ListChangesOption.valueOf(option.name()));
+ }
+ }
+
+ changeInput.notify = NotifyHandling.valueOf(proto.getNotify().name());
+
+ if (proto.getNotifyDetailsCount() > 0) {
+ changeInput.notifyDetails = new HashMap<RecipientType, NotifyInfo>();
+ for (Map.Entry<String, Entities.NotifyInfo> entry : proto.getNotifyDetailsMap().entrySet()) {
+ changeInput.notifyDetails.put(
+ RecipientType.valueOf(entry.getKey()), notifyInfoConverter.fromProto(entry.getValue()));
+ }
+ }
+
+ return changeInput;
+ }
+
+ @Override
+ public Parser<Entities.ChangeInput> getParser() {
+ return Entities.ChangeInput.parser();
+ }
+}
diff --git a/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java b/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
index 3b772d0b48..93cacaf8cc 100644
--- a/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
@@ -79,6 +79,10 @@ public enum ChangeProtoConverter implements ProtoConverter<Entities.Change, Chan
if (cherryPickOf != null) {
builder.setCherryPickOf(patchSetIdConverter.toProto(cherryPickOf));
}
+ String serverId = change.getServerId();
+ if (serverId != null) {
+ builder.setServerId(serverId);
+ }
return builder.build();
}
@@ -119,6 +123,9 @@ public enum ChangeProtoConverter implements ProtoConverter<Entities.Change, Chan
if (proto.hasCherryPickOf()) {
change.setCherryPickOf(patchSetIdConverter.fromProto(proto.getCherryPickOf()));
}
+ if (proto.hasServerId()) {
+ change.setServerId(proto.getServerId());
+ }
return change;
}
diff --git a/java/com/google/gerrit/entities/converter/HumanCommentProtoConverter.java b/java/com/google/gerrit/entities/converter/HumanCommentProtoConverter.java
new file mode 100644
index 0000000000..6e8c907caa
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/HumanCommentProtoConverter.java
@@ -0,0 +1,145 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.entities.converter;
+
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
+
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.Comment.Range;
+import com.google.gerrit.entities.HumanComment;
+import com.google.gerrit.proto.Entities;
+import com.google.gerrit.proto.Entities.HumanComment.InFilePosition;
+import com.google.gerrit.proto.Entities.HumanComment.InFilePosition.Side;
+import com.google.protobuf.Parser;
+import java.time.Instant;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * Proto converter between {@link HumanComment} and {@link
+ * com.google.gerrit.proto.Entities.HumanComment}.
+ */
+@Immutable
+public enum HumanCommentProtoConverter
+ implements ProtoConverter<Entities.HumanComment, HumanComment> {
+ INSTANCE;
+
+ private final ProtoConverter<Entities.Account_Id, Account.Id> accountIdConverter =
+ AccountIdProtoConverter.INSTANCE;
+ private final ProtoConverter<Entities.ObjectId, ObjectId> objectIdConverter =
+ ObjectIdProtoConverter.INSTANCE;
+
+ @Override
+ public Entities.HumanComment toProto(HumanComment val) {
+
+ Entities.HumanComment.Builder res =
+ Entities.HumanComment.newBuilder()
+ .setPatchsetId(val.key.patchSetId)
+ .setAccountId(accountIdConverter.toProto(val.author.getId()))
+ .setCommentUuid(val.key.uuid)
+ .setCommentText(val.message)
+ .setUnresolved(val.unresolved)
+ .setWrittenOnMillis(val.writtenOn.toInstant().toEpochMilli())
+ .setServerId(val.serverId);
+ if (!val.key.filename.equals(PATCHSET_LEVEL)) {
+ InFilePosition.Builder inFilePos =
+ InFilePosition.newBuilder()
+ .setFilePath(val.key.filename)
+ .setSide(val.side <= 0 ? Side.PARENT : Side.REVISION);
+ if (val.range != null) {
+ inFilePos.setPositionRange(
+ InFilePosition.Range.newBuilder()
+ .setStartLine(val.range.startLine)
+ .setStartChar(val.range.startChar)
+ .setEndLine(val.range.endLine)
+ .setEndChar(val.range.endChar));
+ }
+ if (val.lineNbr != 0) {
+ inFilePos.setLineNumber(val.lineNbr);
+ }
+ res.setInFilePosition(inFilePos);
+ }
+
+ if (val.parentUuid != null) {
+ res.setParentCommentUuid(val.parentUuid);
+ }
+ if (val.tag != null) {
+ res.setTag(val.tag);
+ }
+ if (val.realAuthor != null) {
+ res.setRealAuthor(accountIdConverter.toProto(val.realAuthor.getId()));
+ }
+ if (val.getCommitId() != null) {
+ res.setDestCommitId(objectIdConverter.toProto(val.getCommitId()));
+ }
+
+ return res.build();
+ }
+
+ @Override
+ public HumanComment fromProto(Entities.HumanComment proto) {
+ Optional<InFilePosition> optInFilePosition =
+ proto.hasInFilePosition() ? Optional.of(proto.getInFilePosition()) : Optional.empty();
+ Comment.Key key =
+ new Comment.Key(
+ proto.getCommentUuid(),
+ optInFilePosition.isPresent() ? optInFilePosition.get().getFilePath() : PATCHSET_LEVEL,
+ proto.getPatchsetId());
+ HumanComment res =
+ new HumanComment(
+ key,
+ accountIdConverter.fromProto(proto.getAccountId()),
+ Instant.ofEpochMilli(proto.getWrittenOnMillis()),
+ optInFilePosition.isPresent()
+ ? (short) optInFilePosition.get().getSide().getNumber()
+ : Side.REVISION_VALUE,
+ proto.getCommentText(),
+ proto.getServerId(),
+ proto.getUnresolved());
+
+ res.parentUuid = proto.hasParentCommentUuid() ? proto.getParentCommentUuid() : null;
+ res.tag = proto.hasTag() ? proto.getTag() : null;
+ if (proto.hasRealAuthor()) {
+ res.realAuthor = new Comment.Identity(accountIdConverter.fromProto(proto.getRealAuthor()));
+ }
+ if (proto.hasDestCommitId()) {
+ res.setCommitId(objectIdConverter.fromProto(proto.getDestCommitId()));
+ }
+
+ optInFilePosition.ifPresent(
+ inFilePosition -> {
+ if (inFilePosition.hasPositionRange()) {
+ var range = inFilePosition.getPositionRange();
+ res.range =
+ new Range(
+ range.getStartLine(),
+ range.getStartChar(),
+ range.getEndLine(),
+ range.getEndChar());
+ }
+ if (inFilePosition.hasLineNumber()) {
+ res.lineNbr = inFilePosition.getLineNumber();
+ }
+ });
+ return res;
+ }
+
+ @Override
+ public Parser<Entities.HumanComment> getParser() {
+ return Entities.HumanComment.parser();
+ }
+}
diff --git a/java/com/google/gerrit/entities/converter/MergeInputProtoConverter.java b/java/com/google/gerrit/entities/converter/MergeInputProtoConverter.java
new file mode 100644
index 0000000000..11f78a4bfe
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/MergeInputProtoConverter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.google.gerrit.entities.converter;
+
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.extensions.common.MergeInput;
+import com.google.gerrit.proto.Entities;
+import com.google.protobuf.Parser;
+
+/**
+ * Proto converter between {@link MergeInput} and {@link
+ * com.google.gerrit.proto.Entities.MergeInput}.
+ */
+@Immutable
+public enum MergeInputProtoConverter implements ProtoConverter<Entities.MergeInput, MergeInput> {
+ INSTANCE;
+
+ @Override
+ public Entities.MergeInput toProto(MergeInput mergeInput) {
+ Entities.MergeInput.Builder builder = Entities.MergeInput.newBuilder();
+ if (mergeInput.source != null) {
+ builder.setSource(mergeInput.source);
+ }
+ if (mergeInput.sourceBranch != null) {
+ builder.setSourceBranch(mergeInput.sourceBranch);
+ }
+ if (mergeInput.strategy != null) {
+ builder.setStrategy(mergeInput.strategy);
+ }
+ builder.setAllowConflicts(mergeInput.allowConflicts);
+ return builder.build();
+ }
+
+ @Override
+ public MergeInput fromProto(Entities.MergeInput proto) {
+ MergeInput mergeInput = new MergeInput();
+ if (proto.hasSource()) {
+ mergeInput.source = proto.getSource();
+ }
+ if (proto.hasSourceBranch()) {
+ mergeInput.sourceBranch = proto.getSourceBranch();
+ }
+ if (proto.hasStrategy()) {
+ mergeInput.strategy = proto.getStrategy();
+ }
+ if (proto.hasAllowConflicts()) {
+ mergeInput.allowConflicts = proto.getAllowConflicts();
+ }
+ return mergeInput;
+ }
+
+ @Override
+ public Parser<Entities.MergeInput> getParser() {
+ return Entities.MergeInput.parser();
+ }
+}
diff --git a/java/com/google/gerrit/entities/converter/NotifyInfoProtoConverter.java b/java/com/google/gerrit/entities/converter/NotifyInfoProtoConverter.java
new file mode 100644
index 0000000000..201dd78bde
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/NotifyInfoProtoConverter.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.google.gerrit.entities.converter;
+
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.extensions.api.changes.NotifyInfo;
+import com.google.gerrit.proto.Entities;
+import com.google.protobuf.Parser;
+
+/**
+ * Proto converter between {@link NotifyInfo} and {@link
+ * com.google.gerrit.proto.Entities.NotifyInfo}.
+ */
+@Immutable
+public enum NotifyInfoProtoConverter implements ProtoConverter<Entities.NotifyInfo, NotifyInfo> {
+ INSTANCE;
+
+ @Override
+ public Entities.NotifyInfo toProto(NotifyInfo notifyInfo) {
+ Entities.NotifyInfo.Builder builder = Entities.NotifyInfo.newBuilder();
+ if (notifyInfo.accounts != null) {
+ builder.addAllAccounts(notifyInfo.accounts);
+ }
+ return builder.build();
+ }
+
+ @Override
+ public NotifyInfo fromProto(Entities.NotifyInfo proto) {
+ return new NotifyInfo(proto.getAccountsList());
+ }
+
+ @Override
+ public Parser<Entities.NotifyInfo> getParser() {
+ return Entities.NotifyInfo.parser();
+ }
+}
diff --git a/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java b/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
index a3b4abfa79..196decaa7f 100644
--- a/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
@@ -21,7 +21,6 @@ import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.proto.Entities;
import com.google.protobuf.Parser;
import java.time.Instant;
-import java.util.List;
import org.eclipse.jgit.lib.ObjectId;
@Immutable
@@ -45,7 +44,7 @@ public enum PatchSetProtoConverter implements ProtoConverter<Entities.PatchSet,
.setRealUploaderAccountId(accountIdConverter.toProto(patchSet.realUploader()))
.setCreatedOn(patchSet.createdOn().toEpochMilli());
patchSet.branch().ifPresent(builder::setBranch);
- List<String> groups = patchSet.groups();
+ ImmutableList<String> groups = patchSet.groups();
if (!groups.isEmpty()) {
builder.setGroups(PatchSet.joinGroups(groups));
}
diff --git a/java/com/google/gerrit/entities/converter/package-info.java b/java/com/google/gerrit/entities/converter/package-info.java
new file mode 100644
index 0000000000..51ea401286
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.entities.converter;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/entities/package-info.java b/java/com/google/gerrit/entities/package-info.java
new file mode 100644
index 0000000000..f1cde1b375
--- /dev/null
+++ b/java/com/google/gerrit/entities/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.entities;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/exceptions/BUILD b/java/com/google/gerrit/exceptions/BUILD
index 873b659c53..b31d12eab2 100644
--- a/java/com/google/gerrit/exceptions/BUILD
+++ b/java/com/google/gerrit/exceptions/BUILD
@@ -7,5 +7,6 @@ java_library(
deps = [
"//java/com/google/gerrit/entities",
"//lib:jgit",
+ "//lib/errorprone:annotations",
],
)
diff --git a/java/com/google/gerrit/exceptions/package-info.java b/java/com/google/gerrit/exceptions/package-info.java
new file mode 100644
index 0000000000..ce52b4a2e0
--- /dev/null
+++ b/java/com/google/gerrit/exceptions/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.exceptions;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/annotations/package-info.java b/java/com/google/gerrit/extensions/annotations/package-info.java
new file mode 100644
index 0000000000..f338b5b40e
--- /dev/null
+++ b/java/com/google/gerrit/extensions/annotations/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.annotations;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/api/access/package-info.java b/java/com/google/gerrit/extensions/api/access/package-info.java
new file mode 100644
index 0000000000..65e2d755f2
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/access/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.api.access;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/api/accounts/AccountApi.java b/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
index 0d019aa424..e40f82e8ca 100644
--- a/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
+++ b/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
@@ -14,6 +14,7 @@
package com.google.gerrit.extensions.api.accounts;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.EditPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
@@ -44,18 +45,22 @@ public interface AccountApi {
GeneralPreferencesInfo getPreferences() throws RestApiException;
+ @CanIgnoreReturnValue
GeneralPreferencesInfo setPreferences(GeneralPreferencesInfo in) throws RestApiException;
DiffPreferencesInfo getDiffPreferences() throws RestApiException;
+ @CanIgnoreReturnValue
DiffPreferencesInfo setDiffPreferences(DiffPreferencesInfo in) throws RestApiException;
EditPreferencesInfo getEditPreferences() throws RestApiException;
+ @CanIgnoreReturnValue
EditPreferencesInfo setEditPreferences(EditPreferencesInfo in) throws RestApiException;
List<ProjectWatchInfo> getWatchedProjects() throws RestApiException;
+ @CanIgnoreReturnValue
List<ProjectWatchInfo> setWatchedProjects(List<ProjectWatchInfo> in) throws RestApiException;
void deleteWatchedProjects(List<ProjectWatchInfo> in) throws RestApiException;
@@ -72,6 +77,7 @@ public interface AccountApi {
void deleteEmail(String email) throws RestApiException;
+ @CanIgnoreReturnValue
EmailApi createEmail(EmailInput emailInput) throws RestApiException;
EmailApi email(String email) throws RestApiException;
@@ -82,12 +88,14 @@ public interface AccountApi {
List<SshKeyInfo> listSshKeys() throws RestApiException;
+ @CanIgnoreReturnValue
SshKeyInfo addSshKey(String key) throws RestApiException;
void deleteSshKey(int seq) throws RestApiException;
Map<String, GpgKeyInfo> listGpgKeys() throws RestApiException;
+ @CanIgnoreReturnValue
Map<String, GpgKeyInfo> putGpgKeys(List<String> add, List<String> remove) throws RestApiException;
GpgKeyApi gpgKey(String id) throws RestApiException;
@@ -102,6 +110,7 @@ public interface AccountApi {
void deleteExternalIds(List<String> externalIds) throws RestApiException;
+ @CanIgnoreReturnValue
List<DeletedDraftCommentInfo> deleteDraftComments(DeleteDraftCommentsInput input)
throws RestApiException;
@@ -122,6 +131,7 @@ public interface AccountApi {
* @param httpPassword the new password, {@code null} to remove the password.
* @return the new password, {@code null} if the password was removed.
*/
+ @CanIgnoreReturnValue
String setHttpPassword(String httpPassword) throws RestApiException;
void delete() throws RestApiException;
diff --git a/java/com/google/gerrit/extensions/api/accounts/Accounts.java b/java/com/google/gerrit/extensions/api/accounts/Accounts.java
index 285b385896..ad0d385503 100644
--- a/java/com/google/gerrit/extensions/api/accounts/Accounts.java
+++ b/java/com/google/gerrit/extensions/api/accounts/Accounts.java
@@ -14,6 +14,7 @@
package com.google.gerrit.extensions.api.accounts;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.client.ListAccountsOption;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -53,9 +54,11 @@ public interface Accounts {
AccountApi self() throws RestApiException;
/** Create a new account with the given username and default options. */
+ @CanIgnoreReturnValue
AccountApi create(String username) throws RestApiException;
/** Create a new account. */
+ @CanIgnoreReturnValue
AccountApi create(AccountInput input) throws RestApiException;
/**
diff --git a/java/com/google/gerrit/extensions/api/accounts/package-info.java b/java/com/google/gerrit/extensions/api/accounts/package-info.java
new file mode 100644
index 0000000000..5bd477adea
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/accounts/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.api.accounts;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index d8fd727bcc..1c83bc2277 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -17,6 +17,7 @@ package com.google.gerrit.extensions.api.changes;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.ReviewerState;
@@ -25,6 +26,7 @@ import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInfoDifference;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.common.CommitMessageInfo;
import com.google.gerrit.extensions.common.CommitMessageInput;
import com.google.gerrit.extensions.common.MergePatchSetInput;
import com.google.gerrit.extensions.common.PureRevertInfo;
@@ -130,31 +132,29 @@ public interface ChangeApi {
setReadyForReview(null);
}
- /**
- * Create a new change that reverts this change.
- *
- * @see Changes#id(int)
- */
+ /** Create a new change that reverts this change. */
+ @CanIgnoreReturnValue
default ChangeApi revert() throws RestApiException {
return revert(new RevertInput());
}
- /**
- * Create a new change that reverts this change.
- *
- * @see Changes#id(int)
- */
+ /** Create a new change that reverts this change. */
+ @CanIgnoreReturnValue
ChangeApi revert(RevertInput in) throws RestApiException;
+ @CanIgnoreReturnValue
default RevertSubmissionInfo revertSubmission() throws RestApiException {
return revertSubmission(new RevertInput());
}
+ @CanIgnoreReturnValue
RevertSubmissionInfo revertSubmission(RevertInput in) throws RestApiException;
/** Create a merge patch set for the change. */
+ @CanIgnoreReturnValue
ChangeInfo createMergePatchSet(MergePatchSetInput in) throws RestApiException;
+ @CanIgnoreReturnValue
ChangeInfo applyPatch(ApplyPatchPatchSetInput in) throws RestApiException;
default List<ChangeInfo> submittedTogether() throws RestApiException {
@@ -187,6 +187,7 @@ public interface ChangeApi {
* @return a {@code RebaseChainInfo} contains the {@code ChangeInfo} data for the rebased the
* chain
*/
+ @CanIgnoreReturnValue
default Response<RebaseChainInfo> rebaseChain() throws RestApiException {
return rebaseChain(new RebaseInput());
}
@@ -197,6 +198,7 @@ public interface ChangeApi {
* @return a {@code RebaseChainInfo} contains the {@code ChangeInfo} data for the rebased the
* chain
*/
+ @CanIgnoreReturnValue
Response<RebaseChainInfo> rebaseChain(RebaseInput in) throws RestApiException;
/** Deletes a change. */
@@ -208,12 +210,14 @@ public interface ChangeApi {
IncludedInInfo includedIn() throws RestApiException;
+ @CanIgnoreReturnValue
default ReviewerResult addReviewer(String reviewer) throws RestApiException {
ReviewerInput in = new ReviewerInput();
in.reviewer = reviewer;
return addReviewer(in);
}
+ @CanIgnoreReturnValue
ReviewerResult addReviewer(ReviewerInput in) throws RestApiException;
SuggestedReviewersRequest suggestReviewers() throws RestApiException;
@@ -318,6 +322,8 @@ public interface ChangeApi {
*/
ChangeEditApi edit() throws RestApiException;
+ CommitMessageInfo getMessage() throws RestApiException;
+
/** Create a new patch set with a new commit message. */
default void setMessage(String message) throws RestApiException {
CommitMessageInput in = new CommitMessageInput();
@@ -356,6 +362,7 @@ public interface ChangeApi {
AttentionSetApi attention(String id) throws RestApiException;
/** Adds a user to the attention set. */
+ @CanIgnoreReturnValue
AccountInfo addToAttentionSet(AttentionSetInput input) throws RestApiException;
/**
@@ -470,9 +477,11 @@ public interface ChangeApi {
void index() throws RestApiException;
/** Check if this change is a pure revert of the change stored in revertOf. */
+ @CanIgnoreReturnValue
PureRevertInfo pureRevert() throws RestApiException;
/** Check if this change is a pure revert of claimedOriginal (SHA1 in 40 digit hex). */
+ @CanIgnoreReturnValue
PureRevertInfo pureRevert(String claimedOriginal) throws RestApiException;
/**
@@ -711,6 +720,11 @@ public interface ChangeApi {
}
@Override
+ public CommitMessageInfo getMessage() throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public void setMessage(CommitMessageInput in) throws RestApiException {
throw new NotImplementedException();
}
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java
index a26068a5b3..2fd8a072cc 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java
@@ -196,6 +196,18 @@ public interface ChangeEditApi {
void modifyCommitMessage(String newCommitMessage) throws RestApiException;
/**
+ * Updates the author/committer of the change edit. If the change edit doesn't exist, it will be
+ * created based on the current patch set of the change.
+ *
+ * @param name the name of the author/committer
+ * @param email the email of the author/committer
+ * @param type the type of the identity being edited
+ * @throws RestApiException if the author/committer identity couldn't be updated
+ */
+ void modifyIdentity(String name, String email, ChangeEditIdentityType type)
+ throws RestApiException;
+
+ /**
* A default implementation which allows source compatibility when adding new methods to the
* interface.
*/
@@ -269,5 +281,11 @@ public interface ChangeEditApi {
public void modifyCommitMessage(String newCommitMessage) throws RestApiException {
throw new NotImplementedException();
}
+
+ @Override
+ public void modifyIdentity(String name, String email, ChangeEditIdentityType type)
+ throws RestApiException {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeEditIdentityType.java b/java/com/google/gerrit/extensions/api/changes/ChangeEditIdentityType.java
new file mode 100644
index 0000000000..77d3f2e214
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeEditIdentityType.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.extensions.api.changes;
+
+public enum ChangeEditIdentityType {
+ AUTHOR,
+ COMMITTER
+}
diff --git a/java/com/google/gerrit/extensions/api/changes/Changes.java b/java/com/google/gerrit/extensions/api/changes/Changes.java
index ea2a158f00..5e3d08c254 100644
--- a/java/com/google/gerrit/extensions/api/changes/Changes.java
+++ b/java/com/google/gerrit/extensions/api/changes/Changes.java
@@ -16,6 +16,7 @@ package com.google.gerrit.extensions.api.changes;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
@@ -73,6 +74,7 @@ public interface Changes {
*/
ChangeApi id(String project, int id) throws RestApiException;
+ @CanIgnoreReturnValue
ChangeApi create(ChangeInput in) throws RestApiException;
ChangeInfo createAsInfo(ChangeInput in) throws RestApiException;
diff --git a/java/com/google/gerrit/extensions/api/changes/CommentApi.java b/java/com/google/gerrit/extensions/api/changes/CommentApi.java
index 889175ef19..9b5e1dae65 100644
--- a/java/com/google/gerrit/extensions/api/changes/CommentApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/CommentApi.java
@@ -14,6 +14,7 @@
package com.google.gerrit.extensions.api.changes;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -30,6 +31,7 @@ public interface CommentApi {
*
* @return the comment with its message updated.
*/
+ @CanIgnoreReturnValue
CommentInfo delete(DeleteCommentInput input) throws RestApiException;
/**
diff --git a/java/com/google/gerrit/extensions/api/changes/DraftApi.java b/java/com/google/gerrit/extensions/api/changes/DraftApi.java
index fa663a501e..50816b7c02 100644
--- a/java/com/google/gerrit/extensions/api/changes/DraftApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/DraftApi.java
@@ -14,11 +14,13 @@
package com.google.gerrit.extensions.api.changes;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
public interface DraftApi extends CommentApi {
+ @CanIgnoreReturnValue
CommentInfo update(DraftInput in) throws RestApiException;
void delete() throws RestApiException;
diff --git a/java/com/google/gerrit/extensions/api/changes/NotifyHandling.java b/java/com/google/gerrit/extensions/api/changes/NotifyHandling.java
index 98ef31cb9d..f8a769c91c 100644
--- a/java/com/google/gerrit/extensions/api/changes/NotifyHandling.java
+++ b/java/com/google/gerrit/extensions/api/changes/NotifyHandling.java
@@ -15,8 +15,18 @@
package com.google.gerrit.extensions.api.changes;
public enum NotifyHandling {
- NONE,
- OWNER,
- OWNER_REVIEWERS,
- ALL
+ NONE(0),
+ OWNER(1),
+ OWNER_REVIEWERS(2),
+ ALL(3);
+
+ private final int value;
+
+ NotifyHandling(int v) {
+ this.value = v;
+ }
+
+ public int getValue() {
+ return value;
+ }
}
diff --git a/java/com/google/gerrit/extensions/api/changes/RecipientType.java b/java/com/google/gerrit/extensions/api/changes/RecipientType.java
index 3ddc597463..91b2706d37 100644
--- a/java/com/google/gerrit/extensions/api/changes/RecipientType.java
+++ b/java/com/google/gerrit/extensions/api/changes/RecipientType.java
@@ -15,7 +15,17 @@
package com.google.gerrit.extensions.api.changes;
public enum RecipientType {
- TO,
- CC,
- BCC
+ TO(0),
+ CC(1),
+ BCC(2);
+
+ private final int value;
+
+ RecipientType(int v) {
+ this.value = v;
+ }
+
+ public int getValue() {
+ return value;
+ }
}
diff --git a/java/com/google/gerrit/extensions/api/changes/ReviewInput.java b/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index 98807cbb2c..2584448632 100644
--- a/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
@@ -22,7 +22,6 @@ import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.Comment;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.ReviewerState;
-import com.google.gerrit.extensions.common.FixSuggestionInfo;
import com.google.gerrit.extensions.restapi.DefaultInput;
import java.util.ArrayList;
import java.util.Collections;
@@ -39,7 +38,7 @@ public class ReviewInput {
public Map<String, Short> labels;
public Map<String, List<CommentInput>> comments;
- public Map<String, List<RobotCommentInput>> robotComments;
+ @Deprecated public Map<String, List<RobotCommentInput>> robotComments;
/**
* How to process draft comments already in the database that were not also described in this
@@ -115,12 +114,12 @@ public class ReviewInput {
public Boolean unresolved;
}
+ @Deprecated
public static class RobotCommentInput extends Comment {
public String robotId;
public String robotRunId;
public String url;
public Map<String, String> properties;
- public List<FixSuggestionInfo> fixSuggestions;
}
@CanIgnoreReturnValue
diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 73fc170c5c..69cf25d41b 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -15,6 +15,7 @@
package com.google.gerrit.extensions.api.changes;
import com.google.common.collect.ListMultimap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.ArchiveFormat;
import com.google.gerrit.extensions.client.SubmitType;
@@ -44,24 +45,30 @@ public interface RevisionApi {
void description(String description) throws RestApiException;
+ @CanIgnoreReturnValue
ReviewResult review(ReviewInput in) throws RestApiException;
+ @CanIgnoreReturnValue
default ChangeInfo submit() throws RestApiException {
SubmitInput in = new SubmitInput();
return submit(in);
}
+ @CanIgnoreReturnValue
ChangeInfo submit(SubmitInput in) throws RestApiException;
+ @CanIgnoreReturnValue
ChangeApi cherryPick(CherryPickInput in) throws RestApiException;
ChangeInfo cherryPickAsInfo(CherryPickInput in) throws RestApiException;
+ @CanIgnoreReturnValue
default ChangeApi rebase() throws RestApiException {
RebaseInput in = new RebaseInput();
return rebase(in);
}
+ @CanIgnoreReturnValue
ChangeApi rebase(RebaseInput in) throws RestApiException;
ChangeInfo rebaseAsInfo(RebaseInput in) throws RestApiException;
@@ -117,6 +124,7 @@ public interface RevisionApi {
* @param fixId the ID of the fix which should be applied
* @throws RestApiException if the fix couldn't be applied
*/
+ @CanIgnoreReturnValue
EditInfo applyFix(String fixId) throws RestApiException;
/**
@@ -126,6 +134,7 @@ public interface RevisionApi {
* @param applyProvidedFixInput The fix(es) to apply to a new change edit.
* @throws RestApiException if the fix couldn't be applied.
*/
+ @CanIgnoreReturnValue
EditInfo applyFix(ApplyProvidedFixInput applyProvidedFixInput) throws RestApiException;
Map<String, DiffInfo> getFixPreview(String fixId) throws RestApiException;
@@ -133,6 +142,7 @@ public interface RevisionApi {
Map<String, DiffInfo> getFixPreview(ApplyProvidedFixInput applyProvidedFixInput)
throws RestApiException;
+ @CanIgnoreReturnValue
DraftApi createDraft(DraftInput in) throws RestApiException;
DraftApi draft(String id) throws RestApiException;
diff --git a/java/com/google/gerrit/extensions/api/changes/package-info.java b/java/com/google/gerrit/extensions/api/changes/package-info.java
new file mode 100644
index 0000000000..f54821238f
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/changes/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.api.changes;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/api/config/ExperimentApi.java b/java/com/google/gerrit/extensions/api/config/ExperimentApi.java
new file mode 100644
index 0000000000..fb30884b74
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/config/ExperimentApi.java
@@ -0,0 +1,30 @@
+// Copyright (C) 20124 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.extensions.api.config;
+
+import com.google.gerrit.extensions.common.ExperimentInfo;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public interface ExperimentApi {
+ ExperimentInfo get() throws RestApiException;
+
+ class NotImplemented implements ExperimentApi {
+ @Override
+ public ExperimentInfo get() throws RestApiException {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/java/com/google/gerrit/extensions/api/config/Server.java b/java/com/google/gerrit/extensions/api/config/Server.java
index 8b69dedc5f..26806d1ed4 100644
--- a/java/com/google/gerrit/extensions/api/config/Server.java
+++ b/java/com/google/gerrit/extensions/api/config/Server.java
@@ -14,9 +14,12 @@
package com.google.gerrit.extensions.api.config;
+import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.EditPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.common.ExperimentInfo;
import com.google.gerrit.extensions.common.ServerInfo;
import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -31,20 +34,42 @@ public interface Server {
GeneralPreferencesInfo getDefaultPreferences() throws RestApiException;
+ @CanIgnoreReturnValue
GeneralPreferencesInfo setDefaultPreferences(GeneralPreferencesInfo in) throws RestApiException;
DiffPreferencesInfo getDefaultDiffPreferences() throws RestApiException;
+ @CanIgnoreReturnValue
DiffPreferencesInfo setDefaultDiffPreferences(DiffPreferencesInfo in) throws RestApiException;
EditPreferencesInfo getDefaultEditPreferences() throws RestApiException;
+ @CanIgnoreReturnValue
EditPreferencesInfo setDefaultEditPreferences(EditPreferencesInfo in) throws RestApiException;
ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException;
List<TopMenu.MenuEntry> topMenus() throws RestApiException;
+ ExperimentApi experiment(String name) throws RestApiException;
+
+ ListExperimentsRequest listExperiments() throws RestApiException;
+
+ abstract class ListExperimentsRequest {
+ private boolean enabledOnly;
+
+ public abstract ImmutableMap<String, ExperimentInfo> get() throws RestApiException;
+
+ public ListExperimentsRequest enabledOnly() {
+ enabledOnly = true;
+ return this;
+ }
+
+ public boolean getEnabledOnly() {
+ return enabledOnly;
+ }
+ }
+
/**
* A default implementation which allows source compatibility when adding new methods to the
* interface.
@@ -102,5 +127,15 @@ public interface Server {
public List<TopMenu.MenuEntry> topMenus() throws RestApiException {
throw new NotImplementedException();
}
+
+ @Override
+ public ExperimentApi experiment(String name) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public ListExperimentsRequest listExperiments() throws RestApiException {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/java/com/google/gerrit/extensions/api/config/package-info.java b/java/com/google/gerrit/extensions/api/config/package-info.java
new file mode 100644
index 0000000000..2ee371c5d2
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/config/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.api.config;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/api/groups/Groups.java b/java/com/google/gerrit/extensions/api/groups/Groups.java
index 1a46930a0d..8d53af0959 100644
--- a/java/com/google/gerrit/extensions/api/groups/Groups.java
+++ b/java/com/google/gerrit/extensions/api/groups/Groups.java
@@ -14,6 +14,7 @@
package com.google.gerrit.extensions.api.groups;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.client.ListGroupsOption;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -42,9 +43,11 @@ public interface Groups {
GroupApi id(String id) throws RestApiException;
/** Create a new group with the given name and default options. */
+ @CanIgnoreReturnValue
GroupApi create(String name) throws RestApiException;
/** Create a new group. */
+ @CanIgnoreReturnValue
GroupApi create(GroupInput input) throws RestApiException;
/** Returns new request for listing groups. */
diff --git a/java/com/google/gerrit/extensions/api/groups/package-info.java b/java/com/google/gerrit/extensions/api/groups/package-info.java
new file mode 100644
index 0000000000..3322f5cbc0
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/groups/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.api.groups;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/api/lfs/package-info.java b/java/com/google/gerrit/extensions/api/lfs/package-info.java
new file mode 100644
index 0000000000..2246f8da67
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/lfs/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.api.lfs;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/api/package-info.java b/java/com/google/gerrit/extensions/api/package-info.java
new file mode 100644
index 0000000000..b7ad7da4dd
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.api;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/api/plugins/Plugins.java b/java/com/google/gerrit/extensions/api/plugins/Plugins.java
index 6c2d6db4ea..fed8507e59 100644
--- a/java/com/google/gerrit/extensions/api/plugins/Plugins.java
+++ b/java/com/google/gerrit/extensions/api/plugins/Plugins.java
@@ -14,6 +14,7 @@
package com.google.gerrit.extensions.api.plugins;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.common.PluginInfo;
import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -29,9 +30,11 @@ public interface Plugins {
PluginApi name(String name) throws RestApiException;
@Deprecated
+ @CanIgnoreReturnValue
PluginApi install(String name, com.google.gerrit.extensions.common.InstallPluginInput input)
throws RestApiException;
+ @CanIgnoreReturnValue
PluginApi install(String name, InstallPluginInput input) throws RestApiException;
abstract class ListRequest {
diff --git a/java/com/google/gerrit/extensions/api/plugins/package-info.java b/java/com/google/gerrit/extensions/api/plugins/package-info.java
new file mode 100644
index 0000000000..c5324d6d6b
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/plugins/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.api.plugins;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/api/projects/BranchApi.java b/java/com/google/gerrit/extensions/api/projects/BranchApi.java
index 90f1f3f864..a41020540e 100644
--- a/java/com/google/gerrit/extensions/api/projects/BranchApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/BranchApi.java
@@ -14,6 +14,7 @@
package com.google.gerrit.extensions.api.projects;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.api.changes.ChangeApi.SuggestedReviewersRequest;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -21,6 +22,7 @@ import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.List;
public interface BranchApi {
+ @CanIgnoreReturnValue
BranchApi create(BranchInput in) throws RestApiException;
BranchInfo get() throws RestApiException;
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigValue.java b/java/com/google/gerrit/extensions/api/projects/ConfigValue.java
index 5d6d2b0e14..2f60628818 100644
--- a/java/com/google/gerrit/extensions/api/projects/ConfigValue.java
+++ b/java/com/google/gerrit/extensions/api/projects/ConfigValue.java
@@ -17,6 +17,13 @@ package com.google.gerrit.extensions.api.projects;
import java.util.List;
public class ConfigValue {
+
+ public ConfigValue() {}
+
+ public ConfigValue(String value) {
+ this.value = value;
+ }
+
public String value;
public List<String> values;
}
diff --git a/java/com/google/gerrit/extensions/api/projects/LabelApi.java b/java/com/google/gerrit/extensions/api/projects/LabelApi.java
index 975a57edb4..a5e23f252f 100644
--- a/java/com/google/gerrit/extensions/api/projects/LabelApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/LabelApi.java
@@ -14,6 +14,7 @@
package com.google.gerrit.extensions.api.projects;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.common.LabelDefinitionInfo;
import com.google.gerrit.extensions.common.LabelDefinitionInput;
@@ -21,10 +22,12 @@ import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
public interface LabelApi {
+ @CanIgnoreReturnValue
LabelApi create(LabelDefinitionInput input) throws RestApiException;
LabelDefinitionInfo get() throws RestApiException;
+ @CanIgnoreReturnValue
LabelDefinitionInfo update(LabelDefinitionInput input) throws RestApiException;
default void delete() throws RestApiException {
diff --git a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index 370068e247..0b1b6b0b91 100644
--- a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -14,6 +14,7 @@
package com.google.gerrit.extensions.api.projects;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
import com.google.gerrit.extensions.api.access.ProjectAccessInput;
import com.google.gerrit.extensions.api.config.AccessCheckInfo;
@@ -21,6 +22,7 @@ import com.google.gerrit.extensions.api.config.AccessCheckInput;
import com.google.gerrit.extensions.common.BatchLabelInput;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.extensions.common.ListTagSortOption;
import com.google.gerrit.extensions.common.ProjectInfo;
import com.google.gerrit.extensions.common.SubmitRequirementInfo;
import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -43,6 +45,7 @@ public interface ProjectApi {
ProjectAccessInfo access() throws RestApiException;
+ @CanIgnoreReturnValue
ProjectAccessInfo access(ProjectAccessInput p) throws RestApiException;
ChangeInfo accessChange(ProjectAccessInput p) throws RestApiException;
@@ -53,6 +56,7 @@ public interface ProjectApi {
ConfigInfo config() throws RestApiException;
+ @CanIgnoreReturnValue
ConfigInfo config(ConfigInput in) throws RestApiException;
Map<String, Set<String>> commitsIn(Collection<String> commits, Collection<String> refs)
@@ -69,9 +73,11 @@ public interface ProjectApi {
abstract class ListRefsRequest<T extends RefInfo> {
protected int limit;
protected int start;
+ protected boolean descendingOrder;
protected String substring;
protected String regex;
protected String nextPageToken;
+ protected ListTagSortOption sortBy = ListTagSortOption.REF;
public abstract List<T> get() throws RestApiException;
@@ -85,6 +91,16 @@ public interface ProjectApi {
return this;
}
+ public ListRefsRequest<T> withDescendingOrder(boolean descendingOrder) {
+ this.descendingOrder = descendingOrder;
+ return this;
+ }
+
+ public ListRefsRequest<T> withSortBy(ListTagSortOption sortBy) {
+ this.sortBy = sortBy;
+ return this;
+ }
+
public ListRefsRequest<T> withNextPageToken(String token) {
this.nextPageToken = token;
return this;
@@ -108,6 +124,14 @@ public interface ProjectApi {
return start;
}
+ public boolean getDescendingOrder() {
+ return descendingOrder;
+ }
+
+ public ListTagSortOption getSortBy() {
+ return sortBy;
+ }
+
public String getNextPageToken() {
return nextPageToken;
}
diff --git a/java/com/google/gerrit/extensions/api/projects/Projects.java b/java/com/google/gerrit/extensions/api/projects/Projects.java
index 34ca7d493f..7c8ecca5d2 100644
--- a/java/com/google/gerrit/extensions/api/projects/Projects.java
+++ b/java/com/google/gerrit/extensions/api/projects/Projects.java
@@ -14,6 +14,7 @@
package com.google.gerrit.extensions.api.projects;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.client.ProjectState;
import com.google.gerrit.extensions.common.ProjectInfo;
import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -46,6 +47,7 @@ public interface Projects {
* @return API for accessing the newly-created project.
* @throws RestApiException if an error occurred.
*/
+ @CanIgnoreReturnValue
ProjectApi create(String name) throws RestApiException;
/**
@@ -55,6 +57,7 @@ public interface Projects {
* @return API for accessing the newly-created project.
* @throws RestApiException if an error occurred.
*/
+ @CanIgnoreReturnValue
ProjectApi create(ProjectInput in) throws RestApiException;
ListRequest list();
diff --git a/java/com/google/gerrit/extensions/api/projects/SubmitRequirementApi.java b/java/com/google/gerrit/extensions/api/projects/SubmitRequirementApi.java
index a6e79db444..29765c07d2 100644
--- a/java/com/google/gerrit/extensions/api/projects/SubmitRequirementApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/SubmitRequirementApi.java
@@ -14,6 +14,7 @@
package com.google.gerrit.extensions.api.projects;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.common.SubmitRequirementInfo;
import com.google.gerrit.extensions.common.SubmitRequirementInput;
import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -21,12 +22,14 @@ import com.google.gerrit.extensions.restapi.RestApiException;
public interface SubmitRequirementApi {
/** Create a new submit requirement. */
+ @CanIgnoreReturnValue
SubmitRequirementApi create(SubmitRequirementInput input) throws RestApiException;
/** Get existing submit requirement. */
SubmitRequirementInfo get() throws RestApiException;
/** Update existing submit requirement. */
+ @CanIgnoreReturnValue
SubmitRequirementInfo update(SubmitRequirementInput input) throws RestApiException;
/** Delete existing submit requirement. */
diff --git a/java/com/google/gerrit/extensions/api/projects/TagApi.java b/java/com/google/gerrit/extensions/api/projects/TagApi.java
index 39efeacbbc..69c29df5d4 100644
--- a/java/com/google/gerrit/extensions/api/projects/TagApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/TagApi.java
@@ -14,10 +14,12 @@
package com.google.gerrit.extensions.api.projects;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
public interface TagApi {
+ @CanIgnoreReturnValue
TagApi create(TagInput input) throws RestApiException;
TagInfo get() throws RestApiException;
diff --git a/java/com/google/gerrit/extensions/api/projects/package-info.java b/java/com/google/gerrit/extensions/api/projects/package-info.java
new file mode 100644
index 0000000000..4fcfc28b1d
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/projects/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.api.projects;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/auth/oauth/package-info.java b/java/com/google/gerrit/extensions/auth/oauth/package-info.java
new file mode 100644
index 0000000000..591ea676cc
--- /dev/null
+++ b/java/com/google/gerrit/extensions/auth/oauth/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.auth.oauth;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/client/ChangeStatus.java b/java/com/google/gerrit/extensions/client/ChangeStatus.java
index 83d5bd2cc7..4111dc4c94 100644
--- a/java/com/google/gerrit/extensions/client/ChangeStatus.java
+++ b/java/com/google/gerrit/extensions/client/ChangeStatus.java
@@ -31,7 +31,7 @@ public enum ChangeStatus {
* <li>{@link #ABANDONED} - when the Abandon action is used.
* </ul>
*/
- NEW,
+ NEW(0),
/**
* Change is closed, and submitted to its destination branch.
@@ -39,7 +39,7 @@ public enum ChangeStatus {
* <p>Once a change has been merged, it cannot be further modified by adding a replacement patch
* set. Draft comments however may be published, supporting a post-submit review.
*/
- MERGED,
+ MERGED(1),
/**
* Change is closed, but was not submitted to its destination branch.
@@ -54,5 +54,15 @@ public enum ChangeStatus {
* <li>{@link #NEW} - when the Restore action is used.
* </ul>
*/
- ABANDONED
+ ABANDONED(2);
+
+ private final int value;
+
+ ChangeStatus(int v) {
+ this.value = v;
+ }
+
+ public int getValue() {
+ return value;
+ }
}
diff --git a/java/com/google/gerrit/extensions/client/Comment.java b/java/com/google/gerrit/extensions/client/Comment.java
index b8843d3a32..8a68236df1 100644
--- a/java/com/google/gerrit/extensions/client/Comment.java
+++ b/java/com/google/gerrit/extensions/client/Comment.java
@@ -14,9 +14,11 @@
package com.google.gerrit.extensions.client;
+import com.google.gerrit.extensions.common.FixSuggestionInfo;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.Comparator;
+import java.util.List;
import java.util.Objects;
public abstract class Comment {
@@ -49,6 +51,8 @@ public abstract class Comment {
*/
public String commitId;
+ public List<FixSuggestionInfo> fixSuggestions;
+
// TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
// Instant
@SuppressWarnings("JdkObsolete")
@@ -151,13 +155,15 @@ public abstract class Comment {
&& Objects.equals(inReplyTo, c.inReplyTo)
&& Objects.equals(updated, c.updated)
&& Objects.equals(message, c.message)
- && Objects.equals(commitId, c.commitId);
+ && Objects.equals(commitId, c.commitId)
+ && Objects.equals(fixSuggestions, c.fixSuggestions);
}
return false;
}
@Override
public int hashCode() {
- return Objects.hash(patchSet, id, path, side, parent, line, range, inReplyTo, updated, message);
+ return Objects.hash(
+ patchSet, id, path, side, parent, line, range, inReplyTo, updated, message, fixSuggestions);
}
}
diff --git a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
index 5c48aaf84d..1ed9793fe5 100644
--- a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -142,6 +142,7 @@ public class GeneralPreferencesInfo {
public List<MenuItem> my;
public List<String> changeTable;
public Boolean allowBrowserNotifications;
+ public Boolean allowSuggestCodeWhileCommenting;
/**
* The sidebar section that the user prefers to have open on the diff page, or "NONE" if all
* sidebars should be closed.
@@ -296,6 +297,7 @@ public class GeneralPreferencesInfo {
p.workInProgressByDefault = false;
p.allowBrowserNotifications = true;
p.diffPageSidebar = "NONE";
+ p.allowSuggestCodeWhileCommenting = true;
return p;
}
}
diff --git a/java/com/google/gerrit/extensions/client/package-info.java b/java/com/google/gerrit/extensions/client/package-info.java
new file mode 100644
index 0000000000..75bbfda39e
--- /dev/null
+++ b/java/com/google/gerrit/extensions/client/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.client;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 69160e96ab..4a3b423e92 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -116,6 +116,7 @@ public class ChangeInfo {
public Collection<ReviewerUpdateInfo> reviewerUpdates;
public Collection<ChangeMessageInfo> messages;
+ public Integer currentRevisionNumber;
public String currentRevision;
public Map<String, RevisionInfo> revisions;
public Boolean _moreChanges;
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
index 51c35dce71..308daf0949 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
@@ -75,12 +75,13 @@ public final class ChangeInfoDiffer {
@SuppressWarnings("unchecked") // reflection is used to construct instances of T
private static <T> T getAdded(T oldValue, T newValue) {
if (newValue instanceof Collection) {
- List<?> result = getAddedForCollection((Collection<?>) oldValue, (Collection<?>) newValue);
+ ImmutableList<?> result =
+ getAddedForCollection((Collection<?>) oldValue, (Collection<?>) newValue);
return (T) result;
}
if (newValue instanceof Map) {
- Map<?, ?> result = getAddedForMap((Map<?, ?>) oldValue, (Map<?, ?>) newValue);
+ ImmutableMap<?, ?> result = getAddedForMap((Map<?, ?>) oldValue, (Map<?, ?>) newValue);
return (T) result;
}
diff --git a/java/com/google/gerrit/extensions/common/CommentInfo.java b/java/com/google/gerrit/extensions/common/CommentInfo.java
index 35587a0d79..aee2552e7f 100644
--- a/java/com/google/gerrit/extensions/common/CommentInfo.java
+++ b/java/com/google/gerrit/extensions/common/CommentInfo.java
@@ -39,13 +39,14 @@ public class CommentInfo extends Comment {
CommentInfo ci = (CommentInfo) o;
return Objects.equals(author, ci.author)
&& Objects.equals(tag, ci.tag)
- && Objects.equals(unresolved, ci.unresolved);
+ && Objects.equals(unresolved, ci.unresolved)
+ && Objects.equals(fixSuggestions, ci.fixSuggestions);
}
return false;
}
@Override
public int hashCode() {
- return Objects.hash(super.hashCode(), author, tag, unresolved);
+ return Objects.hash(super.hashCode(), author, tag, unresolved, fixSuggestions);
}
}
diff --git a/java/com/google/gerrit/extensions/common/CommitMessageInfo.java b/java/com/google/gerrit/extensions/common/CommitMessageInfo.java
new file mode 100644
index 0000000000..fdd7cc3415
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/CommitMessageInfo.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.extensions.common;
+
+import java.util.Map;
+
+/** Representation of a commit message used in the API. */
+public class CommitMessageInfo {
+ /**
+ * The subject of the change.
+ *
+ * <p>First line of the commit message.
+ */
+ public String subject;
+
+ /** Full commit message of the change. */
+ public String fullMessage;
+
+ /**
+ * The footers from the commit message.
+ *
+ * <p>Key-value pairs from the last paragraph of the commit message.
+ */
+ public Map<String, String> footers;
+}
diff --git a/java/com/google/gerrit/extensions/common/CommitMessageInput.java b/java/com/google/gerrit/extensions/common/CommitMessageInput.java
index 1e23cb4d17..0b27c6b50d 100644
--- a/java/com/google/gerrit/extensions/common/CommitMessageInput.java
+++ b/java/com/google/gerrit/extensions/common/CommitMessageInput.java
@@ -27,4 +27,6 @@ public class CommitMessageInput {
@Nullable public NotifyHandling notify;
public Map<RecipientType, NotifyInfo> notifyDetails;
+
+ public String committerEmail;
}
diff --git a/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java b/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java
index 6f0b178bc8..2c2b8665f1 100644
--- a/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java
+++ b/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java
@@ -18,6 +18,7 @@ import java.util.Map;
public class DownloadSchemeInfo {
public String url;
+ public String description;
public Boolean isAuthRequired;
public Boolean isAuthSupported;
public Map<String, String> commands;
diff --git a/java/com/google/gerrit/extensions/common/ExperimentInfo.java b/java/com/google/gerrit/extensions/common/ExperimentInfo.java
new file mode 100644
index 0000000000..0cf5c9d39c
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/ExperimentInfo.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2034 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.extensions.common;
+
+public class ExperimentInfo {
+ /** Whether the experiment is enabled. */
+ public Boolean enabled;
+}
diff --git a/java/com/google/gerrit/extensions/common/FixReplacementInfo.java b/java/com/google/gerrit/extensions/common/FixReplacementInfo.java
index 9e5890ef16..6df4fb940a 100644
--- a/java/com/google/gerrit/extensions/common/FixReplacementInfo.java
+++ b/java/com/google/gerrit/extensions/common/FixReplacementInfo.java
@@ -15,9 +15,26 @@
package com.google.gerrit.extensions.common;
import com.google.gerrit.extensions.client.Comment;
+import java.util.Objects;
public class FixReplacementInfo {
public String path;
public Comment.Range range;
public String replacement;
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof FixReplacementInfo)) {
+ return false;
+ }
+ FixReplacementInfo fs = (FixReplacementInfo) o;
+ return Objects.equals(path, fs.path)
+ && Objects.equals(range, fs.range)
+ && Objects.equals(replacement, fs.replacement);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(path, range, replacement);
+ }
}
diff --git a/java/com/google/gerrit/extensions/common/FixSuggestionInfo.java b/java/com/google/gerrit/extensions/common/FixSuggestionInfo.java
index 7ba7fcc572..50df366c77 100644
--- a/java/com/google/gerrit/extensions/common/FixSuggestionInfo.java
+++ b/java/com/google/gerrit/extensions/common/FixSuggestionInfo.java
@@ -15,9 +15,26 @@
package com.google.gerrit.extensions.common;
import java.util.List;
+import java.util.Objects;
public class FixSuggestionInfo {
public String fixId;
public String description;
public List<FixReplacementInfo> replacements;
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof FixSuggestionInfo)) {
+ return false;
+ }
+ FixSuggestionInfo fs = (FixSuggestionInfo) o;
+ return Objects.equals(fixId, fs.fixId)
+ && Objects.equals(description, fs.description)
+ && Objects.equals(replacements, fs.replacements);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(fixId, description, replacements);
+ }
}
diff --git a/java/com/google/gerrit/extensions/common/ListTagSortOption.java b/java/com/google/gerrit/extensions/common/ListTagSortOption.java
new file mode 100644
index 0000000000..214092421b
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/ListTagSortOption.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.extensions.common;
+
+public enum ListTagSortOption {
+ REF,
+ CREATION_TIME,
+}
diff --git a/java/com/google/gerrit/extensions/common/RobotCommentInfo.java b/java/com/google/gerrit/extensions/common/RobotCommentInfo.java
index 8d8731f617..780cab7dea 100644
--- a/java/com/google/gerrit/extensions/common/RobotCommentInfo.java
+++ b/java/com/google/gerrit/extensions/common/RobotCommentInfo.java
@@ -14,13 +14,12 @@
package com.google.gerrit.extensions.common;
-import java.util.List;
import java.util.Map;
+@Deprecated
public class RobotCommentInfo extends CommentInfo {
public String robotId;
public String robotRunId;
public String url;
public Map<String, String> properties;
- public List<FixSuggestionInfo> fixSuggestions;
}
diff --git a/java/com/google/gerrit/extensions/common/package-info.java b/java/com/google/gerrit/extensions/common/package-info.java
new file mode 100644
index 0000000000..40f03b242d
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.common;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/common/testing/BUILD b/java/com/google/gerrit/extensions/common/testing/BUILD
index 33cbb99c8c..c126928576 100644
--- a/java/com/google/gerrit/extensions/common/testing/BUILD
+++ b/java/com/google/gerrit/extensions/common/testing/BUILD
@@ -13,6 +13,7 @@ java_library(
"//lib:jgit",
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
+ "//lib/errorprone:annotations",
"//lib/truth",
],
)
diff --git a/java/com/google/gerrit/extensions/common/testing/CommentInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/CommentInfoSubject.java
index c34e4398f1..049d6e4cfb 100644
--- a/java/com/google/gerrit/extensions/common/testing/CommentInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/CommentInfoSubject.java
@@ -17,6 +17,7 @@ package com.google.gerrit.extensions.common.testing;
import static com.google.common.truth.Truth.assertAbout;
import static com.google.gerrit.extensions.common.testing.AccountInfoSubject.accounts;
import static com.google.gerrit.extensions.common.testing.RangeSubject.ranges;
+import static com.google.gerrit.truth.ListSubject.elements;
import com.google.common.truth.BooleanSubject;
import com.google.common.truth.ComparableSubject;
@@ -26,6 +27,7 @@ import com.google.common.truth.StringSubject;
import com.google.common.truth.Subject;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.common.FixSuggestionInfo;
import com.google.gerrit.truth.ListSubject;
import java.sql.Timestamp;
import java.util.List;
@@ -52,6 +54,12 @@ public class CommentInfoSubject extends Subject {
this.commentInfo = commentInfo;
}
+ public ListSubject<FixSuggestionInfoSubject, FixSuggestionInfo> fixSuggestions() {
+ return check("fixSuggestions")
+ .about(elements())
+ .thatCustom(commentInfo.fixSuggestions, FixSuggestionInfoSubject.fixSuggestions());
+ }
+
public StringSubject uuid() {
return check("id").that(commentInfo().id);
}
@@ -116,4 +124,8 @@ public class CommentInfoSubject extends Subject {
isNotNull();
return commentInfo;
}
+
+ public FixSuggestionInfoSubject onlyFixSuggestion() {
+ return fixSuggestions().onlyElement();
+ }
}
diff --git a/java/com/google/gerrit/extensions/common/testing/package-info.java b/java/com/google/gerrit/extensions/common/testing/package-info.java
new file mode 100644
index 0000000000..869891acf5
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.common.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/conditions/BooleanCondition.java b/java/com/google/gerrit/extensions/conditions/BooleanCondition.java
index 9c354fb225..b1c1e93a44 100644
--- a/java/com/google/gerrit/extensions/conditions/BooleanCondition.java
+++ b/java/com/google/gerrit/extensions/conditions/BooleanCondition.java
@@ -14,8 +14,8 @@
package com.google.gerrit.extensions.conditions;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
-import java.util.Collections;
/** Delayed evaluation of a boolean condition. */
public abstract class BooleanCondition {
@@ -270,8 +270,8 @@ public abstract class BooleanCondition {
}
@Override
- public <T> Iterable<T> children(Class<T> type) {
- return Collections.emptyList();
+ public <T> ImmutableList<T> children(Class<T> type) {
+ return ImmutableList.of();
}
@Override
diff --git a/java/com/google/gerrit/extensions/conditions/package-info.java b/java/com/google/gerrit/extensions/conditions/package-info.java
new file mode 100644
index 0000000000..8a6b726899
--- /dev/null
+++ b/java/com/google/gerrit/extensions/conditions/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.conditions;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/config/DownloadScheme.java b/java/com/google/gerrit/extensions/config/DownloadScheme.java
index 15801d4473..5dad525399 100644
--- a/java/com/google/gerrit/extensions/config/DownloadScheme.java
+++ b/java/com/google/gerrit/extensions/config/DownloadScheme.java
@@ -37,4 +37,9 @@ public abstract class DownloadScheme {
/** Returns whether the download scheme is hidden in the UI */
public abstract boolean isHidden();
+
+ /** Returns an optional description of the scheme */
+ public String getDescription() {
+ return null;
+ }
}
diff --git a/java/com/google/gerrit/extensions/config/package-info.java b/java/com/google/gerrit/extensions/config/package-info.java
new file mode 100644
index 0000000000..2ddb32c727
--- /dev/null
+++ b/java/com/google/gerrit/extensions/config/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.config;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/events/GerritEvent.java b/java/com/google/gerrit/extensions/events/GerritEvent.java
index e43a9811fb..abab852b90 100644
--- a/java/com/google/gerrit/extensions/events/GerritEvent.java
+++ b/java/com/google/gerrit/extensions/events/GerritEvent.java
@@ -19,4 +19,8 @@ import com.google.gerrit.extensions.api.changes.NotifyHandling;
/** Base interface to be extended by Events. */
public interface GerritEvent {
NotifyHandling getNotify();
+
+ default String getInstanceId() {
+ return null;
+ }
}
diff --git a/java/com/google/gerrit/extensions/events/package-info.java b/java/com/google/gerrit/extensions/events/package-info.java
new file mode 100644
index 0000000000..e0a454c7fe
--- /dev/null
+++ b/java/com/google/gerrit/extensions/events/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.events;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/package-info.java b/java/com/google/gerrit/extensions/package-info.java
new file mode 100644
index 0000000000..f886030862
--- /dev/null
+++ b/java/com/google/gerrit/extensions/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/registration/DynamicItem.java b/java/com/google/gerrit/extensions/registration/DynamicItem.java
index 3f848cb63c..4464af7c25 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicItem.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicItem.java
@@ -14,6 +14,7 @@
package com.google.gerrit.extensions.registration;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.inject.Binder;
import com.google.inject.Key;
@@ -159,6 +160,7 @@ public class DynamicItem<T> {
* @param pluginName the name of the plugin providing the item.
* @return handle to remove the item at a later point in time.
*/
+ @CanIgnoreReturnValue
public RegistrationHandle set(T item, String pluginName) {
return set(Providers.of(item), pluginName);
}
@@ -170,6 +172,7 @@ public class DynamicItem<T> {
* @param pluginName name of the source providing the implementation.
* @return handle to remove the item at a later point in time.
*/
+ @CanIgnoreReturnValue
public RegistrationHandle set(Provider<T> impl, String pluginName) {
final Extension<T> item = new Extension<>(pluginName, impl);
Extension<T> old = null;
@@ -197,6 +200,7 @@ public class DynamicItem<T> {
* @param pluginName the name of the plugin providing the item.
* @return a handle that can remove this item later, or hot-swap the item.
*/
+ @CanIgnoreReturnValue
public ReloadableRegistrationHandle<T> set(Key<T> key, Provider<T> impl, String pluginName) {
final Extension<T> item = new Extension<>(pluginName, impl);
Extension<T> old = null;
diff --git a/java/com/google/gerrit/extensions/registration/DynamicSet.java b/java/com/google/gerrit/extensions/registration/DynamicSet.java
index 6dc8c6a307..9925a6616b 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -20,6 +20,7 @@ import static java.util.Comparator.naturalOrder;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.inject.Binder;
import com.google.inject.Key;
@@ -249,6 +250,7 @@ public class DynamicSet<T> implements Iterable<T> {
* @param item the item to add to the collection. Must not be null.
* @return handle to remove the item at a later point in time.
*/
+ @CanIgnoreReturnValue
public RegistrationHandle add(String pluginName, T item) {
return add(pluginName, Providers.of(item));
}
@@ -259,6 +261,7 @@ public class DynamicSet<T> implements Iterable<T> {
* @param item the item to add to the collection. Must not be null.
* @return handle to remove the item at a later point in time.
*/
+ @CanIgnoreReturnValue
public RegistrationHandle add(String pluginName, Provider<T> item) {
final AtomicReference<Extension<T>> ref =
new AtomicReference<>(new Extension<>(pluginName, item));
@@ -281,6 +284,7 @@ public class DynamicSet<T> implements Iterable<T> {
* @return a handle that can remove this item later, or hot-swap the item without it ever leaving
* the collection.
*/
+ @CanIgnoreReturnValue
public ReloadableRegistrationHandle<T> add(String pluginName, Key<T> key, Provider<T> item) {
AtomicReference<Extension<T>> ref = new AtomicReference<>(new Extension<>(pluginName, item));
items.add(ref);
diff --git a/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java b/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
index 67fc0683bd..994a8fb354 100644
--- a/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
+++ b/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
@@ -16,6 +16,7 @@ package com.google.gerrit.extensions.registration;
import static java.util.Objects.requireNonNull;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.annotations.Export;
import com.google.inject.Key;
@@ -51,6 +52,7 @@ public class PrivateInternals_DynamicMapImpl<T> extends DynamicMap<T> {
* @return a handle that can remove this item later, or hot-swap the item without it ever leaving
* the collection.
*/
+ @CanIgnoreReturnValue
public ReloadableRegistrationHandle<T> put(String pluginName, Key<T> key, Provider<T> item) {
requireNonNull(item);
String exportName = ((Export) key.getAnnotation()).value();
diff --git a/java/com/google/gerrit/extensions/registration/package-info.java b/java/com/google/gerrit/extensions/registration/package-info.java
new file mode 100644
index 0000000000..bcd5880fb8
--- /dev/null
+++ b/java/com/google/gerrit/extensions/registration/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.registration;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/restapi/BinaryResult.java b/java/com/google/gerrit/extensions/restapi/BinaryResult.java
index 2ee376ea90..905c0db813 100644
--- a/java/com/google/gerrit/extensions/restapi/BinaryResult.java
+++ b/java/com/google/gerrit/extensions/restapi/BinaryResult.java
@@ -16,6 +16,7 @@ package com.google.gerrit.extensions.restapi;
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
@@ -73,6 +74,7 @@ public abstract class BinaryResult implements Closeable {
}
/** Set the MIME type of the result, and return {@code this}. */
+ @CanIgnoreReturnValue
public BinaryResult setContentType(String contentType) {
this.contentType = contentType != null ? contentType : OCTET_STREAM;
return this;
@@ -84,6 +86,7 @@ public abstract class BinaryResult implements Closeable {
}
/** Set the character set used to encode text data and return {@code this}. */
+ @CanIgnoreReturnValue
public BinaryResult setCharacterEncoding(Charset encoding) {
characterEncoding = encoding;
return this;
@@ -95,6 +98,7 @@ public abstract class BinaryResult implements Closeable {
}
/** Set the attachment file name and return {@code this}. */
+ @CanIgnoreReturnValue
public BinaryResult setAttachmentName(String attachmentName) {
this.attachmentName = attachmentName;
return this;
@@ -106,6 +110,7 @@ public abstract class BinaryResult implements Closeable {
}
/** Set the content length of the result; -1 if not known. */
+ @CanIgnoreReturnValue
public BinaryResult setContentLength(long len) {
this.contentLength = len;
return this;
@@ -117,6 +122,7 @@ public abstract class BinaryResult implements Closeable {
}
/** Disable gzip compression for already compressed responses. */
+ @CanIgnoreReturnValue
public BinaryResult disableGzip() {
this.gzip = false;
return this;
@@ -128,6 +134,7 @@ public abstract class BinaryResult implements Closeable {
}
/** Wrap the binary data in base64 encoding. */
+ @CanIgnoreReturnValue
public BinaryResult base64() {
base64 = true;
return this;
diff --git a/java/com/google/gerrit/extensions/restapi/Response.java b/java/com/google/gerrit/extensions/restapi/Response.java
index 3ebae8da2a..85dd643b9d 100644
--- a/java/com/google/gerrit/extensions/restapi/Response.java
+++ b/java/com/google/gerrit/extensions/restapi/Response.java
@@ -15,6 +15,7 @@
package com.google.gerrit.extensions.restapi;
import com.google.common.collect.ImmutableMultimap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.concurrent.TimeUnit;
/** Special return value to mean specific HTTP status codes in a REST API. */
@@ -90,6 +91,7 @@ public abstract class Response<T> {
public abstract CacheControl caching();
+ @CanIgnoreReturnValue
public abstract Response<T> caching(CacheControl c);
@Override
diff --git a/java/com/google/gerrit/extensions/restapi/RestApiModule.java b/java/com/google/gerrit/extensions/restapi/RestApiModule.java
index 85bd5a1ed2..6d0191ea36 100644
--- a/java/com/google/gerrit/extensions/restapi/RestApiModule.java
+++ b/java/com/google/gerrit/extensions/restapi/RestApiModule.java
@@ -14,6 +14,7 @@
package com.google.gerrit.extensions.restapi;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.annotations.Export;
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.config.FactoryModule;
@@ -128,6 +129,7 @@ public abstract class RestApiModule extends FactoryModule {
this.binder = binder;
}
+ @CanIgnoreReturnValue
public <T extends RestReadView<P>> ScopedBindingBuilder to(Class<T> impl) {
return binder.to(impl);
}
@@ -136,11 +138,13 @@ public abstract class RestApiModule extends FactoryModule {
binder.toInstance(impl);
}
+ @CanIgnoreReturnValue
public <T extends RestReadView<P>> ScopedBindingBuilder toProvider(
Class<? extends Provider<? extends T>> providerType) {
return binder.toProvider(providerType);
}
+ @CanIgnoreReturnValue
public <T extends RestReadView<P>> ScopedBindingBuilder toProvider(
Provider<? extends T> provider) {
return binder.toProvider(provider);
@@ -154,6 +158,7 @@ public abstract class RestApiModule extends FactoryModule {
this.binder = binder;
}
+ @CanIgnoreReturnValue
public <T extends RestModifyView<P, ?>> ScopedBindingBuilder to(Class<T> impl) {
return binder.to(impl);
}
@@ -162,11 +167,13 @@ public abstract class RestApiModule extends FactoryModule {
binder.toInstance(impl);
}
+ @CanIgnoreReturnValue
public <T extends RestModifyView<P, ?>> ScopedBindingBuilder toProvider(
Class<? extends Provider<? extends T>> providerType) {
return binder.toProvider(providerType);
}
+ @CanIgnoreReturnValue
public <T extends RestModifyView<P, ?>> ScopedBindingBuilder toProvider(
Provider<? extends T> provider) {
return binder.toProvider(provider);
@@ -180,6 +187,7 @@ public abstract class RestApiModule extends FactoryModule {
this.binder = binder;
}
+ @CanIgnoreReturnValue
public <P extends RestResource, T extends RestCollectionModifyView<P, C, ?>>
ScopedBindingBuilder to(Class<T> impl) {
return binder.to(impl);
@@ -190,11 +198,13 @@ public abstract class RestApiModule extends FactoryModule {
binder.toInstance(impl);
}
+ @CanIgnoreReturnValue
public <P extends RestResource, T extends RestCollectionModifyView<P, C, ?>>
ScopedBindingBuilder toProvider(Class<? extends Provider<? extends T>> providerType) {
return binder.toProvider(providerType);
}
+ @CanIgnoreReturnValue
public <P extends RestResource, T extends RestCollectionModifyView<P, C, ?>>
ScopedBindingBuilder toProvider(Provider<? extends T> provider) {
return binder.toProvider(provider);
@@ -208,6 +218,7 @@ public abstract class RestApiModule extends FactoryModule {
this.binder = binder;
}
+ @CanIgnoreReturnValue
public <P extends RestResource, T extends RestCollectionCreateView<P, C, ?>>
ScopedBindingBuilder to(Class<T> impl) {
return binder.to(impl);
@@ -218,11 +229,13 @@ public abstract class RestApiModule extends FactoryModule {
binder.toInstance(impl);
}
+ @CanIgnoreReturnValue
public <P extends RestResource, T extends RestCollectionCreateView<P, C, ?>>
ScopedBindingBuilder toProvider(Class<? extends Provider<? extends T>> providerType) {
return binder.toProvider(providerType);
}
+ @CanIgnoreReturnValue
public <P extends RestResource, T extends RestCollectionCreateView<P, C, ?>>
ScopedBindingBuilder toProvider(Provider<? extends T> provider) {
return binder.toProvider(provider);
@@ -236,6 +249,7 @@ public abstract class RestApiModule extends FactoryModule {
this.binder = binder;
}
+ @CanIgnoreReturnValue
public <P extends RestResource, T extends RestCollectionDeleteMissingView<P, C, ?>>
ScopedBindingBuilder to(Class<T> impl) {
return binder.to(impl);
@@ -246,11 +260,13 @@ public abstract class RestApiModule extends FactoryModule {
binder.toInstance(impl);
}
+ @CanIgnoreReturnValue
public <P extends RestResource, T extends RestCollectionDeleteMissingView<P, C, ?>>
ScopedBindingBuilder toProvider(Class<? extends Provider<? extends T>> providerType) {
return binder.toProvider(providerType);
}
+ @CanIgnoreReturnValue
public <P extends RestResource, T extends RestCollectionDeleteMissingView<P, C, ?>>
ScopedBindingBuilder toProvider(Provider<? extends T> provider) {
return binder.toProvider(provider);
@@ -264,6 +280,7 @@ public abstract class RestApiModule extends FactoryModule {
this.binder = binder;
}
+ @CanIgnoreReturnValue
public <C extends RestResource, T extends ChildCollection<P, C>> ScopedBindingBuilder to(
Class<T> impl) {
return binder.to(impl);
@@ -273,11 +290,13 @@ public abstract class RestApiModule extends FactoryModule {
binder.toInstance(impl);
}
+ @CanIgnoreReturnValue
public <C extends RestResource, T extends ChildCollection<P, C>>
ScopedBindingBuilder toProvider(Class<? extends Provider<? extends T>> providerType) {
return binder.toProvider(providerType);
}
+ @CanIgnoreReturnValue
public <C extends RestResource, T extends ChildCollection<P, C>>
ScopedBindingBuilder toProvider(Provider<? extends T> provider) {
return binder.toProvider(provider);
diff --git a/java/com/google/gerrit/extensions/restapi/package-info.java b/java/com/google/gerrit/extensions/restapi/package-info.java
new file mode 100644
index 0000000000..f3762e50d9
--- /dev/null
+++ b/java/com/google/gerrit/extensions/restapi/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.restapi;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/restapi/testing/BUILD b/java/com/google/gerrit/extensions/restapi/testing/BUILD
index da11ce8721..a86edb8d37 100644
--- a/java/com/google/gerrit/extensions/restapi/testing/BUILD
+++ b/java/com/google/gerrit/extensions/restapi/testing/BUILD
@@ -9,6 +9,7 @@ java_library(
"//java/com/google/gerrit/entities",
"//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/truth",
+ "//lib/errorprone:annotations",
"//lib/truth",
],
)
diff --git a/java/com/google/gerrit/extensions/restapi/testing/package-info.java b/java/com/google/gerrit/extensions/restapi/testing/package-info.java
new file mode 100644
index 0000000000..74e31e7148
--- /dev/null
+++ b/java/com/google/gerrit/extensions/restapi/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.restapi.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/systemstatus/package-info.java b/java/com/google/gerrit/extensions/systemstatus/package-info.java
new file mode 100644
index 0000000000..ed9a8cc54b
--- /dev/null
+++ b/java/com/google/gerrit/extensions/systemstatus/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.systemstatus;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/validators/package-info.java b/java/com/google/gerrit/extensions/validators/package-info.java
new file mode 100644
index 0000000000..076519a6fa
--- /dev/null
+++ b/java/com/google/gerrit/extensions/validators/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.validators;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/extensions/webui/UiAction.java b/java/com/google/gerrit/extensions/webui/UiAction.java
index 9da0642352..6c1d01d8af 100644
--- a/java/com/google/gerrit/extensions/webui/UiAction.java
+++ b/java/com/google/gerrit/extensions/webui/UiAction.java
@@ -15,6 +15,7 @@
package com.google.gerrit.extensions.webui;
import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.conditions.BooleanCondition;
import com.google.gerrit.extensions.restapi.RestResource;
@@ -68,6 +69,7 @@ public interface UiAction<R extends RestResource> extends RestView<R> {
}
/** Set the label to appear on the button to activate this action. */
+ @CanIgnoreReturnValue
public Description setLabel(String label) {
this.label = label;
return this;
@@ -78,6 +80,7 @@ public interface UiAction<R extends RestResource> extends RestView<R> {
}
/** Set the tool-tip text to appear when the mouse hovers on the button. */
+ @CanIgnoreReturnValue
public Description setTitle(String title) {
this.title = title;
return this;
@@ -95,6 +98,7 @@ public interface UiAction<R extends RestResource> extends RestView<R> {
* Set if the action's button is visible on screen for the current client. If not visible the
* action description may not be sent to the client.
*/
+ @CanIgnoreReturnValue
public Description setVisible(boolean visible) {
return setVisible(BooleanCondition.valueOf(visible));
}
@@ -103,6 +107,7 @@ public interface UiAction<R extends RestResource> extends RestView<R> {
* Set if the action's button is visible on screen for the current client. If not visible the
* action description may not be sent to the client.
*/
+ @CanIgnoreReturnValue
public Description setVisible(BooleanCondition visible) {
this.visible = visible;
return this;
@@ -117,11 +122,13 @@ public interface UiAction<R extends RestResource> extends RestView<R> {
}
/** Set if the button should be invokable (true), or greyed out (false). */
+ @CanIgnoreReturnValue
public Description setEnabled(boolean enabled) {
return setEnabled(BooleanCondition.valueOf(enabled));
}
/** Set if the button should be invokable (true), or greyed out (false). */
+ @CanIgnoreReturnValue
public Description setEnabled(BooleanCondition enabled) {
this.enabled = enabled;
return this;
@@ -135,6 +142,7 @@ public interface UiAction<R extends RestResource> extends RestView<R> {
return ImmutableList.copyOf(enabledOptions);
}
+ @CanIgnoreReturnValue
public Description setOption(String optionName, boolean enabled) {
if (enabled) {
enabledOptions.add(optionName);
diff --git a/java/com/google/gerrit/extensions/webui/package-info.java b/java/com/google/gerrit/extensions/webui/package-info.java
new file mode 100644
index 0000000000..bc520f8d96
--- /dev/null
+++ b/java/com/google/gerrit/extensions/webui/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.extensions.webui;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/git/BUILD b/java/com/google/gerrit/git/BUILD
index 98dacfa800..3cd2be1f72 100644
--- a/java/com/google/gerrit/git/BUILD
+++ b/java/com/google/gerrit/git/BUILD
@@ -11,5 +11,7 @@ java_library(
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
"//lib/errorprone:annotations",
+ "//lib/flogger:api",
+ "//lib/log:log4j",
],
)
diff --git a/java/com/google/gerrit/git/RefUpdateUtil.java b/java/com/google/gerrit/git/RefUpdateUtil.java
index c2db073737..d0f738fffc 100644
--- a/java/com/google/gerrit/git/RefUpdateUtil.java
+++ b/java/com/google/gerrit/git/RefUpdateUtil.java
@@ -14,12 +14,18 @@
package com.google.gerrit.git;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.flogger.FluentLogger;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
+import java.util.Optional;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -27,6 +33,8 @@ import org.eclipse.jgit.transport.ReceiveCommand;
/** Static utilities for working with JGit's ref update APIs. */
public class RefUpdateUtil {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
/**
* Execute a batch ref update, throwing a checked exception if not all updates succeeded.
*
@@ -54,10 +62,36 @@ public class RefUpdateUtil {
* @throws IOException if any result was not {@code OK}.
*/
public static void executeChecked(BatchRefUpdate bru, RevWalk rw) throws IOException {
+ logger.atFine().log(
+ "Executing ref updates: %s\n",
+ Joiner.on("\n")
+ .join(
+ bru.getCommands().stream()
+ .map(
+ cmd ->
+ String.format(
+ "%s (new tree ID: %s)",
+ cmd, getNewTreeId(rw, cmd).map(ObjectId::name).orElse("n/a")))
+ .collect(toImmutableList())));
bru.execute(rw, NullProgressMonitor.INSTANCE);
checkResults(bru);
}
+ private static Optional<ObjectId> getNewTreeId(RevWalk revWalk, ReceiveCommand cmd) {
+ if (ReceiveCommand.Type.DELETE.equals(cmd.getType())) {
+ // Ref deletions do not have a new tree.
+ return Optional.empty();
+ }
+
+ try {
+ return Optional.of(revWalk.parseCommit(cmd.getNewId()).getTree());
+ } catch (IOException e) {
+ logger.atWarning().withCause(e).log(
+ "Failed parsing new commit %s for ref update: %s", cmd.getNewId().name(), cmd);
+ return Optional.empty();
+ }
+ }
+
/**
* Check results of all commands in the update batch, reducing to a single exception if there was
* a failure.
diff --git a/java/com/google/gerrit/git/package-info.java b/java/com/google/gerrit/git/package-info.java
new file mode 100644
index 0000000000..8c4d4b12f8
--- /dev/null
+++ b/java/com/google/gerrit/git/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.git;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/git/testing/BUILD b/java/com/google/gerrit/git/testing/BUILD
index fda9aff9f0..63bcfbac64 100644
--- a/java/com/google/gerrit/git/testing/BUILD
+++ b/java/com/google/gerrit/git/testing/BUILD
@@ -10,6 +10,7 @@ java_library(
"//java/com/google/gerrit/common:annotations",
"//lib:guava",
"//lib:jgit",
+ "//lib/errorprone:annotations",
"//lib/truth",
"//lib/truth:truth-java8-extension",
],
diff --git a/java/com/google/gerrit/git/testing/package-info.java b/java/com/google/gerrit/git/testing/package-info.java
new file mode 100644
index 0000000000..7ed8c4d6f4
--- /dev/null
+++ b/java/com/google/gerrit/git/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.git.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/gpg/BUILD b/java/com/google/gerrit/gpg/BUILD
index 39588214d1..fb24f13410 100644
--- a/java/com/google/gerrit/gpg/BUILD
+++ b/java/com/google/gerrit/gpg/BUILD
@@ -19,6 +19,7 @@ java_library(
"//lib/auto:auto-factory",
"//lib/bouncycastle:bcpg-neverlink",
"//lib/bouncycastle:bcprov-neverlink",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
"//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/gpg/Fingerprint.java b/java/com/google/gerrit/gpg/Fingerprint.java
index c12ff8b0aa..a0212ca7f4 100644
--- a/java/com/google/gerrit/gpg/Fingerprint.java
+++ b/java/com/google/gerrit/gpg/Fingerprint.java
@@ -16,6 +16,7 @@ package com.google.gerrit.gpg;
import static com.google.common.base.Preconditions.checkArgument;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
@@ -53,6 +54,7 @@ public class Fingerprint {
return Collections.unmodifiableMap(result);
}
+ @CanIgnoreReturnValue
private static byte[] checkLength(byte[] fp) {
checkArgument(fp.length == 20, "fingerprint must be 20 bytes, got %s", fp.length);
return fp;
diff --git a/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java b/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
index fff4045dd9..3940cc993f 100644
--- a/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
+++ b/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
@@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.BaseEncoding;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountState;
@@ -129,6 +130,7 @@ public class GerritPublicKeyChecker extends PublicKeyChecker {
* user. (Other keys checked in the course of verifying the web of trust are checked against the
* set of identities in the database belonging to the same user as the key.)
*/
+ @CanIgnoreReturnValue
public GerritPublicKeyChecker setExpectedUser(IdentifiedUser expectedUser) {
this.expectedUser = expectedUser;
return this;
diff --git a/java/com/google/gerrit/gpg/PublicKeyChecker.java b/java/com/google/gerrit/gpg/PublicKeyChecker.java
index 946fee351f..2f8f7e9b6c 100644
--- a/java/com/google/gerrit/gpg/PublicKeyChecker.java
+++ b/java/com/google/gerrit/gpg/PublicKeyChecker.java
@@ -30,6 +30,7 @@ import static org.bouncycastle.openpgp.PGPSignature.DIRECT_KEY;
import static org.bouncycastle.openpgp.PGPSignature.KEY_REVOCATION;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
import java.io.IOException;
@@ -77,6 +78,7 @@ public class PublicKeyChecker {
* construct a map, see {@link Fingerprint#byId(Iterable)}.
* @return a reference to this object.
*/
+ @CanIgnoreReturnValue
public PublicKeyChecker enableTrust(int maxTrustDepth, Map<Long, Fingerprint> trusted) {
if (maxTrustDepth <= 0) {
throw new IllegalArgumentException("maxTrustDepth must be positive, got: " + maxTrustDepth);
@@ -90,12 +92,14 @@ public class PublicKeyChecker {
}
/** Disable web-of-trust checks. */
+ @CanIgnoreReturnValue
public PublicKeyChecker disableTrust() {
trusted = null;
return this;
}
/** Set the public key store for reading keys referenced in signatures. */
+ @CanIgnoreReturnValue
public PublicKeyChecker setStore(PublicKeyStore store) {
if (store == null) {
throw new IllegalArgumentException("PublicKeyStore is required");
@@ -112,6 +116,7 @@ public class PublicKeyChecker {
* @param effectiveTime effective time.
* @return a reference to this object.
*/
+ @CanIgnoreReturnValue
public PublicKeyChecker setEffectiveTime(Instant effectiveTime) {
this.effectiveTime = effectiveTime;
return this;
diff --git a/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java b/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
index 2a05f35957..713db488ab 100644
--- a/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
+++ b/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
@@ -55,7 +55,8 @@ public class GpgKeyApiImpl implements GpgKeyApi {
@Override
public void delete() throws RestApiException {
try {
- delete.apply(rsrc, new Input());
+ @SuppressWarnings("unused")
+ var unused = delete.apply(rsrc, new Input());
} catch (RestApiException e) {
throw e;
} catch (PGPException | IOException | ConfigInvalidException e) {
diff --git a/java/com/google/gerrit/gpg/api/package-info.java b/java/com/google/gerrit/gpg/api/package-info.java
new file mode 100644
index 0000000000..524dc1caf7
--- /dev/null
+++ b/java/com/google/gerrit/gpg/api/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.gpg.api;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/gpg/package-info.java b/java/com/google/gerrit/gpg/package-info.java
new file mode 100644
index 0000000000..6e56395274
--- /dev/null
+++ b/java/com/google/gerrit/gpg/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.gpg;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index 886e4dd24c..e514e92ba7 100644
--- a/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -25,8 +25,10 @@ import static java.util.stream.Collectors.toList;
import com.google.common.base.Joiner;
import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
@@ -131,12 +133,12 @@ public class PostGpgKeys implements RestModifyView<AccountResource, GpgKeysInput
throws RestApiException, PGPException, IOException, ConfigInvalidException {
GpgKeys.checkVisible(self, rsrc);
- Collection<ExternalId> existingExtIds =
+ ImmutableSet<ExternalId> existingExtIds =
externalIds.byAccount(rsrc.getUser().getAccountId(), SCHEME_GPGKEY);
try (PublicKeyStore store = storeProvider.get()) {
- Map<ExternalId, Fingerprint> toRemove = readKeysToRemove(input, existingExtIds);
- Collection<Fingerprint> fingerprintsToRemove = toRemove.values();
- List<PGPPublicKeyRing> newKeys = readKeysToAdd(input, fingerprintsToRemove);
+ ImmutableMap<ExternalId, Fingerprint> toRemove = readKeysToRemove(input, existingExtIds);
+ ImmutableCollection<Fingerprint> fingerprintsToRemove = toRemove.values();
+ ImmutableList<PGPPublicKeyRing> newKeys = readKeysToAdd(input, fingerprintsToRemove);
List<ExternalId> newExtIds = new ArrayList<>(existingExtIds.size());
for (PGPPublicKeyRing keyRing : newKeys) {
diff --git a/java/com/google/gerrit/gpg/server/package-info.java b/java/com/google/gerrit/gpg/server/package-info.java
new file mode 100644
index 0000000000..5379c44c88
--- /dev/null
+++ b/java/com/google/gerrit/gpg/server/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.gpg.server;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/gpg/testing/BUILD b/java/com/google/gerrit/gpg/testing/BUILD
index dc390719c0..d55360b99e 100644
--- a/java/com/google/gerrit/gpg/testing/BUILD
+++ b/java/com/google/gerrit/gpg/testing/BUILD
@@ -10,5 +10,6 @@ java_library(
"//lib:guava",
"//lib:jgit",
"//lib/bouncycastle:bcpg-neverlink",
+ "//lib/errorprone:annotations",
],
)
diff --git a/java/com/google/gerrit/gpg/testing/package-info.java b/java/com/google/gerrit/gpg/testing/package-info.java
new file mode 100644
index 0000000000..d9606c3e8a
--- /dev/null
+++ b/java/com/google/gerrit/gpg/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.gpg.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/httpd/AllRequestFilter.java b/java/com/google/gerrit/httpd/AllRequestFilter.java
index 1c3cafea04..0422655f0a 100644
--- a/java/com/google/gerrit/httpd/AllRequestFilter.java
+++ b/java/com/google/gerrit/httpd/AllRequestFilter.java
@@ -70,7 +70,7 @@ public abstract class AllRequestFilter implements Filter {
* Initializes a filter if needed
*
* @param filter The filter that should get initialized
- * @return {@code true} iff filter is now initialized
+ * @return {@code true} if filter is now initialized
* @throws ServletException if filter itself fails to init
*/
private synchronized boolean initFilterIfNeeded(AllRequestFilter filter)
@@ -150,7 +150,8 @@ public abstract class AllRequestFilter implements Filter {
filterConfig = config;
for (AllRequestFilter f : filters) {
- initFilterIfNeeded(f);
+ @SuppressWarnings("unused")
+ var unused = initFilterIfNeeded(f);
}
}
diff --git a/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index e513a721ca..1537655af0 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -19,7 +19,6 @@ import static org.eclipse.jgit.http.server.GitSmartHttpTools.sendError;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
@@ -259,7 +258,8 @@ public class GitOverHttpServlet extends GitServlet {
return commandName.toString();
}
- private static ListMultimap<String, String> extractParameters(HttpServletRequest request) {
+ private static ImmutableListMultimap<String, String> extractParameters(
+ HttpServletRequest request) {
if (request.getQueryString() == null) {
return ImmutableListMultimap.of();
}
diff --git a/java/com/google/gerrit/httpd/RequestContextFilter.java b/java/com/google/gerrit/httpd/RequestContextFilter.java
index effbac0ee0..9428c5e383 100644
--- a/java/com/google/gerrit/httpd/RequestContextFilter.java
+++ b/java/com/google/gerrit/httpd/RequestContextFilter.java
@@ -63,7 +63,8 @@ public class RequestContextFilter implements Filter {
try {
chain.doFilter(request, response);
} finally {
- local.setContext(old);
+ @SuppressWarnings("unused")
+ var unused = local.setContext(old);
}
}
}
diff --git a/java/com/google/gerrit/httpd/UrlModule.java b/java/com/google/gerrit/httpd/UrlModule.java
index aad6b57ad8..7a100c7d26 100644
--- a/java/com/google/gerrit/httpd/UrlModule.java
+++ b/java/com/google/gerrit/httpd/UrlModule.java
@@ -43,7 +43,7 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jgit.lib.Constants;
class UrlModule extends ServletModule {
- private AuthConfig authConfig;
+ private final AuthConfig authConfig;
UrlModule(AuthConfig authConfig) {
this.authConfig = authConfig;
@@ -86,7 +86,7 @@ class UrlModule extends ServletModule {
serveRegex("^/(?:a/)?tools/(.*)$").with(ToolServlet.class);
// Serve auth check. Mainly used by PolyGerrit for checking if a user is still logged in.
- serveRegex("^/(?:a/)?auth-check$").with(AuthorizationCheckServlet.class);
+ serveRegex("^/(?:a/)?auth-check(\\.svg)?$").with(AuthorizationCheckServlet.class);
// Bind servlets for REST root collections.
// The '/plugins/' root collection is already handled by HttpPluginServlet
diff --git a/java/com/google/gerrit/httpd/auth/become/package-info.java b/java/com/google/gerrit/httpd/auth/become/package-info.java
new file mode 100644
index 0000000000..6887673aa5
--- /dev/null
+++ b/java/com/google/gerrit/httpd/auth/become/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.httpd.auth.become;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java b/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
index 59a7379601..82cef6b557 100644
--- a/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
+++ b/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
@@ -63,7 +63,7 @@ class HttpsClientSslCertAuthFilter implements Filter {
throw new ServletException(
"Couldn't get the attribute javax.servlet.request.X509Certificate from the request");
}
- String name = certs[0].getSubjectDN().getName();
+ String name = certs[0].getSubjectX500Principal().getName();
Matcher m = REGEX_USERID.matcher(name);
String userName;
if (m.find()) {
diff --git a/java/com/google/gerrit/httpd/auth/container/package-info.java b/java/com/google/gerrit/httpd/auth/container/package-info.java
new file mode 100644
index 0000000000..ca1c89f7c7
--- /dev/null
+++ b/java/com/google/gerrit/httpd/auth/container/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.httpd.auth.container;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/httpd/auth/ldap/package-info.java b/java/com/google/gerrit/httpd/auth/ldap/package-info.java
new file mode 100644
index 0000000000..2a81cc9b3e
--- /dev/null
+++ b/java/com/google/gerrit/httpd/auth/ldap/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.httpd.auth.ldap;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/httpd/auth/oauth/BUILD b/java/com/google/gerrit/httpd/auth/oauth/BUILD
index 2bc65de413..3ced4ab8b1 100644
--- a/java/com/google/gerrit/httpd/auth/oauth/BUILD
+++ b/java/com/google/gerrit/httpd/auth/oauth/BUILD
@@ -17,6 +17,7 @@ java_library(
"//lib:guava",
"//lib:jgit",
"//lib:servlet-api",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
"//lib/guice:guice-servlet",
diff --git a/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java b/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
index 935762fe2a..453dc0b30b 100644
--- a/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
+++ b/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
@@ -102,7 +102,9 @@ class OAuthWebFilter implements Filter {
service = findService(provider);
}
oauthSession.setServiceProvider(service);
- oauthSession.login(httpRequest, httpResponse, service);
+
+ @SuppressWarnings("unused")
+ var unused = oauthSession.login(httpRequest, httpResponse, service);
} else {
chain.doFilter(httpRequest, response);
}
diff --git a/java/com/google/gerrit/httpd/auth/oauth/package-info.java b/java/com/google/gerrit/httpd/auth/oauth/package-info.java
new file mode 100644
index 0000000000..4f8c601d08
--- /dev/null
+++ b/java/com/google/gerrit/httpd/auth/oauth/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.httpd.auth.oauth;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/httpd/auth/openid/BUILD b/java/com/google/gerrit/httpd/auth/openid/BUILD
index 29841aafdf..7afb8acfd7 100644
--- a/java/com/google/gerrit/httpd/auth/openid/BUILD
+++ b/java/com/google/gerrit/httpd/auth/openid/BUILD
@@ -17,6 +17,7 @@ java_library(
"//java/com/google/gerrit/server",
"//lib:guava",
"//lib:servlet-api",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
"//lib/guice:guice-servlet",
diff --git a/java/com/google/gerrit/httpd/auth/openid/LoginForm.java b/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
index e7057ad514..4ed9078b2a 100644
--- a/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
+++ b/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
@@ -174,7 +174,9 @@ class LoginForm extends HttpServlet {
if ((isGerritLogin(req) || oauthSession.isOAuthFinal(req))) {
oauthSession.setServiceProvider(oauthProvider);
oauthSession.setLinkMode(link);
- oauthSession.login(req, res, oauthProvider);
+
+ @SuppressWarnings("unused")
+ var unused = oauthSession.login(req, res, oauthProvider);
}
}
}
diff --git a/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java b/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java
index 3d9c8190c0..043b7a183b 100644
--- a/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java
+++ b/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java
@@ -72,7 +72,9 @@ class OAuthWebFilterOverOpenID implements Filter {
throw new IllegalStateException("service is unknown");
}
oauthSession.setServiceProvider(service);
- oauthSession.login(httpRequest, httpResponse, service);
+
+ @SuppressWarnings("unused")
+ var unused = oauthSession.login(httpRequest, httpResponse, service);
} else {
chain.doFilter(httpRequest, response);
}
diff --git a/java/com/google/gerrit/httpd/auth/openid/package-info.java b/java/com/google/gerrit/httpd/auth/openid/package-info.java
new file mode 100644
index 0000000000..5e597da60f
--- /dev/null
+++ b/java/com/google/gerrit/httpd/auth/openid/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.httpd.auth.openid;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/httpd/auth/restapi/BUILD b/java/com/google/gerrit/httpd/auth/restapi/BUILD
index 9ab51c571e..a85fd5e30d 100644
--- a/java/com/google/gerrit/httpd/auth/restapi/BUILD
+++ b/java/com/google/gerrit/httpd/auth/restapi/BUILD
@@ -9,6 +9,7 @@ java_library(
"//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/server",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
],
diff --git a/java/com/google/gerrit/httpd/auth/restapi/package-info.java b/java/com/google/gerrit/httpd/auth/restapi/package-info.java
new file mode 100644
index 0000000000..a56dbdeae3
--- /dev/null
+++ b/java/com/google/gerrit/httpd/auth/restapi/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.httpd.auth.restapi;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
index d6718ca9d3..d3de8e07a6 100644
--- a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
+++ b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
@@ -35,6 +35,8 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.entities.Project;
@@ -75,7 +77,6 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -771,8 +772,8 @@ class GitwebServlet extends HttpServlet {
}
@SuppressWarnings("JdkObsolete")
- private static Iterable<String> getHeaderNames(HttpServletRequest req) {
- return Collections.list(req.getHeaderNames());
+ private static ImmutableList<String> getHeaderNames(HttpServletRequest req) {
+ return ImmutableList.copyOf(Iterators.forEnumeration(req.getHeaderNames()));
}
/** private utility class that manages the Environment passed to exec. */
diff --git a/java/com/google/gerrit/httpd/gitweb/package-info.java b/java/com/google/gerrit/httpd/gitweb/package-info.java
new file mode 100644
index 0000000000..614388efb0
--- /dev/null
+++ b/java/com/google/gerrit/httpd/gitweb/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.httpd.gitweb;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/httpd/init/BUILD b/java/com/google/gerrit/httpd/init/BUILD
index 990b5d7c62..e6fc53cb0e 100644
--- a/java/com/google/gerrit/httpd/init/BUILD
+++ b/java/com/google/gerrit/httpd/init/BUILD
@@ -31,6 +31,7 @@ java_library(
"//lib:jgit",
"//lib:jgit-ssh-apache",
"//lib:servlet-api",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
"//lib/guice:guice-servlet",
diff --git a/java/com/google/gerrit/httpd/init/SiteInitializer.java b/java/com/google/gerrit/httpd/init/SiteInitializer.java
index 04a0ddd356..f9f93e1530 100644
--- a/java/com/google/gerrit/httpd/init/SiteInitializer.java
+++ b/java/com/google/gerrit/httpd/init/SiteInitializer.java
@@ -19,7 +19,6 @@ import com.google.common.flogger.FluentLogger;
import com.google.gerrit.pgm.init.BaseInit;
import com.google.gerrit.pgm.init.PluginsDistribution;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.List;
public final class SiteInitializer {
@@ -45,24 +44,29 @@ public final class SiteInitializer {
public void init() {
try {
if (sitePath != null) {
- Path site = Paths.get(sitePath);
+ Path site = Path.of(sitePath);
logger.atInfo().log("Initializing site at %s", site.toRealPath().normalize());
- new BaseInit(site, false, pluginsDistribution, pluginsToInstall).run();
+
+ @SuppressWarnings("unused")
+ var unused = new BaseInit(site, false, pluginsDistribution, pluginsToInstall).run();
+
return;
}
String path = System.getProperty(GERRIT_SITE_PATH);
Path site = null;
if (!Strings.isNullOrEmpty(path)) {
- site = Paths.get(path);
+ site = Path.of(path);
}
if (site == null && initPath != null) {
- site = Paths.get(initPath);
+ site = Path.of(initPath);
}
if (site != null) {
logger.atInfo().log("Initializing site at %s", site.toRealPath().normalize());
- new BaseInit(site, false, pluginsDistribution, pluginsToInstall).run();
+
+ @SuppressWarnings("unused")
+ var unused = new BaseInit(site, false, pluginsDistribution, pluginsToInstall).run();
}
} catch (Exception e) {
logger.atSevere().withCause(e).log("Site init failed");
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index e1abcb1c22..fa67034cdb 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -45,7 +45,7 @@ import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.lucene.LuceneIndexModule;
import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker;
-import com.google.gerrit.pgm.util.LogFileCompressor.LogFileCompressorModule;
+import com.google.gerrit.pgm.util.LogFileManager.LogFileManagerModule;
import com.google.gerrit.server.DefaultRefLogIdentityProvider;
import com.google.gerrit.server.LibModuleLoader;
import com.google.gerrit.server.LibModuleType;
@@ -132,7 +132,6 @@ import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -176,7 +175,7 @@ public class WebAppInitializer extends GuiceServletContextListener implements Fi
if (manager == null) {
String path = System.getProperty(GERRIT_SITE_PATH);
if (path != null) {
- sitePath = Paths.get(path);
+ sitePath = Path.of(path);
} else {
throw new ProvisionException(GERRIT_SITE_PATH + " must be defined");
}
@@ -303,7 +302,7 @@ public class WebAppInitializer extends GuiceServletContextListener implements Fi
private Injector createSysInjector() {
final List<Module> modules = new ArrayList<>();
modules.add(new DropWizardMetricMaker.RestModule());
- modules.add(new LogFileCompressorModule());
+ modules.add(new LogFileManagerModule());
modules.add(new EventBrokerModule());
modules.add(new JdbcAccountPatchReviewStoreModule(config));
modules.add(cfgInjector.getInstance(GitRepositoryManagerModule.class));
@@ -311,7 +310,10 @@ public class WebAppInitializer extends GuiceServletContextListener implements Fi
modules.add(new SysExecutorModule());
modules.add(new DiffExecutorModule());
modules.add(new MimeUtil2Module());
+
modules.add(cfgInjector.getInstance(AccountCacheImpl.AccountCacheModule.class));
+ modules.add(cfgInjector.getInstance(AccountCacheImpl.AccountCacheBindingModule.class));
+
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
modules.add(new GerritApiModule());
modules.add(new ProjectQueryBuilderModule());
diff --git a/java/com/google/gerrit/httpd/init/package-info.java b/java/com/google/gerrit/httpd/init/package-info.java
new file mode 100644
index 0000000000..8ca6c9f64b
--- /dev/null
+++ b/java/com/google/gerrit/httpd/init/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.httpd.init;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/httpd/package-info.java b/java/com/google/gerrit/httpd/package-info.java
new file mode 100644
index 0000000000..e0bb8e096d
--- /dev/null
+++ b/java/com/google/gerrit/httpd/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.httpd;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java b/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
index 9ab2d721b1..3d0a139f25 100644
--- a/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
+++ b/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
@@ -30,6 +30,7 @@ import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServlet;
+@SuppressWarnings("MutableGuiceModule")
class HttpAutoRegisterModuleGenerator extends ServletModule implements ModuleGenerator {
private final Map<String, Class<HttpServlet>> serve = new HashMap<>();
private final ListMultimap<TypeLiteral<?>, Class<?>> listeners = LinkedListMultimap.create();
diff --git a/java/com/google/gerrit/httpd/plugins/package-info.java b/java/com/google/gerrit/httpd/plugins/package-info.java
new file mode 100644
index 0000000000..1149405c83
--- /dev/null
+++ b/java/com/google/gerrit/httpd/plugins/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.httpd.plugins;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/httpd/raw/AuthorizationCheckServlet.java b/java/com/google/gerrit/httpd/raw/AuthorizationCheckServlet.java
index e3e96df9aa..1e043b1ce6 100644
--- a/java/com/google/gerrit/httpd/raw/AuthorizationCheckServlet.java
+++ b/java/com/google/gerrit/httpd/raw/AuthorizationCheckServlet.java
@@ -14,6 +14,8 @@
package com.google.gerrit.httpd.raw;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.util.http.CacheHeaders;
import com.google.inject.Inject;
@@ -44,8 +46,18 @@ public class AuthorizationCheckServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
CacheHeaders.setNotCacheable(res);
if (user.get().isIdentifiedUser()) {
- res.setContentLength(0);
- res.setStatus(HttpServletResponse.SC_NO_CONTENT);
+ if (req.getRequestURI().endsWith(".svg")) {
+ String responseToClient =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1\" height=\"1\"/>";
+ res.setContentType("image/svg+xml");
+ res.setCharacterEncoding(UTF_8.name());
+ res.setStatus(HttpServletResponse.SC_OK);
+ res.getWriter().write(responseToClient);
+ res.getWriter().flush();
+ } else {
+ res.setContentLength(0);
+ res.setStatus(HttpServletResponse.SC_NO_CONTENT);
+ }
} else {
res.setStatus(HttpServletResponse.SC_FORBIDDEN);
}
diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
index fb28d30061..77cbe5b9a4 100644
--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
@@ -146,7 +146,7 @@ public class IndexHtmlUtil {
}
/** Returns all static parameters of {@code index.html}. */
- static Map<String, Object> staticTemplateData(
+ static ImmutableMap<String, Object> staticTemplateData(
String canonicalURL,
String cdnPath,
String faviconPath,
diff --git a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
index 4c42e79c88..cc11638a04 100644
--- a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
@@ -99,6 +99,7 @@ public class IndexPreloadingUtil {
ListChangesOption.DETAILED_LABELS,
ListChangesOption.DOWNLOAD_COMMANDS,
ListChangesOption.MESSAGES,
+ ListChangesOption.REVIEWER_UPDATES,
ListChangesOption.SUBMITTABLE,
ListChangesOption.WEB_LINKS,
ListChangesOption.SKIP_DIFFSTAT,
diff --git a/java/com/google/gerrit/httpd/raw/StaticModule.java b/java/com/google/gerrit/httpd/raw/StaticModule.java
index d7909b211f..b00294f73e 100644
--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -19,6 +19,7 @@ import static com.google.gerrit.httpd.raw.StaticModuleConstants.POLYGERRIT_INDEX
import static java.nio.file.Files.exists;
import static java.nio.file.Files.isReadable;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
@@ -62,7 +63,7 @@ import org.eclipse.jgit.lib.Config;
public class StaticModule extends ServletModule {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- public static final String CHANGE_NUMBER_URI_REGEX = "^(?:/c)?/([1-9][0-9]*)/?$";
+ public static final String CHANGE_NUMBER_URI_REGEX = "^(?:/c)?/([1-9][0-9]*)/?.*";
private static final Pattern CHANGE_NUMBER_URI_PATTERN = Pattern.compile(CHANGE_NUMBER_URI_REGEX);
/**
@@ -87,19 +88,17 @@ public class StaticModule extends ServletModule {
private static final String ROBOTS_TXT_SERVLET = "RobotsTxtServlet";
private final GerritOptions options;
- private Paths paths;
+ private final Paths paths;
@Inject
public StaticModule(GerritOptions options) {
this.options = options;
+ this.paths = new Paths(options);
}
@Provides
@Singleton
private Paths getPaths() {
- if (paths == null) {
- paths = new Paths(options);
- }
return paths;
}
@@ -263,8 +262,7 @@ public class StaticModule extends ServletModule {
// root directory
warFs = null;
unpackedWar =
- java.nio.file.Paths.get(
- launcherLoadedFrom.getParentFile().getParentFile().getParentFile().toURI());
+ Path.of(launcherLoadedFrom.getParentFile().getParentFile().getParentFile().toURI());
sourceRoot = null;
development = false;
return;
@@ -354,7 +352,7 @@ public class StaticModule extends ServletModule {
}
@Singleton
- private static class PolyGerritFilter implements Filter {
+ protected static class PolyGerritFilter implements Filter {
private final HttpServlet polyGerritIndex;
private final PolyGerritUiServlet polygerritUI;
@@ -405,7 +403,8 @@ public class StaticModule extends ServletModule {
return matchPath(POLYGERRIT_ASSET_PATHS, path);
}
- private static boolean isPolyGerritIndex(String path) {
+ @VisibleForTesting
+ protected static boolean isPolyGerritIndex(String path) {
return !isChangeNumberUri(path) && matchPath(POLYGERRIT_INDEX_PATHS, path);
}
diff --git a/java/com/google/gerrit/httpd/raw/package-info.java b/java/com/google/gerrit/httpd/raw/package-info.java
new file mode 100644
index 0000000000..65050628a1
--- /dev/null
+++ b/java/com/google/gerrit/httpd/raw/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.httpd.raw;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/httpd/resources/package-info.java b/java/com/google/gerrit/httpd/resources/package-info.java
new file mode 100644
index 0000000000..94d35f6203
--- /dev/null
+++ b/java/com/google/gerrit/httpd/resources/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.httpd.resources;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 9b53c1792e..6951398ff6 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -51,6 +51,7 @@ import com.google.common.io.BaseEncoding;
import com.google.common.io.CountingOutputStream;
import com.google.common.math.IntMath;
import com.google.common.net.HttpHeaders;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.entities.Project;
@@ -520,6 +521,7 @@ public class RestApiServlet extends HttpServlet {
(RestReadView<RestResource>) viewData.view,
rsrc);
} else if (viewData.view instanceof RestModifyView<?, ?>) {
+ @SuppressWarnings("unchecked")
RestModifyView<RestResource, Object> m =
(RestModifyView<RestResource, Object>) viewData.view;
@@ -535,6 +537,7 @@ public class RestApiServlet extends HttpServlet {
}
}
} else if (viewData.view instanceof RestCollectionCreateView<?, ?, ?>) {
+ @SuppressWarnings("unchecked")
RestCollectionCreateView<RestResource, RestResource, Object> m =
(RestCollectionCreateView<RestResource, RestResource, Object>) viewData.view;
@@ -549,6 +552,7 @@ public class RestApiServlet extends HttpServlet {
}
}
} else if (viewData.view instanceof RestCollectionDeleteMissingView<?, ?, ?>) {
+ @SuppressWarnings("unchecked")
RestCollectionDeleteMissingView<RestResource, RestResource, Object> m =
(RestCollectionDeleteMissingView<RestResource, RestResource, Object>)
viewData.view;
@@ -564,6 +568,7 @@ public class RestApiServlet extends HttpServlet {
}
}
} else if (viewData.view instanceof RestCollectionModifyView<?, ?, ?>) {
+ @SuppressWarnings("unchecked")
RestCollectionModifyView<RestResource, RestResource, Object> m =
(RestCollectionModifyView<RestResource, RestResource, Object>) viewData.view;
@@ -1306,6 +1311,7 @@ public class RestApiServlet extends HttpServlet {
* @param result the object that should be formatted as JSON
* @return the length of the response
*/
+ @CanIgnoreReturnValue
public static long replyJson(
@Nullable HttpServletRequest req,
HttpServletResponse res,
@@ -1397,6 +1403,7 @@ public class RestApiServlet extends HttpServlet {
}
@SuppressWarnings("resource")
+ @CanIgnoreReturnValue
static long replyBinaryResult(
@Nullable HttpServletRequest req, HttpServletResponse res, BinaryResult bin)
throws IOException {
@@ -1698,9 +1705,21 @@ public class RestApiServlet extends HttpServlet {
if (rootCollection instanceof ProjectsCollection) {
requestInfo.project(Project.nameKey(resourceId));
} else if (rootCollection instanceof ChangesCollection) {
- Optional<ChangeNotes> changeNotes = globals.changeFinder.findOne(resourceId);
- if (changeNotes.isPresent()) {
- requestInfo.project(changeNotes.get().getProjectName());
+ try {
+ Optional<ChangeNotes> changeNotes =
+ globals
+ .retryHelper
+ .action(
+ ActionType.INDEX_QUERY,
+ "find-change",
+ () -> globals.changeFinder.findOne(resourceId))
+ .call();
+ if (changeNotes.isPresent()) {
+ requestInfo.project(changeNotes.get().getProjectName());
+ }
+ } catch (Exception e) {
+ logger.atWarning().withCause(e).log(
+ "failed looking up change %s to populate project in request info", resourceId);
}
}
return requestInfo.build();
@@ -1828,6 +1847,7 @@ public class RestApiServlet extends HttpServlet {
return uri;
}
+ @CanIgnoreReturnValue
public static long replyError(
HttpServletRequest req,
HttpServletResponse res,
@@ -1838,6 +1858,7 @@ public class RestApiServlet extends HttpServlet {
return replyError(req, res, statusCode, msg, CacheControl.NONE, err);
}
+ @CanIgnoreReturnValue
public static long replyError(
HttpServletRequest req,
HttpServletResponse res,
@@ -1866,6 +1887,7 @@ public class RestApiServlet extends HttpServlet {
* @param text the text reply
* @return the length of the response
*/
+ @CanIgnoreReturnValue
static long replyText(
@Nullable HttpServletRequest req, HttpServletResponse res, boolean allowTracing, String text)
throws IOException {
diff --git a/java/com/google/gerrit/httpd/restapi/package-info.java b/java/com/google/gerrit/httpd/restapi/package-info.java
new file mode 100644
index 0000000000..b38188f7b8
--- /dev/null
+++ b/java/com/google/gerrit/httpd/restapi/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.httpd.restapi;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/httpd/template/package-info.java b/java/com/google/gerrit/httpd/template/package-info.java
new file mode 100644
index 0000000000..374cc91118
--- /dev/null
+++ b/java/com/google/gerrit/httpd/template/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.httpd.template;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/index/Index.java b/java/com/google/gerrit/index/Index.java
index 3ed76bacec..ec530c1090 100644
--- a/java/com/google/gerrit/index/Index.java
+++ b/java/com/google/gerrit/index/Index.java
@@ -22,6 +22,7 @@ import com.google.gerrit.index.query.IndexPredicate;
import com.google.gerrit.index.query.Matchable;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
+import java.io.IOException;
import java.util.Optional;
/**
@@ -169,4 +170,15 @@ public interface Index<K, V> {
default Optional<Matchable<V>> getIndexFilter() {
return Optional.empty();
}
+
+ /**
+ * Creates a snapshot of the index.
+ *
+ * @param id an ID used for the snapshot.
+ * @return {@code true} if the snapshot was successful.
+ * @throws IOException if writing the snapshot to disk fails.
+ */
+ default boolean snapshot(String id) throws IOException {
+ return false;
+ }
}
diff --git a/java/com/google/gerrit/index/IndexCollection.java b/java/com/google/gerrit/index/IndexCollection.java
index c61e6c7bef..66a9fba447 100644
--- a/java/com/google/gerrit/index/IndexCollection.java
+++ b/java/com/google/gerrit/index/IndexCollection.java
@@ -16,6 +16,7 @@ package com.google.gerrit.index;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.events.LifecycleListener;
import java.util.Collection;
import java.util.Collections;
@@ -53,6 +54,7 @@ public abstract class IndexCollection<K, V, I extends Index<K, V>> implements Li
return Collections.unmodifiableCollection(writeIndexes);
}
+ @CanIgnoreReturnValue
public synchronized I addWriteIndex(I index) {
int version = index.getSchema().getVersion();
for (int i = 0; i < writeIndexes.size(); i++) {
diff --git a/java/com/google/gerrit/index/IndexDefinition.java b/java/com/google/gerrit/index/IndexDefinition.java
index f283bf1739..cbcb34ecc9 100644
--- a/java/com/google/gerrit/index/IndexDefinition.java
+++ b/java/com/google/gerrit/index/IndexDefinition.java
@@ -67,7 +67,11 @@ public abstract class IndexDefinition<K, V, I extends Index<K, V>> {
}
@Nullable
- public final SiteIndexer<K, V, I> getSiteIndexer() {
+ public SiteIndexer<K, V, I> getSiteIndexer() {
+ return siteIndexer;
+ }
+
+ public SiteIndexer<K, V, I> getSiteIndexer(boolean reuseExistingDocuments) {
return siteIndexer;
}
}
diff --git a/java/com/google/gerrit/index/IndexedField.java b/java/com/google/gerrit/index/IndexedField.java
index 94943d6ca9..d1aaff61e1 100644
--- a/java/com/google/gerrit/index/IndexedField.java
+++ b/java/com/google/gerrit/index/IndexedField.java
@@ -23,6 +23,7 @@ import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.reflect.TypeToken;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.converter.ProtoConverter;
import com.google.gerrit.exceptions.StorageException;
@@ -211,6 +212,7 @@ public abstract class IndexedField<I, T> {
return IndexedField.this;
}
+ @CanIgnoreReturnValue
private String checkName(String name) {
CharMatcher m = CharMatcher.anyOf("abcdefghijklmnopqrstuvwxyz0123456789_");
checkArgument(name != null && m.matchesAllOf(name), "illegal field name: %s", name);
@@ -350,6 +352,7 @@ public abstract class IndexedField<I, T> {
return this.getter(getter).fieldSetter(Optional.empty()).build();
}
+ @CanIgnoreReturnValue
private static String checkName(String name) {
String allowedCharacters = "abcdefghijklmnopqrstuvwxyz0123456789_";
CharMatcher m =
diff --git a/java/com/google/gerrit/index/Schema.java b/java/com/google/gerrit/index/Schema.java
index ab10d9ecfb..dcd7591af8 100644
--- a/java/com/google/gerrit/index/Schema.java
+++ b/java/com/google/gerrit/index/Schema.java
@@ -26,6 +26,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
@@ -48,11 +49,13 @@ public class Schema<T> {
private Optional<Integer> version = Optional.empty();
+ @CanIgnoreReturnValue
public Builder<T> version(int version) {
this.version = Optional.of(version);
return this;
}
+ @CanIgnoreReturnValue
public Builder<T> add(Schema<T> schema) {
this.indexedFields.addAll(schema.getIndexFields().values());
this.searchFields.addAll(schema.getSchemaFields().values());
@@ -63,10 +66,12 @@ public class Schema<T> {
}
@SafeVarargs
+ @CanIgnoreReturnValue
public final Builder<T> addSearchSpecs(IndexedField<T, ?>.SearchSpec... searchSpecs) {
return addSearchSpecs(ImmutableList.copyOf(searchSpecs));
}
+ @CanIgnoreReturnValue
public Builder<T> addSearchSpecs(ImmutableList<IndexedField<T, ?>.SearchSpec> searchSpecs) {
for (IndexedField<T, ?>.SearchSpec searchSpec : searchSpecs) {
checkArgument(
@@ -80,22 +85,26 @@ public class Schema<T> {
}
@SafeVarargs
+ @CanIgnoreReturnValue
public final Builder<T> addIndexedFields(IndexedField<T, ?>... fields) {
return addIndexedFields(ImmutableList.copyOf(fields));
}
+ @CanIgnoreReturnValue
public Builder<T> addIndexedFields(ImmutableList<IndexedField<T, ?>> indexedFields) {
this.indexedFields.addAll(indexedFields);
return this;
}
@SafeVarargs
+ @CanIgnoreReturnValue
public final Builder<T> remove(IndexedField<T, ?>.SearchSpec... searchSpecs) {
this.searchFields.removeAll(Arrays.asList(searchSpecs));
return this;
}
@SafeVarargs
+ @CanIgnoreReturnValue
public final Builder<T> remove(IndexedField<T, ?>... indexedFields) {
for (IndexedField<T, ?> field : indexedFields) {
ImmutableMap<String, ? extends IndexedField<T, ?>.SearchSpec> searchSpecs =
@@ -134,6 +143,7 @@ public class Schema<T> {
}
}
+ @CanIgnoreReturnValue
private static <T> SchemaField<T, ?> checkSame(SchemaField<T, ?> f1, SchemaField<T, ?> f2) {
checkState(f1 == f2, "Mismatched %s fields: %s != %s", f1.getName(), f1, f2);
return f1;
diff --git a/java/com/google/gerrit/index/SiteIndexer.java b/java/com/google/gerrit/index/SiteIndexer.java
index 32b4b21b99..bfb440721c 100644
--- a/java/com/google/gerrit/index/SiteIndexer.java
+++ b/java/com/google/gerrit/index/SiteIndexer.java
@@ -76,6 +76,16 @@ public abstract class SiteIndexer<K, V, I extends Index<K, V>> {
/** Indexes all entities for the provided index. */
public abstract Result indexAll(I index);
+ /**
+ * Indexes all entities for the provided index.
+ *
+ * <p>NOTE: This method does not implement the 'notifyListeners' logic which is effectively
+ * ignored and all listeners are always notified.
+ */
+ public Result indexAll(I index, @SuppressWarnings("unused") boolean notifyListeners) {
+ return indexAll(index);
+ }
+
protected final void addErrorListener(
ListenableFuture<?> future, String desc, ProgressMonitor progress, AtomicBoolean ok) {
future.addListener(
diff --git a/java/com/google/gerrit/index/package-info.java b/java/com/google/gerrit/index/package-info.java
new file mode 100644
index 0000000000..9c652fe78b
--- /dev/null
+++ b/java/com/google/gerrit/index/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.index;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/index/project/BUILD b/java/com/google/gerrit/index/project/BUILD
index b423f84f1f..b02951328f 100644
--- a/java/com/google/gerrit/index/project/BUILD
+++ b/java/com/google/gerrit/index/project/BUILD
@@ -9,6 +9,7 @@ java_library(
"//java/com/google/gerrit/index",
"//java/com/google/gerrit/index:query_exception",
"//lib:guava",
+ "//lib/errorprone:annotations",
"//lib/guice",
],
)
diff --git a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
index 6cd43db13e..3bccb0dfb4 100644
--- a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
+++ b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
@@ -81,6 +81,9 @@ public class ProjectSchemaDefinitions extends SchemaDefinitions<ProjectData> {
.addSearchSpecs(ProjectField.PREFIX_NAME_SPEC)
.build();
+ // Upgrade Lucene to 9.x requires reindexing.
+ static final Schema<ProjectData> V9 = schema(V8);
+
/**
* Name of the project index to be used when contacting index backends or loading configurations.
*/
diff --git a/java/com/google/gerrit/index/project/package-info.java b/java/com/google/gerrit/index/project/package-info.java
new file mode 100644
index 0000000000..8965ed5000
--- /dev/null
+++ b/java/com/google/gerrit/index/project/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.index.project;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/index/query/FilteredSource.java b/java/com/google/gerrit/index/query/FilteredSource.java
index 8269793e41..95e6435261 100644
--- a/java/com/google/gerrit/index/query/FilteredSource.java
+++ b/java/com/google/gerrit/index/query/FilteredSource.java
@@ -110,6 +110,11 @@ public class FilteredSource<T> implements DataSource<T> {
return cardinality;
}
+ /**
+ * Whether this data source matches with the given object.
+ *
+ * @param object object to be matched
+ */
protected boolean match(T object) {
return true;
}
diff --git a/java/com/google/gerrit/index/query/InternalQuery.java b/java/com/google/gerrit/index/query/InternalQuery.java
index d610dbf52e..24ce0fe94e 100644
--- a/java/com/google/gerrit/index/query/InternalQuery.java
+++ b/java/com/google/gerrit/index/query/InternalQuery.java
@@ -20,6 +20,7 @@ import static java.util.stream.Collectors.toSet;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.Index;
@@ -61,22 +62,26 @@ public class InternalQuery<T, Q extends InternalQuery<T, Q>> {
return (Q) this;
}
+ @CanIgnoreReturnValue
final Q setStart(int start) {
queryProcessor.setStart(start);
return self();
}
+ @CanIgnoreReturnValue
public final Q setLimit(int n) {
- queryProcessor.setUserProvidedLimit(n);
+ queryProcessor.setUserProvidedLimit(n, /* applyDefaultLimit */ false);
return self();
}
+ @CanIgnoreReturnValue
public final Q enforceVisibility(boolean enforce) {
queryProcessor.enforceVisibility(enforce);
return self();
}
@SafeVarargs
+ @CanIgnoreReturnValue
public final Q setRequestedFields(SchemaField<T, ?>... fields) {
checkArgument(fields.length > 0, "requested field list is empty");
queryProcessor.setRequestedFields(
@@ -84,6 +89,7 @@ public class InternalQuery<T, Q extends InternalQuery<T, Q>> {
return self();
}
+ @CanIgnoreReturnValue
public final Q noFields() {
queryProcessor.setRequestedFields(ImmutableSet.of());
return self();
diff --git a/java/com/google/gerrit/index/query/QueryProcessor.java b/java/com/google/gerrit/index/query/QueryProcessor.java
index f49cecb089..b8eb8bb6ea 100644
--- a/java/com/google/gerrit/index/query/QueryProcessor.java
+++ b/java/com/google/gerrit/index/query/QueryProcessor.java
@@ -122,6 +122,7 @@ public abstract class QueryProcessor<T> {
.build();
}
+ @CanIgnoreReturnValue
public QueryProcessor<T> setStart(int n) {
start = n;
return this;
@@ -141,11 +142,18 @@ public abstract class QueryProcessor<T> {
* @param enforce whether to enforce visibility.
* @return this.
*/
+ @CanIgnoreReturnValue
public QueryProcessor<T> enforceVisibility(boolean enforce) {
enforceVisibility = enforce;
return this;
}
+ /** Convenience method for API backward compatibility. */
+ @CanIgnoreReturnValue
+ public QueryProcessor<T> setUserProvidedLimit(int n) {
+ return setUserProvidedLimit(n, true);
+ }
+
/**
* Set an end-user-provided limit on the number of results returned.
*
@@ -154,13 +162,20 @@ public abstract class QueryProcessor<T> {
* account and choose the one that makes the most sense.
*
* @param n limit; zero or negative means no limit.
+ * @param applyDefaultLimit Should the default limit be applied, if n <= 0? For internal queries
+ * this should be false. For API endpoints this should be true.
* @return this.
*/
- public QueryProcessor<T> setUserProvidedLimit(int n) {
+ @CanIgnoreReturnValue
+ public QueryProcessor<T> setUserProvidedLimit(int n, boolean applyDefaultLimit) {
userProvidedLimit = n;
+ if (applyDefaultLimit && userProvidedLimit <= 0 && indexConfig.defaultLimit() > 0) {
+ userProvidedLimit = indexConfig.defaultLimit();
+ }
return this;
}
+ @CanIgnoreReturnValue
public QueryProcessor<T> setNoLimit(boolean isNoLimit) {
this.isNoLimit = isNoLimit;
return this;
@@ -172,6 +187,7 @@ public abstract class QueryProcessor<T> {
return this;
}
+ @CanIgnoreReturnValue
public QueryProcessor<T> setRequestedFields(Set<String> fields) {
requestedFields = fields;
return this;
@@ -433,8 +449,6 @@ public abstract class QueryProcessor<T> {
possibleLimits.add(getPermittedLimit());
if (userProvidedLimit > 0) {
possibleLimits.add(userProvidedLimit);
- } else if (indexConfig.defaultLimit() > 0) {
- possibleLimits.add(indexConfig.defaultLimit());
}
if (limitField != null) {
Integer limitFromPredicate = LimitPredicate.getLimit(limitField, p);
@@ -465,8 +479,4 @@ public abstract class QueryProcessor<T> {
}
protected abstract String formatForLogging(T t);
-
- protected abstract int getIndexSize();
-
- protected abstract int getBatchSize();
}
diff --git a/java/com/google/gerrit/index/query/package-info.java b/java/com/google/gerrit/index/query/package-info.java
new file mode 100644
index 0000000000..ab99f47cb8
--- /dev/null
+++ b/java/com/google/gerrit/index/query/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.index.query;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/index/query/testing/BUILD b/java/com/google/gerrit/index/query/testing/BUILD
index 1785f49c3b..1e61988473 100644
--- a/java/com/google/gerrit/index/query/testing/BUILD
+++ b/java/com/google/gerrit/index/query/testing/BUILD
@@ -12,6 +12,7 @@ java_library(
"//antlr3:query_parser",
"//lib:guava",
"//lib/antlr:java-runtime",
+ "//lib/errorprone:annotations",
"//lib/truth",
],
)
diff --git a/java/com/google/gerrit/index/query/testing/package-info.java b/java/com/google/gerrit/index/query/testing/package-info.java
new file mode 100644
index 0000000000..20604ad4e6
--- /dev/null
+++ b/java/com/google/gerrit/index/query/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.index.query.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
index 60402ba671..570290e235 100644
--- a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
+++ b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
@@ -135,7 +135,7 @@ public abstract class AbstractFakeIndex<K, V, D> implements Index<K, V> {
@Override
public DataSource<V> getSource(Predicate<V> p, QueryOptions opts) {
- List<V> results;
+ ImmutableList<V> results;
synchronized (indexedDocuments) {
Stream<V> valueStream =
indexedDocuments.values().stream()
@@ -292,7 +292,10 @@ public abstract class AbstractFakeIndex<K, V, D> implements Index<K, V> {
Integer.valueOf((String) doc.get(ChangeField.NUMERIC_ID_STR_SPEC.getName()))));
for (SchemaField<ChangeData, ?> field : getSchema().getSchemaFields().values()) {
boolean isProtoField = SchemaFieldDefs.isProtoField(field);
- field.setIfPossible(cd, new FakeStoredValue(doc.get(field.getName()), isProtoField));
+
+ @SuppressWarnings("unused")
+ var unused =
+ field.setIfPossible(cd, new FakeStoredValue(doc.get(field.getName()), isProtoField));
}
return cd;
}
diff --git a/java/com/google/gerrit/index/testing/BUILD b/java/com/google/gerrit/index/testing/BUILD
index 44bf70d357..c9f1994d48 100644
--- a/java/com/google/gerrit/index/testing/BUILD
+++ b/java/com/google/gerrit/index/testing/BUILD
@@ -16,6 +16,7 @@ java_library(
"//lib:guava",
"//lib:jgit",
"//lib:protobuf",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
"//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/index/testing/FakeIndexModule.java b/java/com/google/gerrit/index/testing/FakeIndexModule.java
index 126ff10e8f..b4d5315b01 100644
--- a/java/com/google/gerrit/index/testing/FakeIndexModule.java
+++ b/java/com/google/gerrit/index/testing/FakeIndexModule.java
@@ -21,7 +21,6 @@ import com.google.gerrit.server.index.VersionManager;
import com.google.gerrit.server.index.account.AccountIndex;
import com.google.gerrit.server.index.change.ChangeIndex;
import com.google.gerrit.server.index.group.GroupIndex;
-import java.util.Map;
/** Module to bind {@link FakeIndexModule}. */
public class FakeIndexModule extends AbstractIndexModule {
@@ -30,15 +29,16 @@ public class FakeIndexModule extends AbstractIndexModule {
}
public static FakeIndexModule singleVersionWithExplicitVersions(
- Map<String, Integer> versions, int threads, boolean secondary) {
+ ImmutableMap<String, Integer> versions, int threads, boolean secondary) {
return new FakeIndexModule(versions, threads, secondary);
}
public static FakeIndexModule latestVersion(boolean secondary) {
- return new FakeIndexModule(null, -1 /* direct executor */, secondary);
+ return new FakeIndexModule(/* singleVersions= */ null, -1 /* direct executor */, secondary);
}
- private FakeIndexModule(Map<String, Integer> singleVersions, int threads, boolean secondary) {
+ private FakeIndexModule(
+ ImmutableMap<String, Integer> singleVersions, int threads, boolean secondary) {
super(singleVersions, threads, secondary);
}
diff --git a/java/com/google/gerrit/index/testing/FakeIndexVersionManager.java b/java/com/google/gerrit/index/testing/FakeIndexVersionManager.java
index 5044e38093..40d51fd8b3 100644
--- a/java/com/google/gerrit/index/testing/FakeIndexVersionManager.java
+++ b/java/com/google/gerrit/index/testing/FakeIndexVersionManager.java
@@ -40,7 +40,12 @@ public class FakeIndexVersionManager extends VersionManager {
SitePaths sitePaths,
PluginSetContext<OnlineUpgradeListener> listeners,
Collection<IndexDefinition<?, ?, ?>> defs) {
- super(sitePaths, listeners, defs, VersionManager.getOnlineUpgrade(cfg));
+ super(
+ sitePaths,
+ listeners,
+ defs,
+ VersionManager.getOnlineUpgrade(cfg),
+ cfg.getBoolean("index", "reuseExistingDocuments", false));
}
@Override
diff --git a/java/com/google/gerrit/index/testing/package-info.java b/java/com/google/gerrit/index/testing/package-info.java
new file mode 100644
index 0000000000..0f6e5b231e
--- /dev/null
+++ b/java/com/google/gerrit/index/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.index.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/jgit/BUILD b/java/com/google/gerrit/jgit/BUILD
index 1041f1fe7d..04f2220cde 100644
--- a/java/com/google/gerrit/jgit/BUILD
+++ b/java/com/google/gerrit/jgit/BUILD
@@ -9,5 +9,6 @@ java_library(
deps = [
"//lib:gson",
"//lib:jgit",
+ "//lib/errorprone:annotations",
],
)
diff --git a/java/com/google/gerrit/jgit/diff/package-info.java b/java/com/google/gerrit/jgit/diff/package-info.java
new file mode 100644
index 0000000000..ba04b82b8f
--- /dev/null
+++ b/java/com/google/gerrit/jgit/diff/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.jgit.diff;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/json/BUILD b/java/com/google/gerrit/json/BUILD
index 7b2fe2f8d4..601a7b44a7 100644
--- a/java/com/google/gerrit/json/BUILD
+++ b/java/com/google/gerrit/json/BUILD
@@ -9,6 +9,7 @@ java_library(
"//java/com/google/gerrit/entities",
"//lib:gson",
"//lib:guava",
+ "//lib/errorprone:annotations",
"//lib/guice",
],
)
diff --git a/java/com/google/gerrit/json/package-info.java b/java/com/google/gerrit/json/package-info.java
new file mode 100644
index 0000000000..000b667335
--- /dev/null
+++ b/java/com/google/gerrit/json/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.json;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/launcher/BUILD b/java/com/google/gerrit/launcher/BUILD
index 15fa0ce9aa..309d1dd458 100644
--- a/java/com/google/gerrit/launcher/BUILD
+++ b/java/com/google/gerrit/launcher/BUILD
@@ -6,4 +6,7 @@ java_library(
name = "launcher",
srcs = ["GerritLauncher.java"],
visibility = ["//visibility:public"],
+ deps = [
+ "//lib/errorprone:annotations",
+ ],
)
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index 07a071ab2e..989c82b8fb 100644
--- a/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -36,7 +36,6 @@ import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.security.CodeSource;
import java.util.ArrayList;
import java.util.Arrays;
@@ -706,7 +705,7 @@ public final class GerritLauncher {
Path dir;
String sourceRoot = System.getProperty("sourceRoot");
if (sourceRoot != null) {
- dir = Paths.get(sourceRoot);
+ dir = Path.of(sourceRoot);
if (!Files.exists(dir)) {
throw new FileNotFoundException("source root not found: " + dir);
}
@@ -729,7 +728,7 @@ public final class GerritLauncher {
}
// Pop up to the top-level source folder by looking for WORKSPACE.
- dir = Paths.get(u.getPath());
+ dir = Path.of(u.getPath());
while (!Files.isRegularFile(dir.resolve("WORKSPACE"))) {
Path parent = dir.getParent();
if (parent == null) {
@@ -756,7 +755,7 @@ public final class GerritLauncher {
Path rootPath = resolveInSourceRoot(".").normalize();
Properties properties = loadBuildProperties(rootPath.resolve(".bazel_path"));
- Path outputBase = Paths.get(properties.getProperty("output_base"));
+ Path outputBase = Path.of(properties.getProperty("output_base"));
Path runtimeClasspath =
rootPath.resolve("bazel-bin/tools/eclipse/main_classpath_collect.runtime_classpath");
diff --git a/java/com/google/gerrit/launcher/package-info.java b/java/com/google/gerrit/launcher/package-info.java
new file mode 100644
index 0000000000..b30810c26e
--- /dev/null
+++ b/java/com/google/gerrit/launcher/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.launcher;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/lifecycle/BUILD b/java/com/google/gerrit/lifecycle/BUILD
index a3f3d81a87..fa3c2a3c2c 100644
--- a/java/com/google/gerrit/lifecycle/BUILD
+++ b/java/com/google/gerrit/lifecycle/BUILD
@@ -7,6 +7,7 @@ java_library(
deps = [
"//java/com/google/gerrit/extensions:api",
"//lib:guava",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
],
diff --git a/java/com/google/gerrit/lifecycle/package-info.java b/java/com/google/gerrit/lifecycle/package-info.java
new file mode 100644
index 0000000000..753cc8bcc4
--- /dev/null
+++ b/java/com/google/gerrit/lifecycle/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.lifecycle;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index 938cd67911..e00c394f6c 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
+import com.google.common.io.Files;
import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.AbstractFuture;
import com.google.common.util.concurrent.Futures;
@@ -54,6 +55,8 @@ import com.google.gerrit.server.logging.LoggingContextAwareExecutorService;
import com.google.gerrit.server.logging.LoggingContextAwareScheduledExecutorService;
import com.google.protobuf.MessageLite;
import java.io.IOException;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.Path;
import java.sql.Timestamp;
import java.util.Set;
import java.util.concurrent.Callable;
@@ -74,8 +77,10 @@ import org.apache.lucene.document.LongPoint;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
+import org.apache.lucene.index.IndexCommit;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.index.SnapshotDeletionPolicy;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.ControlledRealTimeReopenThread;
import org.apache.lucene.search.IndexSearcher;
@@ -88,6 +93,7 @@ import org.apache.lucene.search.Sort;
import org.apache.lucene.search.TopFieldDocs;
import org.apache.lucene.store.AlreadyClosedException;
import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;
/** Basic Lucene index implementation. */
public abstract class AbstractLuceneIndex<K, V> implements Index<K, V> {
@@ -134,6 +140,9 @@ public abstract class AbstractLuceneIndex<K, V> implements Index<K, V> {
String index = Joiner.on('_').skipNulls().join(name, subIndex);
long commitPeriod = writerConfig.getCommitWithinMs();
+ writerConfig.setIndexDeletionPolicy(
+ new SnapshotDeletionPolicy(writerConfig.getIndexDeletionPolicy()));
+
if (commitPeriod < 0) {
writer = new AutoCommitWriter(dir, writerConfig.getLuceneConfig());
} else if (commitPeriod == 0) {
@@ -516,6 +525,34 @@ public abstract class AbstractLuceneIndex<K, V> implements Index<K, V> {
return schema;
}
+ @Override
+ public boolean snapshot(String id) throws IOException {
+ SnapshotDeletionPolicy snapshooter =
+ (SnapshotDeletionPolicy) writer.getConfig().getIndexDeletionPolicy();
+
+ IndexCommit commit = snapshooter.snapshot();
+ try {
+ Path sourceDir = canonical(((FSDirectory) commit.getDirectory()).getDirectory());
+ Path indexDir = canonical(sitePaths.index_dir);
+ Path targetDir =
+ indexDir.resolve("snapshots").resolve(id).resolve(indexDir.relativize(sourceDir));
+ if (targetDir.toFile().exists()) {
+ throw new FileAlreadyExistsException(targetDir.toString());
+ }
+ targetDir.toFile().mkdirs();
+ for (String file : commit.getFileNames()) {
+ Files.copy(sourceDir.resolve(file).toFile(), targetDir.resolve(file).toFile());
+ }
+ } finally {
+ snapshooter.release(commit);
+ }
+ return true;
+ }
+
+ private static Path canonical(Path p) throws IOException {
+ return p.toFile().getCanonicalFile().toPath();
+ }
+
protected class LuceneQuerySource implements DataSource<V> {
private final QueryOptions opts;
private final Query query;
diff --git a/java/com/google/gerrit/lucene/BUILD b/java/com/google/gerrit/lucene/BUILD
index 879e7068aa..6584bfd301 100644
--- a/java/com/google/gerrit/lucene/BUILD
+++ b/java/com/google/gerrit/lucene/BUILD
@@ -10,7 +10,8 @@ java_library(
"//java/com/google/gerrit/index",
"//java/com/google/gerrit/index:query_exception",
"//lib:guava",
- "//lib/lucene:lucene-core-and-backward-codecs",
+ "//lib/errorprone:annotations",
+ "//lib/lucene:lucene-core",
],
)
@@ -35,11 +36,12 @@ java_library(
"//lib:guava",
"//lib:jgit",
"//lib:protobuf",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
"//lib/guice:guice-assistedinject",
"//lib/lucene:lucene-analyzers-common",
- "//lib/lucene:lucene-core-and-backward-codecs",
+ "//lib/lucene:lucene-core",
"//lib/lucene:lucene-misc",
],
)
diff --git a/java/com/google/gerrit/lucene/GerritIndexWriterConfig.java b/java/com/google/gerrit/lucene/GerritIndexWriterConfig.java
index f6b2f0edf6..bec63bd5cb 100644
--- a/java/com/google/gerrit/lucene/GerritIndexWriterConfig.java
+++ b/java/com/google/gerrit/lucene/GerritIndexWriterConfig.java
@@ -22,6 +22,7 @@ import com.google.gerrit.server.config.ConfigUtil;
import org.apache.lucene.analysis.CharArraySet;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.index.ConcurrentMergeScheduler;
+import org.apache.lucene.index.IndexDeletionPolicy;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.eclipse.jgit.lib.Config;
@@ -77,6 +78,14 @@ class GerritIndexWriterConfig {
}
}
+ void setIndexDeletionPolicy(IndexDeletionPolicy indexDeletionPolicy) {
+ luceneConfig.setIndexDeletionPolicy(indexDeletionPolicy);
+ }
+
+ IndexDeletionPolicy getIndexDeletionPolicy() {
+ return luceneConfig.getIndexDeletionPolicy();
+ }
+
CustomMappingAnalyzer getAnalyzer() {
return analyzer;
}
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index 3f2a5ae8ce..ffd25ba76b 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -17,6 +17,7 @@ package com.google.gerrit.lucene;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.lucene.AbstractLuceneIndex.sortFieldName;
import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
+import static com.google.gerrit.server.index.change.ChangeField.CHANGENUM_SPEC;
import static com.google.gerrit.server.index.change.ChangeField.NUMERIC_ID_STR_SPEC;
import static com.google.gerrit.server.index.change.ChangeField.PROJECT_SPEC;
import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES;
@@ -284,6 +285,11 @@ public class LuceneChangeIndex implements ChangeIndex {
openIndex.markReady(ready);
}
+ @Override
+ public boolean snapshot(String id) throws IOException {
+ return openIndex.snapshot(id) && closedIndex.snapshot(id);
+ }
+
private Sort getSort() {
return new Sort(
new SortField(UPDATED_SORT_FIELD, SortField.Type.LONG, true),
@@ -417,13 +423,16 @@ public class LuceneChangeIndex implements ChangeIndex {
TopFieldDocs subIndexHits =
searchers[i].searchAfter(
searchAfter, query, maxRemainingHits, sort, /* doDocScores= */ false);
+ assignShardIndexValues(subIndexHits, i);
searchAfterHitsCount += subIndexHits.scoreDocs.length;
hits.add(subIndexHits);
searchAfterBySubIndex.put(
subIndex, Iterables.getLast(Arrays.asList(subIndexHits.scoreDocs), searchAfter));
}
} else {
- hits.add(searchers[i].search(query, queryLimit, sort));
+ TopFieldDocs subIndexHits = searchers[i].search(query, queryLimit, sort);
+ assignShardIndexValues(subIndexHits, i);
+ hits.add(subIndexHits);
}
}
TopDocs docs = TopDocs.merge(sort, queryLimit, hits.stream().toArray(TopFieldDocs[]::new));
@@ -447,6 +456,25 @@ public class LuceneChangeIndex implements ChangeIndex {
}
}
+ /*
+ * Assign shard index values to the score documents.
+ *
+ * <p>TopDocs.merge()'s API has been changed to stop allowing passing in a parameter to
+ * indicate if it should set shard indices for hits as they are seen during the merge
+ * process. This is done to simplify the API to be more dynamic in terms of passing in
+ * custom tie breakers. If shard indices are to be used for tie breaking docs with equal
+ * scores during TopDocs.merge(), then it is mandatory that the input ScoreDocs have their
+ * shard indices set to valid values prior to calling merge().
+ *
+ * @param doc document
+ * @param shard index
+ */
+ private void assignShardIndexValues(TopFieldDocs doc, int shard) {
+ for (int docID = 0; docID < doc.scoreDocs.length; docID++) {
+ doc.scoreDocs[docID].shardIndex = shard;
+ }
+ }
+
/**
* Returns null for the first page or when pagination type is not {@link
* PaginationType#SEARCH_AFTER search-after}, otherwise returns the last doc from previous
@@ -501,7 +529,11 @@ public class LuceneChangeIndex implements ChangeIndex {
ImmutableList.Builder<ChangeData> result =
ImmutableList.builderWithExpectedSize(docs.size());
for (Document doc : docs) {
- result.add(toChangeData(fields(doc, fields), fields, NUMERIC_ID_STR_SPEC.getName()));
+ String fieldName =
+ doc.getField(CHANGENUM_SPEC.getName()) != null
+ ? CHANGENUM_SPEC.getName()
+ : NUMERIC_ID_STR_SPEC.getName();
+ result.add(toChangeData(fields(doc, fields), fields, fieldName));
}
return result.build();
} catch (InterruptedException e) {
@@ -562,7 +594,8 @@ public class LuceneChangeIndex implements ChangeIndex {
for (SchemaField<ChangeData, ?> field : getSchema().getSchemaFields().values()) {
if (fields.contains(field.getName())) {
- field.setIfPossible(cd, new LuceneStoredValue(doc.get(field.getName())));
+ @SuppressWarnings("unused")
+ var unused = field.setIfPossible(cd, new LuceneStoredValue(doc.get(field.getName())));
}
}
return cd;
diff --git a/java/com/google/gerrit/lucene/LuceneIndexModule.java b/java/com/google/gerrit/lucene/LuceneIndexModule.java
index 3aa9c6ef1b..89b83a49a3 100644
--- a/java/com/google/gerrit/lucene/LuceneIndexModule.java
+++ b/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -26,7 +26,6 @@ import com.google.gerrit.server.index.account.AccountIndex;
import com.google.gerrit.server.index.change.ChangeIndex;
import com.google.gerrit.server.index.group.GroupIndex;
import com.google.gerrit.server.index.options.AutoFlush;
-import java.util.Map;
import org.apache.lucene.search.BooleanQuery;
import org.eclipse.jgit.lib.Config;
@@ -41,17 +40,17 @@ public class LuceneIndexModule extends AbstractIndexModule {
@VisibleForTesting
public static LuceneIndexModule singleVersionWithExplicitVersions(
- Map<String, Integer> versions, int threads, boolean slave) {
+ ImmutableMap<String, Integer> versions, int threads, boolean slave) {
return new LuceneIndexModule(versions, threads, slave, AutoFlush.ENABLED);
}
public static LuceneIndexModule singleVersionWithExplicitVersions(
- Map<String, Integer> versions, int threads, boolean slave, AutoFlush autoFlush) {
+ ImmutableMap<String, Integer> versions, int threads, boolean slave, AutoFlush autoFlush) {
return new LuceneIndexModule(versions, threads, slave, autoFlush);
}
public static LuceneIndexModule latestVersion(boolean slave, AutoFlush autoFlush) {
- return new LuceneIndexModule(null, 0, slave, autoFlush);
+ return new LuceneIndexModule(/* singleVersions= */ null, 0, slave, autoFlush);
}
static boolean isInMemoryTest(Config cfg) {
@@ -59,7 +58,10 @@ public class LuceneIndexModule extends AbstractIndexModule {
}
private LuceneIndexModule(
- Map<String, Integer> singleVersions, int threads, boolean slave, AutoFlush autoFlush) {
+ ImmutableMap<String, Integer> singleVersions,
+ int threads,
+ boolean slave,
+ AutoFlush autoFlush) {
super(singleVersions, threads, slave);
this.autoFlush = autoFlush;
}
diff --git a/java/com/google/gerrit/lucene/LuceneVersionManager.java b/java/com/google/gerrit/lucene/LuceneVersionManager.java
index f3ba73ded9..265d3e02c2 100644
--- a/java/com/google/gerrit/lucene/LuceneVersionManager.java
+++ b/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -49,7 +49,12 @@ public class LuceneVersionManager extends VersionManager {
SitePaths sitePaths,
PluginSetContext<OnlineUpgradeListener> listeners,
Collection<IndexDefinition<?, ?, ?>> defs) {
- super(sitePaths, listeners, defs, VersionManager.getOnlineUpgrade(cfg));
+ super(
+ sitePaths,
+ listeners,
+ defs,
+ VersionManager.getOnlineUpgrade(cfg),
+ cfg.getBoolean("index", "reuseExistingDocuments", false));
}
@Override
diff --git a/java/com/google/gerrit/lucene/package-info.java b/java/com/google/gerrit/lucene/package-info.java
new file mode 100644
index 0000000000..fb5f8ec30d
--- /dev/null
+++ b/java/com/google/gerrit/lucene/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.lucene;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/mail/BUILD b/java/com/google/gerrit/mail/BUILD
index 0fe6c43a93..8830a666bb 100644
--- a/java/com/google/gerrit/mail/BUILD
+++ b/java/com/google/gerrit/mail/BUILD
@@ -10,6 +10,7 @@ java_library(
"//lib:guava",
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/jsoup",
"//lib/mime4j:core",
diff --git a/java/com/google/gerrit/mail/MailMessage.java b/java/com/google/gerrit/mail/MailMessage.java
index 2ce6cbb53e..b2385a7e5d 100644
--- a/java/com/google/gerrit/mail/MailMessage.java
+++ b/java/com/google/gerrit/mail/MailMessage.java
@@ -16,6 +16,7 @@ package com.google.gerrit.mail;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Address;
import java.time.Instant;
@@ -72,6 +73,7 @@ public abstract class MailMessage {
public abstract ImmutableList.Builder<Address> toBuilder();
+ @CanIgnoreReturnValue
public Builder addTo(Address val) {
toBuilder().add(val);
return this;
@@ -79,6 +81,7 @@ public abstract class MailMessage {
public abstract ImmutableList.Builder<Address> ccBuilder();
+ @CanIgnoreReturnValue
public Builder addCc(Address val) {
ccBuilder().add(val);
return this;
@@ -88,6 +91,7 @@ public abstract class MailMessage {
public abstract ImmutableList.Builder<String> additionalHeadersBuilder();
+ @CanIgnoreReturnValue
public Builder addAdditionalHeader(String val) {
additionalHeadersBuilder().add(val);
return this;
diff --git a/java/com/google/gerrit/mail/package-info.java b/java/com/google/gerrit/mail/package-info.java
new file mode 100644
index 0000000000..5db0133193
--- /dev/null
+++ b/java/com/google/gerrit/mail/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.mail;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/metrics/Description.java b/java/com/google/gerrit/metrics/Description.java
index f5963afaa3..d4d3bb3dbe 100644
--- a/java/com/google/gerrit/metrics/Description.java
+++ b/java/com/google/gerrit/metrics/Description.java
@@ -17,6 +17,7 @@ package com.google.gerrit.metrics;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -73,6 +74,7 @@ public class Description {
* @param unitName name of the unit, e.g. "requests", "seconds", etc.
* @return this
*/
+ @CanIgnoreReturnValue
public Description setUnit(String unitName) {
annotations.put(UNIT, unitName);
return this;
@@ -84,6 +86,7 @@ public class Description {
*
* @return this
*/
+ @CanIgnoreReturnValue
public Description setConstant() {
annotations.put(CONSTANT, TRUE_VALUE);
return this;
@@ -95,6 +98,7 @@ public class Description {
*
* @return this
*/
+ @CanIgnoreReturnValue
public Description setRate() {
annotations.put(RATE, TRUE_VALUE);
return this;
@@ -106,6 +110,7 @@ public class Description {
*
* @return this
*/
+ @CanIgnoreReturnValue
public Description setGauge() {
annotations.put(GAUGE, TRUE_VALUE);
return this;
@@ -117,6 +122,7 @@ public class Description {
*
* @return this
*/
+ @CanIgnoreReturnValue
public Description setCumulative() {
annotations.put(CUMULATIVE, TRUE_VALUE);
return this;
@@ -128,6 +134,7 @@ public class Description {
* @param ordering field ordering
* @return this
*/
+ @CanIgnoreReturnValue
public Description setFieldOrdering(FieldOrdering ordering) {
annotations.put(FIELD_ORDERING, ordering.name());
return this;
diff --git a/java/com/google/gerrit/metrics/MetricMaker.java b/java/com/google/gerrit/metrics/MetricMaker.java
index 3f9bab1471..00f07925f1 100644
--- a/java/com/google/gerrit/metrics/MetricMaker.java
+++ b/java/com/google/gerrit/metrics/MetricMaker.java
@@ -140,15 +140,18 @@ public abstract class MetricMaker {
* @param trigger trigger to connect
* @return registration handle
*/
+ @CanIgnoreReturnValue
public RegistrationHandle newTrigger(CallbackMetric<?> metric1, Runnable trigger) {
return newTrigger(ImmutableSet.of(metric1), trigger);
}
+ @CanIgnoreReturnValue
public RegistrationHandle newTrigger(
CallbackMetric<?> metric1, CallbackMetric<?> metric2, Runnable trigger) {
return newTrigger(ImmutableSet.of(metric1, metric2), trigger);
}
+ @CanIgnoreReturnValue
public RegistrationHandle newTrigger(
CallbackMetric<?> metric1,
CallbackMetric<?> metric2,
@@ -157,6 +160,7 @@ public abstract class MetricMaker {
return newTrigger(ImmutableSet.of(metric1, metric2, metric3), trigger);
}
+ @CanIgnoreReturnValue
public abstract RegistrationHandle newTrigger(Set<CallbackMetric<?>> metrics, Runnable trigger);
/**
diff --git a/java/com/google/gerrit/metrics/Timer0.java b/java/com/google/gerrit/metrics/Timer0.java
index 72ebc670b2..d59595ab16 100644
--- a/java/com/google/gerrit/metrics/Timer0.java
+++ b/java/com/google/gerrit/metrics/Timer0.java
@@ -49,8 +49,6 @@ public abstract class Timer0 implements RegistrationHandle {
}
}
- private boolean suppressLogging;
-
protected final String name;
public Timer0(String name) {
@@ -76,22 +74,14 @@ public abstract class Timer0 implements RegistrationHandle {
public final void record(long value, TimeUnit unit) {
long durationMs = unit.toMillis(value);
- if (!suppressLogging) {
- LoggingContext.getInstance()
- .addPerformanceLogRecord(() -> PerformanceLogRecord.create(name, durationMs));
- logger.atFinest().log("%s took %dms", name, durationMs);
- }
+ LoggingContext.getInstance()
+ .addPerformanceLogRecord(() -> PerformanceLogRecord.create(name, durationMs));
+ logger.atFinest().log("%s took %dms", name, durationMs);
doRecord(value, unit);
RequestStateContext.abortIfCancelled();
}
- /** Suppress logging (debug log and performance log) when values are recorded. */
- public final Timer0 suppressLogging() {
- this.suppressLogging = true;
- return this;
- }
-
/**
* Record a value in the distribution.
*
diff --git a/java/com/google/gerrit/metrics/TimerContext.java b/java/com/google/gerrit/metrics/TimerContext.java
index a3754c5ed7..0e01de03a2 100644
--- a/java/com/google/gerrit/metrics/TimerContext.java
+++ b/java/com/google/gerrit/metrics/TimerContext.java
@@ -14,6 +14,8 @@
package com.google.gerrit.metrics;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
abstract class TimerContext implements AutoCloseable {
private final long startNanos;
private boolean stopped;
@@ -40,6 +42,7 @@ abstract class TimerContext implements AutoCloseable {
* @return the elapsed time in nanoseconds.
* @throws IllegalStateException if the timer is already stopped.
*/
+ @CanIgnoreReturnValue
public long stop() {
if (!stopped) {
stopped = true;
diff --git a/java/com/google/gerrit/metrics/dropwizard/BUILD b/java/com/google/gerrit/metrics/dropwizard/BUILD
index dbb8f5ec5f..130eb8b824 100644
--- a/java/com/google/gerrit/metrics/dropwizard/BUILD
+++ b/java/com/google/gerrit/metrics/dropwizard/BUILD
@@ -12,6 +12,7 @@ java_library(
"//lib:args4j",
"//lib:guava",
"//lib/dropwizard:dropwizard-core",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
],
diff --git a/java/com/google/gerrit/metrics/dropwizard/BucketedCallback.java b/java/com/google/gerrit/metrics/dropwizard/BucketedCallback.java
index da9ec70e53..fdfe129330 100644
--- a/java/com/google/gerrit/metrics/dropwizard/BucketedCallback.java
+++ b/java/com/google/gerrit/metrics/dropwizard/BucketedCallback.java
@@ -19,6 +19,7 @@ import com.codahale.metrics.Metric;
import com.codahale.metrics.MetricRegistry;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Field;
import java.util.Iterator;
@@ -85,14 +86,17 @@ abstract class BucketedCallback<V> implements BucketedMetric {
}
}
+ @CanIgnoreReturnValue
ValueGauge getOrCreate(Object f1, Object f2) {
return getOrCreate(ImmutableList.of(f1, f2));
}
+ @CanIgnoreReturnValue
ValueGauge getOrCreate(Object f1, Object f2, Object f3) {
return getOrCreate(ImmutableList.of(f1, f2, f3));
}
+ @CanIgnoreReturnValue
ValueGauge getOrCreate(Object key) {
ValueGauge c = cells.get(key);
if (c != null) {
diff --git a/java/com/google/gerrit/metrics/dropwizard/package-info.java b/java/com/google/gerrit/metrics/dropwizard/package-info.java
new file mode 100644
index 0000000000..e91c94de56
--- /dev/null
+++ b/java/com/google/gerrit/metrics/dropwizard/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.metrics.dropwizard;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/metrics/package-info.java b/java/com/google/gerrit/metrics/package-info.java
new file mode 100644
index 0000000000..7d691d96ab
--- /dev/null
+++ b/java/com/google/gerrit/metrics/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.metrics;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/metrics/proc/package-info.java b/java/com/google/gerrit/metrics/proc/package-info.java
new file mode 100644
index 0000000000..0136afdab9
--- /dev/null
+++ b/java/com/google/gerrit/metrics/proc/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.metrics.proc;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/pgm/BUILD b/java/com/google/gerrit/pgm/BUILD
index 8523e8ad12..0967130fb3 100644
--- a/java/com/google/gerrit/pgm/BUILD
+++ b/java/com/google/gerrit/pgm/BUILD
@@ -51,6 +51,7 @@ java_library(
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
"//lib/commons:lang3",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
"//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java b/java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java
index 00e8fa42b4..9c74f780a0 100644
--- a/java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java
+++ b/java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java
@@ -14,6 +14,7 @@
package com.google.gerrit.pgm;
+import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.lifecycle.LifecycleManager;
@@ -35,7 +36,6 @@ import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.assistedinject.FactoryModuleBuilder;
import java.io.IOException;
-import java.util.Collection;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ProgressMonitor;
@@ -108,7 +108,7 @@ public class ChangeExternalIdCaseSensitivity extends SiteProgram {
return 0;
}
- Collection<ExternalId> todo = externalIds.all();
+ ImmutableSet<ExternalId> todo = externalIds.all();
monitor.beginTask("Converting external ID note names", todo.size());
manager.start();
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 6230136d73..198eeaa316 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -52,7 +52,7 @@ import com.google.gerrit.pgm.http.jetty.JettyEnv;
import com.google.gerrit.pgm.http.jetty.JettyModule;
import com.google.gerrit.pgm.http.jetty.ProjectQoSFilter.ProjectQoSFilterModule;
import com.google.gerrit.pgm.util.ErrorLogFile;
-import com.google.gerrit.pgm.util.LogFileCompressor.LogFileCompressorModule;
+import com.google.gerrit.pgm.util.LogFileManager.LogFileManagerModule;
import com.google.gerrit.pgm.util.RuntimeShutdown;
import com.google.gerrit.pgm.util.SiteProgram;
import com.google.gerrit.server.DefaultRefLogIdentityProvider;
@@ -103,6 +103,8 @@ import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier.SignedTokenEm
import com.google.gerrit.server.mail.receive.MailReceiver.MailReceiverModule;
import com.google.gerrit.server.mail.send.SmtpEmailSender.SmtpEmailSenderModule;
import com.google.gerrit.server.mime.MimeUtil2Module;
+import com.google.gerrit.server.notedb.NoteDbDraftCommentsModule;
+import com.google.gerrit.server.notedb.NoteDbStarredChangesModule;
import com.google.gerrit.server.notedb.RepoSequence.RepoSequenceModule;
import com.google.gerrit.server.patch.DiffExecutorModule;
import com.google.gerrit.server.permissions.DefaultPermissionBackendModule;
@@ -272,7 +274,8 @@ public class Daemon extends SiteProgram {
}
if (doInit) {
try {
- new Init(getSitePath()).run();
+ @SuppressWarnings("unused")
+ var unused = new Init(getSitePath()).run();
} catch (Exception e) {
throw die("Init failed", e);
}
@@ -447,7 +450,7 @@ public class Daemon extends SiteProgram {
final List<Module> modules = new ArrayList<>();
modules.add(NoteDbSchemaVersionCheck.module());
modules.add(new DropWizardMetricMaker.RestModule());
- modules.add(new LogFileCompressorModule());
+ modules.add(new LogFileManagerModule());
// Index module shutdown must happen before work queue shutdown, otherwise
// work queue can get stuck waiting on index futures that will never return.
@@ -466,11 +469,15 @@ public class Daemon extends SiteProgram {
modules.add(new SysExecutorModule());
modules.add(new DiffExecutorModule());
modules.add(new MimeUtil2Module());
+
modules.add(cfgInjector.getInstance(AccountCacheImpl.AccountCacheModule.class));
+ modules.add(cfgInjector.getInstance(AccountCacheImpl.AccountCacheBindingModule.class));
modules.add(new AccountNoteDbWriteStorageModule());
modules.add(new AccountNoteDbReadStorageModule());
modules.add(new RepoSequenceModule());
+ modules.add(new NoteDbDraftCommentsModule());
+ modules.add(new NoteDbStarredChangesModule());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
modules.add(new GerritApiModule());
modules.add(new ProjectQueryBuilderModule());
diff --git a/java/com/google/gerrit/pgm/Init.java b/java/com/google/gerrit/pgm/Init.java
index c05bff53e2..d3e9988d32 100644
--- a/java/com/google/gerrit/pgm/Init.java
+++ b/java/com/google/gerrit/pgm/Init.java
@@ -143,7 +143,7 @@ public class Init extends BaseInit {
@Override
protected void afterInit(SiteRun run) throws Exception {
- List<SchemaDefinitions<?>> schemaDefs =
+ ImmutableList<SchemaDefinitions<?>> schemaDefs =
ImmutableList.of(
AccountSchemaDefinitions.INSTANCE,
ChangeSchemaDefinitions.INSTANCE,
@@ -302,7 +302,9 @@ public class Init extends BaseInit {
.message(String.format("Init complete, reindexing %s with:", String.join(",", indices)));
getConsoleUI().message(" reindex " + reindexArgs.stream().collect(joining(" ")));
Reindex reindexPgm = new Reindex();
- reindexPgm.main(reindexArgs.stream().toArray(String[]::new));
+
+ @SuppressWarnings("unused")
+ var unused = reindexPgm.main(reindexArgs.stream().toArray(String[]::new));
}
private static boolean nullOrEmpty(List<?> list) {
diff --git a/java/com/google/gerrit/pgm/JythonShell.java b/java/com/google/gerrit/pgm/JythonShell.java
index d85bdc0a79..dcd89dcf23 100644
--- a/java/com/google/gerrit/pgm/JythonShell.java
+++ b/java/com/google/gerrit/pgm/JythonShell.java
@@ -70,12 +70,14 @@ public class JythonShell {
pyObject = findClass("org.python.core.PyObject");
pySystemState = findClass("org.python.core.PySystemState");
- runMethod(
- pySystemState,
- pySystemState,
- "initialize",
- new Class<?>[] {Properties.class, Properties.class},
- new Object[] {null, env});
+ @SuppressWarnings("unused")
+ var unused =
+ runMethod(
+ pySystemState,
+ pySystemState,
+ "initialize",
+ new Class<?>[] {Properties.class, Properties.class},
+ new Object[] {null, env});
try {
shell = console.getConstructor(new Class<?>[] {}).newInstance();
@@ -125,10 +127,12 @@ public class JythonShell {
}
protected void printInjectedVariable(String id) {
- runInterpreter(
- "exec",
- new Class<?>[] {String.class},
- new Object[] {"print '\"%s\" is \"%s\"' % (\"" + id + "\", " + id + ")"});
+ @SuppressWarnings("unused")
+ var unused =
+ runInterpreter(
+ "exec",
+ new Class<?>[] {String.class},
+ new Object[] {"print '\"%s\" is \"%s\"' % (\"" + id + "\", " + id + ")"});
}
public void run() {
@@ -136,19 +140,26 @@ public class JythonShell {
printInjectedVariable(key);
}
reload();
- runInterpreter(
- "interact",
- new Class<?>[] {String.class, pyObject},
- new Object[] {
- getDefaultBanner()
- + " running for Gerrit "
- + com.google.gerrit.common.Version.getVersion(),
- null,
- });
+
+ @SuppressWarnings("unused")
+ var unused =
+ runInterpreter(
+ "interact",
+ new Class<?>[] {String.class, pyObject},
+ new Object[] {
+ getDefaultBanner()
+ + " running for Gerrit "
+ + com.google.gerrit.common.Version.getVersion(),
+ null,
+ });
}
public void set(String key, Object content) {
- runInterpreter("set", new Class<?>[] {String.class, Object.class}, new Object[] {key, content});
+ @SuppressWarnings("unused")
+ var unused =
+ runInterpreter(
+ "set", new Class<?>[] {String.class, Object.class}, new Object[] {key, content});
+
injectedVariables.add(key);
}
@@ -181,12 +192,14 @@ public class JythonShell {
try {
File script = new File(parent, p);
if (script.canExecute()) {
- runMethod0(
- console,
- shell,
- "execfile",
- new Class<?>[] {String.class},
- new Object[] {script.getAbsolutePath()});
+ @SuppressWarnings("unused")
+ var unused =
+ runMethod0(
+ console,
+ shell,
+ "execfile",
+ new Class<?>[] {String.class},
+ new Object[] {script.getAbsolutePath()});
} else {
logger.atInfo().log(
"User initialization file %s is not found or not executable", script.getAbsolutePath());
@@ -200,12 +213,14 @@ public class JythonShell {
protected void execStream(InputStream in, String p) {
try {
- runMethod0(
- console,
- shell,
- "execfile",
- new Class<?>[] {InputStream.class, String.class},
- new Object[] {in, p});
+ @SuppressWarnings("unused")
+ var unused =
+ runMethod0(
+ console,
+ shell,
+ "execfile",
+ new Class<?>[] {InputStream.class, String.class},
+ new Object[] {in, p});
} catch (InvocationTargetException e) {
logger.atSevere().withCause(e).log("Exception occurred while loading %s", p);
}
diff --git a/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java b/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
index 6967fb10e4..db8cc161da 100644
--- a/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
+++ b/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
@@ -16,6 +16,7 @@ package com.google.gerrit.pgm;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.exceptions.DuplicateKeyException;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.lifecycle.LifecycleManager;
@@ -35,7 +36,6 @@ import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Provider;
import java.io.IOException;
-import java.util.Collection;
import java.util.Locale;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
@@ -78,7 +78,7 @@ public class LocalUsernamesToLowerCase extends SiteProgram {
})
.injectMembers(this);
- Collection<ExternalId> todo = externalIds.all();
+ ImmutableSet<ExternalId> todo = externalIds.all();
monitor.beginTask("Converting local usernames", todo.size());
try (Repository repo = repoManager.openRepository(allUsersName)) {
diff --git a/java/com/google/gerrit/pgm/Ls.java b/java/com/google/gerrit/pgm/Ls.java
index 4b48148e12..b705b3f941 100644
--- a/java/com/google/gerrit/pgm/Ls.java
+++ b/java/com/google/gerrit/pgm/Ls.java
@@ -16,6 +16,7 @@ package com.google.gerrit.pgm;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.launcher.GerritLauncher;
import com.google.gerrit.pgm.util.AbstractProgram;
import java.io.IOException;
@@ -49,7 +50,7 @@ public class Ls extends AbstractProgram {
return 0;
}
- private static Iterable<? extends ZipEntry> entriesOf(ZipFile zipFile) {
+ private static ImmutableList<? extends ZipEntry> entriesOf(ZipFile zipFile) {
return zipFile.stream().collect(toImmutableList());
}
}
diff --git a/java/com/google/gerrit/pgm/Reindex.java b/java/com/google/gerrit/pgm/Reindex.java
index a2e780d97d..e800d1788e 100644
--- a/java/com/google/gerrit/pgm/Reindex.java
+++ b/java/com/google/gerrit/pgm/Reindex.java
@@ -18,6 +18,7 @@ import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toSet;
import com.google.common.cache.Cache;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.gerrit.common.Die;
import com.google.gerrit.extensions.config.FactoryModule;
@@ -44,6 +45,8 @@ import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
import com.google.gerrit.server.index.options.AutoFlush;
import com.google.gerrit.server.index.options.BuildBloomFilter;
import com.google.gerrit.server.index.options.IsFirstInsertForEntry;
+import com.google.gerrit.server.notedb.NoteDbDraftCommentsModule;
+import com.google.gerrit.server.notedb.NoteDbStarredChangesModule;
import com.google.gerrit.server.notedb.RepoSequence.RepoSequenceModule;
import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
import com.google.gerrit.server.util.ReplicaUtil;
@@ -59,9 +62,7 @@ import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
@@ -97,6 +98,8 @@ public class Reindex extends SiteProgram {
@Option(name = "--build-bloom-filter", usage = "Build bloom filter for H2 disk caches.")
private boolean buildBloomFilter;
+ private Boolean reuseExistingDocumentsOption;
+
private Injector dbInjector;
private Injector sysInjector;
private Injector cfgInjector;
@@ -105,6 +108,11 @@ public class Reindex extends SiteProgram {
@Inject private Collection<IndexDefinition<?, ?, ?>> indexDefs;
@Inject private DynamicMap<Cache<?, ?>> cacheMap;
+ @Option(name = "--reuse", usage = "Reindex only when existing index entry is stale")
+ public void setReuseExistingDocuments(boolean value) {
+ reuseExistingDocumentsOption = value;
+ }
+
@Override
public int run() throws Exception {
mustHaveValidSite();
@@ -173,10 +181,10 @@ public class Reindex extends SiteProgram {
}
private Injector createSysInjector() {
- Map<String, Integer> versions = new HashMap<>();
- if (changesVersion != null) {
- versions.put(ChangeSchemaDefinitions.INSTANCE.getName(), changesVersion);
- }
+ ImmutableMap<String, Integer> versions =
+ changesVersion != null
+ ? ImmutableMap.of(ChangeSchemaDefinitions.INSTANCE.getName(), changesVersion)
+ : ImmutableMap.of();
boolean replica = ReplicaUtil.isReplica(globalConfig);
List<Module> modules = new ArrayList<>();
modules.add(new WorkQueueModule());
@@ -194,7 +202,7 @@ public class Reindex extends SiteProgram {
Class<?> clazz = Class.forName("com.google.gerrit.index.testing.FakeIndexModule");
Method m =
clazz.getMethod(
- "singleVersionWithExplicitVersions", Map.class, int.class, boolean.class);
+ "singleVersionWithExplicitVersions", ImmutableMap.class, int.class, boolean.class);
indexModule = (Module) m.invoke(null, versions, threads, replica);
} catch (NoSuchMethodException
| ClassNotFoundException
@@ -230,6 +238,8 @@ public class Reindex extends SiteProgram {
modules.add(new AccountNoteDbWriteStorageModule());
modules.add(new AccountNoteDbReadStorageModule());
modules.add(new RepoSequenceModule());
+ modules.add(new NoteDbDraftCommentsModule());
+ modules.add(new NoteDbStarredChangesModule());
return dbInjector.createChildInjector(
ModuleOverloader.override(
@@ -255,9 +265,16 @@ public class Reindex extends SiteProgram {
requireNonNull(
index, () -> String.format("no active search index configured for %s", def.getName()));
index.markReady(false);
- index.deleteAll();
+ boolean reuseExistingDocuments =
+ reuseExistingDocumentsOption != null
+ ? reuseExistingDocumentsOption
+ : globalConfig.getBoolean("index", null, "reuseExistingDocuments", false);
+
+ if (!reuseExistingDocuments) {
+ index.deleteAll();
+ }
- SiteIndexer<K, V, I> siteIndexer = def.getSiteIndexer();
+ SiteIndexer<K, V, I> siteIndexer = def.getSiteIndexer(reuseExistingDocuments);
siteIndexer.setProgressOut(System.err);
siteIndexer.setVerboseOut(verbose ? System.out : NullOutputStream.INSTANCE);
SiteIndexer.Result result = siteIndexer.indexAll(index);
diff --git a/java/com/google/gerrit/pgm/SwitchSecureStore.java b/java/com/google/gerrit/pgm/SwitchSecureStore.java
index 063fcdbaf6..3cd0c47d9d 100644
--- a/java/com/google/gerrit/pgm/SwitchSecureStore.java
+++ b/java/com/google/gerrit/pgm/SwitchSecureStore.java
@@ -31,7 +31,6 @@ import com.google.inject.Injector;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.jar.JarFile;
@@ -63,7 +62,7 @@ public class SwitchSecureStore extends SiteProgram {
@Override
public int run() throws Exception {
SitePaths sitePaths = new SitePaths(getSitePath());
- Path newSecureStorePath = Paths.get(newSecureStoreLib);
+ Path newSecureStorePath = Path.of(newSecureStoreLib);
if (!Files.exists(newSecureStorePath)) {
logger.atSevere().log("File %s doesn't exist", newSecureStorePath.toAbsolutePath());
return -1;
diff --git a/java/com/google/gerrit/pgm/WarDistribution.java b/java/com/google/gerrit/pgm/WarDistribution.java
index 013c8501e5..37178b0d4d 100644
--- a/java/com/google/gerrit/pgm/WarDistribution.java
+++ b/java/com/google/gerrit/pgm/WarDistribution.java
@@ -18,6 +18,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.pgm.init.InitPlugins.JAR;
import static com.google.gerrit.pgm.init.InitPlugins.PLUGIN_DIR;
+import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.launcher.GerritLauncher;
import com.google.gerrit.pgm.init.PluginsDistribution;
@@ -64,7 +65,7 @@ public class WarDistribution implements PluginsDistribution {
throw new UnsupportedOperationException();
}
- private static Iterable<? extends ZipEntry> entriesOf(ZipFile zipFile) {
+ private static ImmutableList<? extends ZipEntry> entriesOf(ZipFile zipFile) {
return zipFile.stream().collect(toImmutableList());
}
}
diff --git a/java/com/google/gerrit/pgm/http/jetty/BUILD b/java/com/google/gerrit/pgm/http/jetty/BUILD
index cd188f51c3..e006c91fed 100644
--- a/java/com/google/gerrit/pgm/http/jetty/BUILD
+++ b/java/com/google/gerrit/pgm/http/jetty/BUILD
@@ -18,6 +18,7 @@ java_library(
"//lib:guava",
"//lib:jgit",
"//lib:servlet-api",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
"//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/pgm/http/jetty/package-info.java b/java/com/google/gerrit/pgm/http/jetty/package-info.java
new file mode 100644
index 0000000000..45a3b4f412
--- /dev/null
+++ b/java/com/google/gerrit/pgm/http/jetty/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.pgm.http.jetty;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/pgm/init/AccountsOnInitNoteDbImpl.java b/java/com/google/gerrit/pgm/init/AccountsOnInitNoteDbImpl.java
index e3e485f2d7..5584eba293 100644
--- a/java/com/google/gerrit/pgm/init/AccountsOnInitNoteDbImpl.java
+++ b/java/com/google/gerrit/pgm/init/AccountsOnInitNoteDbImpl.java
@@ -110,6 +110,7 @@ public class AccountsOnInitNoteDbImpl implements AccountsOnInit {
throw new IOException(String.format("Failed to update ref %s: %s", refName, result.name()));
}
account.setMetaId(id.name());
+ account.setUniqueTag(id.name());
}
return account.build();
}
diff --git a/java/com/google/gerrit/pgm/init/BUILD b/java/com/google/gerrit/pgm/init/BUILD
index 0c0d937f48..73c37609ec 100644
--- a/java/com/google/gerrit/pgm/init/BUILD
+++ b/java/com/google/gerrit/pgm/init/BUILD
@@ -25,6 +25,7 @@ java_library(
"//lib:guava",
"//lib:jgit",
"//lib/commons:validator",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
"//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/pgm/init/BaseInit.java b/java/com/google/gerrit/pgm/init/BaseInit.java
index abaefb259a..1f565129cc 100644
--- a/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -62,7 +62,6 @@ import java.lang.reflect.InvocationTargetException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
@@ -313,7 +312,7 @@ public class BaseInit extends SiteProgram {
return null;
}
- Path secureStoreLib = Paths.get(secureStore);
+ Path secureStoreLib = Path.of(secureStore);
if (!Files.exists(secureStoreLib)) {
throw new InvalidSecureStoreException(String.format("File %s doesn't exist", secureStore));
}
diff --git a/java/com/google/gerrit/pgm/init/InitAdminUser.java b/java/com/google/gerrit/pgm/init/InitAdminUser.java
index 3dce97447d..44ad96ee04 100644
--- a/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -40,7 +40,6 @@ import com.google.inject.Inject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -186,7 +185,7 @@ public class InitAdminUser implements InitStep {
@Nullable
private AccountSshKey readSshKey(Account.Id id) throws IOException {
String defaultPublicSshKeyFile = "";
- Path defaultPublicSshKeyPath = Paths.get(System.getProperty("user.home"), ".ssh", "id_rsa.pub");
+ Path defaultPublicSshKeyPath = Path.of(System.getProperty("user.home"), ".ssh", "id_rsa.pub");
if (Files.exists(defaultPublicSshKeyPath)) {
defaultPublicSshKeyFile = defaultPublicSshKeyPath.toString();
}
@@ -195,7 +194,7 @@ public class InitAdminUser implements InitStep {
}
private AccountSshKey createSshKey(Account.Id id, String keyFile) throws IOException {
- Path p = Paths.get(keyFile);
+ Path p = Path.of(keyFile);
if (!Files.exists(p)) {
throw new IOException(String.format("Cannot add public SSH key: %s is not a file", keyFile));
}
diff --git a/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java b/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
index acde91ff60..34f6615a81 100644
--- a/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
+++ b/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
@@ -17,6 +17,7 @@ package com.google.gerrit.pgm.init;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.base.Strings;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.pgm.init.api.AllUsersNameOnInitProvider;
@@ -63,6 +64,7 @@ public class VersionedAuthorizedKeysOnInit extends VersionedMetaDataOnInit {
keys = AuthorizedKeys.parse(accountId, readUTF8(AuthorizedKeys.FILE_NAME));
}
+ @CanIgnoreReturnValue
public AccountSshKey addKey(String pub) {
checkState(keys != null, "SSH keys not loaded yet");
int seq = keys.isEmpty() ? 1 : keys.size() + 1;
diff --git a/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java b/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
index abd7d43e18..226f12bfe1 100644
--- a/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
+++ b/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
@@ -15,6 +15,7 @@
package com.google.gerrit.pgm.init.api;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.server.config.AllProjectsConfigProvider;
@@ -58,6 +59,7 @@ public class AllProjectsConfig extends VersionedMetaDataOnInit {
}
@Override
+ @CanIgnoreReturnValue
public AllProjectsConfig load() throws IOException, ConfigInvalidException {
super.load();
return this;
diff --git a/java/com/google/gerrit/pgm/init/api/GitRepositoryManagerOnInit.java b/java/com/google/gerrit/pgm/init/api/GitRepositoryManagerOnInit.java
index fabad490a9..dd2978338e 100644
--- a/java/com/google/gerrit/pgm/init/api/GitRepositoryManagerOnInit.java
+++ b/java/com/google/gerrit/pgm/init/api/GitRepositoryManagerOnInit.java
@@ -44,7 +44,8 @@ public class GitRepositoryManagerOnInit implements GitRepositoryManager {
@Override
public Status getRepositoryStatus(NameKey name) {
try {
- openRepository(name);
+ @SuppressWarnings("unused")
+ var unused = openRepository(name);
} catch (RepositoryNotFoundException e) {
return Status.NON_EXISTENT;
} catch (IOException e) {
diff --git a/java/com/google/gerrit/pgm/init/api/Section.java b/java/com/google/gerrit/pgm/init/api/Section.java
index 5cc4b5dbca..720c1f8447 100644
--- a/java/com/google/gerrit/pgm/init/api/Section.java
+++ b/java/com/google/gerrit/pgm/init/api/Section.java
@@ -14,6 +14,7 @@
package com.google.gerrit.pgm.init.api;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.securestore.SecureStore;
@@ -100,10 +101,12 @@ public class Section {
set(name, (String) null);
}
+ @CanIgnoreReturnValue
public String string(String title, String name, String dv) {
return string(title, name, dv, false);
}
+ @CanIgnoreReturnValue
public String string(final String title, String name, String dv, boolean nullIfDefault) {
final String ov = get(name);
String nv = ui.readString(ov != null ? ov : dv, "%s", title);
@@ -120,11 +123,13 @@ public class Section {
return site.resolve(string(title, name, defValue));
}
+ @CanIgnoreReturnValue
public <T extends Enum<?>, E extends EnumSet<? extends T>> T select(
String title, String name, T defValue) {
return select(title, name, defValue, false);
}
+ @CanIgnoreReturnValue
public <T extends Enum<?>, E extends EnumSet<? extends T>> T select(
String title, String name, T defValue, boolean nullIfDefault) {
@SuppressWarnings("rawtypes")
@@ -134,11 +139,13 @@ public class Section {
return select(title, name, defValue, allowedValues, nullIfDefault);
}
+ @CanIgnoreReturnValue
public <T extends Enum<?>, E extends EnumSet<? extends T>> T select(
String title, String name, T defValue, E allowedValues) {
return select(title, name, defValue, allowedValues, false);
}
+ @CanIgnoreReturnValue
public <T extends Enum<?>, A extends EnumSet<? extends T>> T select(
String title, String name, T defValue, A allowedValues, boolean nullIfDefault) {
final boolean set = get(name) != null;
@@ -167,6 +174,7 @@ public class Section {
}
@Nullable
+ @CanIgnoreReturnValue
public String password(String username, String password) {
final String ov = getSecure(password);
@@ -196,6 +204,7 @@ public class Section {
return nv;
}
+ @CanIgnoreReturnValue
public String passwordForKey(String prompt, String passwordKey) {
String ov = getSecure(passwordKey);
if (ov != null) {
diff --git a/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java b/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java
index f77960131a..0a82c9802d 100644
--- a/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java
+++ b/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java
@@ -14,6 +14,7 @@
package com.google.gerrit.pgm.init.api;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.GerritPersonIdentProvider;
import com.google.gerrit.server.config.SitePaths;
@@ -54,6 +55,7 @@ public abstract class VersionedMetaDataOnInit extends VersionedMetaData {
return ref;
}
+ @CanIgnoreReturnValue
public VersionedMetaDataOnInit load() throws IOException, ConfigInvalidException {
File path = getPath();
if (path != null) {
@@ -89,7 +91,9 @@ public abstract class VersionedMetaDataOnInit extends VersionedMetaData {
commit.setCommitter(ident);
commit.setMessage(msg);
- onSave(commit);
+ if (!onSave(commit)) {
+ return;
+ }
ObjectId res = newTree.writeTree(inserter);
if (res.equals(srcTree)) {
diff --git a/java/com/google/gerrit/pgm/init/api/package-info.java b/java/com/google/gerrit/pgm/init/api/package-info.java
new file mode 100644
index 0000000000..ef26020b96
--- /dev/null
+++ b/java/com/google/gerrit/pgm/init/api/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.pgm.init.api;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/pgm/init/index/IndexModuleOnInit.java b/java/com/google/gerrit/pgm/init/index/IndexModuleOnInit.java
index d1d0729894..65e749337c 100644
--- a/java/com/google/gerrit/pgm/init/index/IndexModuleOnInit.java
+++ b/java/com/google/gerrit/pgm/init/index/IndexModuleOnInit.java
@@ -15,9 +15,9 @@
package com.google.gerrit.pgm.init.index;
import com.google.common.collect.FluentIterable;
-import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.index.IndexDefinition;
import com.google.gerrit.index.SchemaDefinitions;
@@ -40,12 +40,11 @@ import com.google.inject.name.Names;
import com.google.inject.util.Providers;
import java.util.Collection;
import java.util.Map;
-import java.util.Set;
public class IndexModuleOnInit extends AbstractModule {
static final String INDEX_MANAGER = "IndexModuleOnInit/IndexManager";
- private static final ImmutableCollection<SchemaDefinitions<?>> ALL_SCHEMA_DEFS =
+ private static final ImmutableList<SchemaDefinitions<?>> ALL_SCHEMA_DEFS =
ImmutableList.of(AccountSchemaDefinitions.INSTANCE, GroupSchemaDefinitions.INSTANCE);
@Override
@@ -83,10 +82,11 @@ public class IndexModuleOnInit extends AbstractModule {
@Provides
Collection<IndexDefinition<?, ?, ?>> getIndexDefinitions(
AccountIndexDefinition accounts, GroupIndexDefinition groups) {
- Collection<IndexDefinition<?, ?, ?>> result = ImmutableList.of(accounts, groups);
- Set<String> expected =
+ ImmutableList<IndexDefinition<?, ?, ?>> result = ImmutableList.of(accounts, groups);
+ ImmutableSet<String> expected =
FluentIterable.from(ALL_SCHEMA_DEFS).transform(SchemaDefinitions::getName).toSet();
- Set<String> actual = FluentIterable.from(result).transform(IndexDefinition::getName).toSet();
+ ImmutableSet<String> actual =
+ FluentIterable.from(result).transform(IndexDefinition::getName).toSet();
if (!expected.equals(actual)) {
throw new ProvisionException(
"need index definitions for all schemas: " + expected + " != " + actual);
diff --git a/java/com/google/gerrit/pgm/init/index/lucene/package-info.java b/java/com/google/gerrit/pgm/init/index/lucene/package-info.java
new file mode 100644
index 0000000000..e72368b47a
--- /dev/null
+++ b/java/com/google/gerrit/pgm/init/index/lucene/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.pgm.init.index.lucene;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/pgm/init/index/package-info.java b/java/com/google/gerrit/pgm/init/index/package-info.java
new file mode 100644
index 0000000000..cb32880770
--- /dev/null
+++ b/java/com/google/gerrit/pgm/init/index/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.pgm.init.index;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/pgm/init/package-info.java b/java/com/google/gerrit/pgm/init/package-info.java
new file mode 100644
index 0000000000..f4f49452e5
--- /dev/null
+++ b/java/com/google/gerrit/pgm/init/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.pgm.init;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/pgm/package-info.java b/java/com/google/gerrit/pgm/package-info.java
new file mode 100644
index 0000000000..3e7920f8b4
--- /dev/null
+++ b/java/com/google/gerrit/pgm/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.pgm;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/pgm/rules/package-info.java b/java/com/google/gerrit/pgm/rules/package-info.java
new file mode 100644
index 0000000000..5ac41760cc
--- /dev/null
+++ b/java/com/google/gerrit/pgm/rules/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.pgm.rules;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/pgm/util/BUILD b/java/com/google/gerrit/pgm/util/BUILD
index 5b01c9c8e5..ad4ce887e5 100644
--- a/java/com/google/gerrit/pgm/util/BUILD
+++ b/java/com/google/gerrit/pgm/util/BUILD
@@ -23,6 +23,7 @@ java_library(
"//lib:gson",
"//lib:guava",
"//lib:jgit",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
"//lib/log:log4j",
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index 21ae8e1586..f45f1be5d0 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -27,7 +27,6 @@ import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.server.ChangeDraftUpdate;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.DefaultRefLogIdentityProvider;
import com.google.gerrit.server.IdentifiedUser;
@@ -73,9 +72,9 @@ import com.google.gerrit.server.extensions.events.WorkInProgressStateChanged;
import com.google.gerrit.server.git.ChangesByProjectCache;
import com.google.gerrit.server.git.PureRevertCache;
import com.google.gerrit.server.git.TagCache;
-import com.google.gerrit.server.notedb.ChangeDraftNotesUpdate;
import com.google.gerrit.server.notedb.NoteDbModule;
import com.google.gerrit.server.patch.DiffExecutorModule;
+import com.google.gerrit.server.patch.DiffOperationsForCommitValidation;
import com.google.gerrit.server.patch.DiffOperationsImpl;
import com.google.gerrit.server.patch.PatchListCacheImpl;
import com.google.gerrit.server.permissions.DefaultPermissionBackendModule;
@@ -114,7 +113,7 @@ import org.eclipse.jgit.lib.Config;
/** Module for programs that perform batch operations on a site. */
public class BatchProgramModule extends FactoryModule {
- private Injector parentInjector;
+ private final Injector parentInjector;
public BatchProgramModule(Injector parentInjector) {
this.parentInjector = parentInjector;
@@ -164,6 +163,7 @@ public class BatchProgramModule extends FactoryModule {
bind(CurrentUser.class).to(InternalUser.class);
factory(PatchSetInserter.Factory.class);
factory(RebaseChangeOp.Factory.class);
+ factory(DiffOperationsForCommitValidation.Factory.class);
bind(new TypeLiteral<ImmutableSet<GroupReference>>() {})
.annotatedWith(AdministrateServerGroups.class)
@@ -185,6 +185,7 @@ public class BatchProgramModule extends FactoryModule {
modules.add(new GroupModule());
modules.add(new NoteDbModule());
modules.add(AccountCacheImpl.module());
+ modules.add(AccountCacheImpl.bindingModule());
modules.add(ConflictsCacheImpl.module());
modules.add(DefaultPreferencesCacheImpl.module());
modules.add(GroupCacheImpl.module());
@@ -204,7 +205,6 @@ public class BatchProgramModule extends FactoryModule {
factory(DistinctVotersPredicate.Factory.class);
factory(HasSubmoduleUpdatePredicate.Factory.class);
factory(ProjectState.Factory.class);
- bind(ChangeDraftUpdate.ChangeDraftUpdateFactory.class).to(ChangeDraftNotesUpdate.Factory.class);
DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeOperatorFactory.class);
DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeHasOperandFactory.class);
diff --git a/java/com/google/gerrit/pgm/util/LogFileCompressor.java b/java/com/google/gerrit/pgm/util/LogFileManager.java
index 5e49312e98..902f7d64a6 100644
--- a/java/com/google/gerrit/pgm/util/LogFileCompressor.java
+++ b/java/com/google/gerrit/pgm/util/LogFileManager.java
@@ -17,10 +17,12 @@ package com.google.gerrit.pgm.util;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.ByteStreams;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.git.WorkQueue;
@@ -31,18 +33,30 @@ import java.io.OutputStream;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.time.Duration;
+import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
+import java.util.Optional;
import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.zip.GZIPOutputStream;
import org.eclipse.jgit.lib.Config;
-/** Compresses the old error logs. */
-public class LogFileCompressor implements Runnable {
+/** Compresses and eventually deletes the old logs. */
+public class LogFileManager implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final Pattern LOG_FILENAME_PATTERN =
+ Pattern.compile("^.+(?<date>\\d{4}-\\d{2}-\\d{2})(.gz)?");
+ protected final boolean compressionEnabled;
+ private final Duration timeToKeep;
- public static class LogFileCompressorModule extends LifecycleModule {
+ public static class LogFileManagerModule extends LifecycleModule {
@Override
protected void configure() {
listener().to(Lifecycle.class);
@@ -51,23 +65,21 @@ public class LogFileCompressor implements Runnable {
static class Lifecycle implements LifecycleListener {
private final WorkQueue queue;
- private final LogFileCompressor compressor;
- private final boolean enabled;
+ private final LogFileManager manager;
@Inject
- Lifecycle(WorkQueue queue, LogFileCompressor compressor, @GerritServerConfig Config config) {
+ Lifecycle(WorkQueue queue, LogFileManager manager) {
this.queue = queue;
- this.compressor = compressor;
- this.enabled = config.getBoolean("log", "compress", true);
+ this.manager = manager;
}
@Override
public void start() {
- if (!enabled) {
+ if (!manager.compressionEnabled && manager.timeToKeep.isNegative()) {
return;
}
// compress log once and then schedule compression every day at 11:00pm
- queue.getDefaultQueue().execute(compressor);
+ queue.getDefaultQueue().execute(manager);
ZoneId zone = ZoneId.systemDefault();
LocalDateTime now = LocalDateTime.now(zone);
long milliSecondsUntil11pm =
@@ -77,7 +89,7 @@ public class LogFileCompressor implements Runnable {
queue
.getDefaultQueue()
.scheduleAtFixedRate(
- compressor, milliSecondsUntil11pm, HOURS.toMillis(24), MILLISECONDS);
+ manager, milliSecondsUntil11pm, HOURS.toMillis(24), MILLISECONDS);
}
@Override
@@ -87,8 +99,21 @@ public class LogFileCompressor implements Runnable {
private final Path logs_dir;
@Inject
- LogFileCompressor(SitePaths site) {
- logs_dir = resolve(site.logs_dir);
+ LogFileManager(SitePaths site, @GerritServerConfig Config config) {
+ this.logs_dir = resolve(site.logs_dir);
+ this.compressionEnabled = config.getBoolean("log", "compress", true);
+ this.timeToKeep = getTimeToKeep(config);
+ }
+
+ private Duration getTimeToKeep(Config config) {
+ try {
+ return Duration.ofDays(
+ ConfigUtil.getTimeUnit(config, "log", null, "timeToKeep", -1, TimeUnit.DAYS));
+ } catch (IllegalArgumentException e) {
+ logger.atWarning().withCause(e).log(
+ "Illegal duration value for log deletion. Disabling log deletion.");
+ return Duration.ofDays(-1L);
+ }
}
private static Path resolve(Path p) {
@@ -101,13 +126,22 @@ public class LogFileCompressor implements Runnable {
@Override
public void run() {
+ logger.atInfo().log("Starting log file maintenance.");
try {
if (!Files.isDirectory(logs_dir)) {
return;
}
try (DirectoryStream<Path> list = Files.newDirectoryStream(logs_dir)) {
for (Path entry : list) {
- if (!isLive(entry) && !isCompressed(entry) && isLogFile(entry)) {
+ if (isLive(entry) || !isLogFile(entry)) {
+ continue;
+ }
+ if (!timeToKeep.isNegative() && isExpired(entry)) {
+ if (delete(entry)) {
+ continue;
+ }
+ }
+ if (compressionEnabled && !isCompressed(entry)) {
compress(entry);
}
}
@@ -115,8 +149,9 @@ public class LogFileCompressor implements Runnable {
logger.atSevere().withCause(e).log("Error listing logs to compress in %s", logs_dir);
}
} catch (Exception e) {
- logger.atSevere().withCause(e).log("Failed to compress log files: %s", e.getMessage());
+ logger.atSevere().withCause(e).log("Failed to process log files: %s", e.getMessage());
}
+ logger.atInfo().log("Log file maintenance has finished.");
}
private boolean isLive(Path entry) {
@@ -139,6 +174,49 @@ public class LogFileCompressor implements Runnable {
return Files.isRegularFile(entry);
}
+ @VisibleForTesting
+ boolean isExpired(Path entry) {
+ try {
+ FileTime creationTime = Files.readAttributes(entry, BasicFileAttributes.class).creationTime();
+
+ if (creationTime.toInstant().equals(Instant.EPOCH)) {
+ Optional<Instant> fileDate = getDateFromFilename(entry);
+ if (fileDate.isPresent()) {
+ return fileDate.get().isBefore(Instant.now().minus(timeToKeep));
+ }
+ return false;
+ }
+
+ return creationTime.toInstant().isBefore(Instant.now().minus(timeToKeep));
+ } catch (IOException e) {
+ logger.atSevere().withCause(e).log("Failed to get creation time of log file %s", entry);
+ }
+ return false;
+ }
+
+ @VisibleForTesting
+ Optional<Instant> getDateFromFilename(Path entry) {
+ Matcher filenameMatcher = LOG_FILENAME_PATTERN.matcher(entry.getFileName().toString());
+ if (filenameMatcher.matches()) {
+ String rotationDate = filenameMatcher.group("date");
+ if (rotationDate != null && !rotationDate.isBlank()) {
+ return Optional.of(Instant.parse(rotationDate + "T00:00:00.00Z"));
+ }
+ }
+ return Optional.empty();
+ }
+
+ private boolean delete(Path entry) {
+ try {
+ Files.deleteIfExists(entry);
+ logger.atInfo().log("Log file %s has been deleted.", entry);
+ return true;
+ } catch (IOException e) {
+ logger.atWarning().withCause(e).log("Failed to delete log file %s", entry);
+ }
+ return false;
+ }
+
private void compress(Path src) {
Path dst = src.resolveSibling(src.getFileName() + ".gz");
Path tmp = src.resolveSibling(".tmp." + src.getFileName());
@@ -166,6 +244,6 @@ public class LogFileCompressor implements Runnable {
@Override
public String toString() {
- return "Log File Compressor";
+ return "Log File Manager";
}
}
diff --git a/java/com/google/gerrit/pgm/util/SiteProgram.java b/java/com/google/gerrit/pgm/util/SiteProgram.java
index ff0b31ed72..aeaa1d6c82 100644
--- a/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -42,7 +42,6 @@ import com.google.inject.spi.Message;
import com.google.inject.util.Providers;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import org.kohsuke.args4j.Option;
@@ -53,10 +52,10 @@ public abstract class SiteProgram extends AbstractProgram {
aliases = {"-d"},
usage = "Local directory containing site data")
void setSitePath(String path) {
- sitePath = Paths.get(path).normalize();
+ sitePath = Path.of(path).normalize();
}
- private Path sitePath = Paths.get(".");
+ private Path sitePath = Path.of(".");
protected SiteProgram() {}
diff --git a/java/com/google/gerrit/pgm/util/package-info.java b/java/com/google/gerrit/pgm/util/package-info.java
new file mode 100644
index 0000000000..3bd1fd9233
--- /dev/null
+++ b/java/com/google/gerrit/pgm/util/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.pgm.util;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/prettify/BUILD b/java/com/google/gerrit/prettify/BUILD
index a5c8b77b01..5b07849603 100644
--- a/java/com/google/gerrit/prettify/BUILD
+++ b/java/com/google/gerrit/prettify/BUILD
@@ -10,5 +10,6 @@ java_library(
"//lib:jgit",
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
+ "//lib/errorprone:annotations",
],
)
diff --git a/java/com/google/gerrit/prettify/common/package-info.java b/java/com/google/gerrit/prettify/common/package-info.java
new file mode 100644
index 0000000000..5f70f74700
--- /dev/null
+++ b/java/com/google/gerrit/prettify/common/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.prettify.common;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/prettify/common/testing/BUILD b/java/com/google/gerrit/prettify/common/testing/BUILD
index 5057fdb3c3..ecc28ff6ae 100644
--- a/java/com/google/gerrit/prettify/common/testing/BUILD
+++ b/java/com/google/gerrit/prettify/common/testing/BUILD
@@ -9,6 +9,7 @@ java_library(
deps = [
"//java/com/google/gerrit/prettify:server",
"//lib:guava",
+ "//lib/errorprone:annotations",
"//lib/truth",
],
)
diff --git a/java/com/google/gerrit/prettify/common/testing/package-info.java b/java/com/google/gerrit/prettify/common/testing/package-info.java
new file mode 100644
index 0000000000..430ba69a96
--- /dev/null
+++ b/java/com/google/gerrit/prettify/common/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.prettify.common.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/proto/BUILD b/java/com/google/gerrit/proto/BUILD
index 98558c54f0..82af646838 100644
--- a/java/com/google/gerrit/proto/BUILD
+++ b/java/com/google/gerrit/proto/BUILD
@@ -6,5 +6,6 @@ java_library(
visibility = ["//visibility:public"],
deps = [
"//lib:protobuf",
+ "//lib/errorprone:annotations",
],
)
diff --git a/java/com/google/gerrit/proto/package-info.java b/java/com/google/gerrit/proto/package-info.java
new file mode 100644
index 0000000000..8c4522fcbb
--- /dev/null
+++ b/java/com/google/gerrit/proto/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.proto;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/proto/testing/BUILD b/java/com/google/gerrit/proto/testing/BUILD
index 069bb4699b..17a2eaf389 100644
--- a/java/com/google/gerrit/proto/testing/BUILD
+++ b/java/com/google/gerrit/proto/testing/BUILD
@@ -9,6 +9,7 @@ java_library(
deps = [
"//lib:guava",
"//lib/commons:lang3",
+ "//lib/errorprone:annotations",
"//lib/guice",
"//lib/truth",
],
diff --git a/java/com/google/gerrit/proto/testing/package-info.java b/java/com/google/gerrit/proto/testing/package-info.java
new file mode 100644
index 0000000000..6f1945453a
--- /dev/null
+++ b/java/com/google/gerrit/proto/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.proto.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index 000f095a5f..96d888af9b 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -105,7 +105,7 @@ java_library(
"//lib/jsoup",
"//lib/log:log4j",
"//lib/lucene:lucene-analyzers-common",
- "//lib/lucene:lucene-core-and-backward-codecs",
+ "//lib/lucene:lucene-core",
"//lib/lucene:lucene-queryparser",
"//lib/mime4j:core",
"//lib/mime4j:dom",
diff --git a/java/com/google/gerrit/server/ChangeDraftUpdate.java b/java/com/google/gerrit/server/ChangeDraftUpdate.java
index eb33fb51c0..a3b13e5ac6 100644
--- a/java/com/google/gerrit/server/ChangeDraftUpdate.java
+++ b/java/com/google/gerrit/server/ChangeDraftUpdate.java
@@ -21,6 +21,7 @@ import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.server.notedb.ChangeNotes;
import java.time.Instant;
import java.util.List;
+import java.util.Optional;
import org.eclipse.jgit.lib.PersonIdent;
/** An interface for updating draft comments. */
@@ -49,11 +50,7 @@ public interface ChangeDraftUpdate {
* Marks a comment for deletion. Called when the comment is deleted because the user published it.
*
* <p>NOTE for implementers: The actual deletion of a published draft should only happen after the
- * published comment is successfully updated. For more context, see {@link
- * com.google.gerrit.server.notedb.NoteDbUpdateManager#execute(boolean)}.
- *
- * <p>TODO(nitzan) - add generalized support for the above sync issue. The implementation should
- * support deletion of published drafts from multiple ChangeDraftUpdateFactory instances.
+ * published comment is successfully updated. Please use {@link ChangeDraftUpdateExecutor}.
*/
void markDraftCommentAsPublished(HumanComment c);
@@ -67,4 +64,26 @@ public interface ChangeDraftUpdate {
* comments storage and the drafts one.
*/
void addAllDraftCommentsForDeletion(List<Comment> comments);
+
+ /** Whether all updates in this updater can run asynchronously. */
+ boolean canRunAsync();
+
+ /**
+ * A unique identifier for the draft, used by the storage system. For example, NoteDB's ref name.
+ */
+ String getStorageKey();
+
+ /**
+ * Converts this update to the given subtype if possible. Returns {@link Optional#empty()}
+ * otherwise.
+ */
+ default <UpdateT extends ChangeDraftUpdate> Optional<UpdateT> toOptionalChangeDraftUpdateSubtype(
+ Class<UpdateT> subtype) {
+ if (this.getClass().isAssignableFrom(subtype)) {
+ @SuppressWarnings("unchecked")
+ UpdateT update = (UpdateT) this;
+ return Optional.of(update);
+ }
+ return Optional.empty();
+ }
}
diff --git a/java/com/google/gerrit/server/ChangeDraftUpdateExecutor.java b/java/com/google/gerrit/server/ChangeDraftUpdateExecutor.java
new file mode 100644
index 0000000000..8c932fc2b1
--- /dev/null
+++ b/java/com/google/gerrit/server/ChangeDraftUpdateExecutor.java
@@ -0,0 +1,129 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.server.update.BatchUpdateListener;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Optional;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.transport.PushCertificate;
+
+/**
+ * An interface for executing updates of multiple {@link ChangeDraftUpdate} instances.
+ *
+ * <p>Expected usage flow:
+ *
+ * <ol>
+ * <li>Inject an instance of {@link AbstractFactory}.
+ * <li>Create an instance of this interface using the factory.
+ * <li>Call ({@link #queueAllDraftUpdates} or {@link #queueDeletionForChangeDrafts} for all
+ * expected updates. The changes are marked to be executed either synchronously or
+ * asynchronously, based on {@link #canRunAsync}.
+ * <li>Call both {@link #executeAllSyncUpdates} and {@link #executeAllAsyncUpdates} methods.
+ * Running these methods with no pending updates is a no-op.
+ * </ol>
+ */
+public interface ChangeDraftUpdateExecutor {
+ interface AbstractFactory {
+ // Guice cannot bind either:
+ // - A parameterized entity.
+ // - A factory creating an interface (rather than a class).
+ // To overcome this - we declare the create method in this non-parameterized interface, then
+ // extend it with a factory returning an actual class.
+ ChangeDraftUpdateExecutor create(CurrentUser currentUser);
+ }
+
+ interface Factory<T extends ChangeDraftUpdateExecutor> extends AbstractFactory {
+ @Override
+ T create(CurrentUser currentUser);
+ }
+
+ /**
+ * Queues all provided updates for later execution.
+ *
+ * <p>The updates are queued to either run synchronously just after change repositories updates,
+ * or to run asynchronously afterwards, based on {@link #canRunAsync}.
+ */
+ void queueAllDraftUpdates(ListMultimap<String, ChangeDraftUpdate> updates) throws IOException;
+
+ /**
+ * Extracts all drafts (of all authors) for the given change and queue their deletion.
+ *
+ * <p>See {@link #canRunAsync} for whether the deletions are scheduled as synchronous or
+ * asynchronous.
+ */
+ void queueDeletionForChangeDrafts(Change.Id id) throws IOException;
+
+ /**
+ * Execute all previously queued sync updates.
+ *
+ * <p>NOTE that {@link BatchUpdateListener#beforeUpdateRefs} events are not fired by this method.
+ * post-update events can be fired by the caller only for implementations that return a valid
+ * {@link BatchRefUpdate}.
+ *
+ * @param dryRun whether this is a dry run - i.e. no updates should be made
+ * @param refLogIdent user to log as the update creator
+ * @param refLogMessage message to put in the updates log
+ * @return the executed update, if supported by the implementing class
+ * @throws IOException in case of an update failure.
+ */
+ Optional<BatchRefUpdate> executeAllSyncUpdates(
+ boolean dryRun, @Nullable PersonIdent refLogIdent, @Nullable String refLogMessage)
+ throws IOException;
+
+ /**
+ * Execute all previously queued async updates.
+ *
+ * @param refLogIdent user to log as the update creator
+ * @param refLogMessage message to put in the updates log
+ * @param pushCert to use for the update
+ */
+ void executeAllAsyncUpdates(
+ @Nullable PersonIdent refLogIdent,
+ @Nullable String refLogMessage,
+ @Nullable PushCertificate pushCert);
+
+ /** Returns whether any updates are queued. */
+ boolean isEmpty();
+
+ /** Returns the given updates that match the provided type. */
+ default <UpdateT extends ChangeDraftUpdate> ListMultimap<String, UpdateT> filterTypedUpdates(
+ ListMultimap<String, ChangeDraftUpdate> updates, Class<UpdateT> updateType) {
+ ListMultimap<String, UpdateT> res = MultimapBuilder.hashKeys().arrayListValues().build();
+ for (String key : updates.keySet()) {
+ res.putAll(
+ key,
+ updates.get(key).stream()
+ .map(u -> u.toOptionalChangeDraftUpdateSubtype(updateType))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(toImmutableList()));
+ }
+ return res;
+ }
+
+ /** Returns whether all provided updates can run asynchronously. */
+ default boolean canRunAsync(Collection<? extends ChangeDraftUpdate> updates) {
+ return updates.stream().allMatch(u -> u.canRunAsync());
+ }
+}
diff --git a/java/com/google/gerrit/server/ChangeMessagesUtil.java b/java/com/google/gerrit/server/ChangeMessagesUtil.java
index 400da582b3..7e8271be4f 100644
--- a/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -16,6 +16,7 @@ package com.google.gerrit.server;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.ChangeMessage;
@@ -83,6 +84,7 @@ public class ChangeMessagesUtil {
* @return message built from {@code messageTemplate}. Templates are replaced, so it might contain
* user identifiable information.
*/
+ @CanIgnoreReturnValue
public String setChangeMessage(
ChangeUpdate update, String messageTemplate, @Nullable String tag) {
update.setChangeMessage(messageTemplate);
@@ -91,6 +93,7 @@ public class ChangeMessagesUtil {
}
/** See {@link #setChangeMessage(ChangeUpdate, String, String)}. */
+ @CanIgnoreReturnValue
public String setChangeMessage(ChangeContext ctx, String messageTemplate, @Nullable String tag) {
return setChangeMessage(
ctx.getUpdate(ctx.getChange().currentPatchSetId()), messageTemplate, tag);
diff --git a/java/com/google/gerrit/server/ChangeUtil.java b/java/com/google/gerrit/server/ChangeUtil.java
index dd86f8853c..5b78658415 100644
--- a/java/com/google/gerrit/server/ChangeUtil.java
+++ b/java/com/google/gerrit/server/ChangeUtil.java
@@ -146,7 +146,8 @@ public class ChangeUtil {
Constants.encode("tree " + ObjectId.zeroId().name() + "\n\n" + newCommitMessage));
// Check that the commit message without footers is not empty
- CommitMessageUtil.checkAndSanitizeCommitMessage(revCommit.getShortMessage());
+ @SuppressWarnings("unused")
+ var unused = CommitMessageUtil.checkAndSanitizeCommitMessage(revCommit.getShortMessage());
List<String> changeIdFooters = getChangeIdsFromFooter(revCommit);
if (requireChangeId && changeIdFooters.isEmpty()) {
diff --git a/java/com/google/gerrit/server/CmdLineParserModule.java b/java/com/google/gerrit/server/CmdLineParserModule.java
index be6b4cd828..c523a29a1d 100644
--- a/java/com/google/gerrit/server/CmdLineParserModule.java
+++ b/java/com/google/gerrit/server/CmdLineParserModule.java
@@ -18,12 +18,14 @@ import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.common.ListTagSortOption;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.server.args4j.AccountGroupIdHandler;
import com.google.gerrit.server.args4j.AccountGroupUUIDHandler;
import com.google.gerrit.server.args4j.AccountIdHandler;
import com.google.gerrit.server.args4j.ChangeIdHandler;
import com.google.gerrit.server.args4j.InstantHandler;
+import com.google.gerrit.server.args4j.ListTagSortOptionHandler;
import com.google.gerrit.server.args4j.ObjectIdHandler;
import com.google.gerrit.server.args4j.PatchSetIdHandler;
import com.google.gerrit.server.args4j.ProjectHandler;
@@ -54,6 +56,7 @@ public class CmdLineParserModule extends FactoryModule {
registerOptionHandler(PatchSet.Id.class, PatchSetIdHandler.class);
registerOptionHandler(ProjectState.class, ProjectHandler.class);
registerOptionHandler(SocketAddress.class, SocketAddressHandler.class);
+ registerOptionHandler(ListTagSortOption.class, ListTagSortOptionHandler.class);
}
private <T> void registerOptionHandler(Class<T> type, Class<? extends OptionHandler<T>> impl) {
diff --git a/java/com/google/gerrit/server/CommentVerifier.java b/java/com/google/gerrit/server/CommentVerifier.java
new file mode 100644
index 0000000000..b6e4321c5f
--- /dev/null
+++ b/java/com/google/gerrit/server/CommentVerifier.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Comment;
+import org.eclipse.jgit.lib.PersonIdent;
+
+/** Verifier for {@link Comment} objects */
+public final class CommentVerifier {
+ public static void verify(
+ Comment c, Account.Id accountId, Account.Id realAccountId, PersonIdent authorIdent) {
+ checkArgument(c.getCommitId() != null, "commit ID required for comment: %s", c);
+ checkAccountId(accountId, authorIdent);
+ checkArgument(
+ c.author.getId().equals(accountId),
+ "The author for the following comment does not match the author of this CommentVerifier (%s): %s",
+ accountId,
+ c);
+ checkArgument(
+ c.getRealAuthor().getId().equals(realAccountId),
+ "The real author for the following comment does not match the real"
+ + " author of this CommentVerifier (%s): %s",
+ realAccountId,
+ c);
+ }
+
+ @CanIgnoreReturnValue
+ private static Account.Id checkAccountId(Account.Id accountId, PersonIdent authorIdent) {
+ checkState(
+ accountId != null,
+ "author identity for CommentVerifier is not from an IdentifiedUser: %s",
+ authorIdent.toExternalString());
+ return accountId;
+ }
+}
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index bddd86af8c..30b87476e4 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -21,23 +21,24 @@ import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toList;
import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.FixReplacement;
+import com.google.gerrit.entities.FixSuggestion;
import com.google.gerrit.entities.HumanComment;
-import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.RobotComment;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.CommentInfo;
-import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.extensions.common.FixReplacementInfo;
+import com.google.gerrit.extensions.common.FixSuggestionInfo;
import com.google.gerrit.server.config.GerritServerId;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -46,7 +47,6 @@ import com.google.gerrit.server.patch.DiffNotAvailableException;
import com.google.gerrit.server.patch.DiffOperations;
import com.google.gerrit.server.patch.DiffOptions;
import com.google.gerrit.server.patch.filediff.FileDiffOutput;
-import com.google.gerrit.server.query.change.ChangeNumberVirtualIdAlgorithm;
import com.google.gerrit.server.update.ChangeContext;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -59,7 +59,6 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -116,22 +115,16 @@ public class CommentsUtil {
private final DiffOperations diffOperations;
private final GitRepositoryManager repoManager;
- private final AllUsersName allUsers;
private final String serverId;
- private final ChangeNumberVirtualIdAlgorithm virtualIdAlgorithm;
@Inject
CommentsUtil(
DiffOperations diffOperations,
GitRepositoryManager repoManager,
- AllUsersName allUsers,
- @GerritServerId String serverId,
- @Nullable ChangeNumberVirtualIdAlgorithm virtualIdAlgorithm) {
+ @GerritServerId String serverId) {
this.diffOperations = diffOperations;
this.repoManager = repoManager;
- this.allUsers = allUsers;
this.serverId = serverId;
- this.virtualIdAlgorithm = virtualIdAlgorithm;
}
public HumanComment newHumanComment(
@@ -143,7 +136,8 @@ public class CommentsUtil {
short side,
String message,
@Nullable Boolean unresolved,
- @Nullable String parentUuid) {
+ @Nullable String parentUuid,
+ @Nullable List<FixSuggestion> fixSuggestions) {
if (unresolved == null) {
if (parentUuid == null) {
// Default to false if comment is not descended from another.
@@ -168,6 +162,7 @@ public class CommentsUtil {
serverId,
unresolved);
c.parentUuid = parentUuid;
+ c.fixSuggestions = fixSuggestions;
currentUser.updateRealAccountId(c::setRealAuthor);
return c;
}
@@ -220,13 +215,8 @@ public class CommentsUtil {
return robotCommentsByChange(notes).stream().filter(c -> c.key.uuid.equals(uuid)).findFirst();
}
- public List<HumanComment> publishedByChangeFile(ChangeNotes notes, String file) {
- return commentsOnFile(notes.load().getHumanComments().values(), file);
- }
-
public List<HumanComment> publishedByPatchSet(ChangeNotes notes, PatchSet.Id psId) {
- return removeCommentsOnAncestorOfCommitMessage(
- commentsOnPatchSet(notes.load().getHumanComments().values(), psId));
+ return commentsOnPatchSet(notes.load().getHumanComments().values(), psId);
}
public List<RobotComment> robotCommentsByPatchSet(ChangeNotes notes, PatchSet.Id psId) {
@@ -309,29 +299,6 @@ public class CommentsUtil {
Optional.ofNullable(cm.getAuthor()).map(a -> a.get()),
Optional.ofNullable(comment.author).map(a -> a._accountId));
}
- /**
- * For the commit message the A side in a diff view is always empty when a comparison against an
- * ancestor is done, so there can't be any comments on this ancestor. However earlier we showed
- * the auto-merge commit message on side A when for a merge commit a comparison against the
- * auto-merge was done. From that time there may still be comments on the auto-merge commit
- * message and those we want to filter out.
- */
- private List<HumanComment> removeCommentsOnAncestorOfCommitMessage(List<HumanComment> list) {
- return list.stream()
- .filter(c -> c.side != 0 || !Patch.COMMIT_MSG.equals(c.key.filename))
- .collect(toList());
- }
-
- public List<HumanComment> draftByPatchSetAuthor(
- PatchSet.Id psId, Account.Id author, ChangeNotes notes) {
- return commentsOnPatchSet(notes.load().getDraftComments(author, getVirtualId(notes)), psId);
- }
-
- public List<HumanComment> draftByChangeAuthor(ChangeNotes notes, Account.Id author) {
- List<HumanComment> comments = new ArrayList<>();
- comments.addAll(notes.getDraftComments(author, getVirtualId(notes)));
- return sort(comments);
- }
public void putHumanComments(
ChangeUpdate update, Comment.Status status, Iterable<HumanComment> comments) {
@@ -357,18 +324,6 @@ public class CommentsUtil {
update.deleteCommentByRewritingHistory(commentKey.uuid, newMessage);
}
- private static List<HumanComment> commentsOnFile(
- Collection<HumanComment> allComments, String file) {
- List<HumanComment> result = new ArrayList<>(allComments.size());
- for (HumanComment c : allComments) {
- String currentFilename = c.key.filename;
- if (currentFilename.equals(file)) {
- result.add(c);
- }
- }
- return sort(result);
- }
-
private static <T extends Comment> List<T> commentsOnPatchSet(
Collection<T> allComments, PatchSet.Id psId) {
List<T> result = new ArrayList<>(allComments.size());
@@ -447,36 +402,39 @@ public class CommentsUtil {
}
}
- /**
- * Get NoteDb draft refs for a change.
- *
- * <p>This is just a simple ref scan, so the results may potentially include refs for zombie draft
- * comments. A zombie draft is one which has been published but the write to delete the draft ref
- * from All-Users failed.
- *
- * @param changeId change ID.
- * @return raw refs from All-Users repo.
- */
- public Collection<Ref> getDraftRefs(Change.Id changeId) {
- try (Repository repo = repoManager.openRepository(allUsers)) {
- return getDraftRefs(repo, changeId);
- } catch (IOException e) {
- throw new StorageException(e);
+ public static <T extends Comment> List<T> sort(List<T> comments) {
+ comments.sort(COMMENT_ORDER);
+ return comments;
+ }
+
+ @Nullable
+ public static ImmutableList<FixSuggestion> createFixSuggestionsFromInput(
+ List<FixSuggestionInfo> fixSuggestionInfos) {
+ if (fixSuggestionInfos == null) {
+ return null;
+ }
+
+ ImmutableList.Builder<FixSuggestion> fixSuggestions =
+ ImmutableList.builderWithExpectedSize(fixSuggestionInfos.size());
+ for (FixSuggestionInfo fixSuggestionInfo : fixSuggestionInfos) {
+ fixSuggestions.add(createFixSuggestionFromInput(fixSuggestionInfo));
}
+ return fixSuggestions.build();
}
- private Collection<Ref> getDraftRefs(Repository repo, Change.Id virtualId) throws IOException {
- return repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftCommentsPrefix(virtualId));
+ public static FixSuggestion createFixSuggestionFromInput(FixSuggestionInfo fixSuggestionInfo) {
+ List<FixReplacement> fixReplacements = toFixReplacements(fixSuggestionInfo.replacements);
+ String fixId = ChangeUtil.messageUuid();
+ return new FixSuggestion(fixId, fixSuggestionInfo.description, fixReplacements);
}
- public static <T extends Comment> List<T> sort(List<T> comments) {
- comments.sort(COMMENT_ORDER);
- return comments;
+ public static List<FixReplacement> toFixReplacements(
+ List<FixReplacementInfo> fixReplacementInfos) {
+ return fixReplacementInfos.stream().map(CommentsUtil::toFixReplacement).collect(toList());
}
- private Change.Id getVirtualId(ChangeNotes notes) {
- return virtualIdAlgorithm == null
- ? notes.getChangeId()
- : virtualIdAlgorithm.apply(notes.getServerId(), notes.getChangeId());
+ public static FixReplacement toFixReplacement(FixReplacementInfo fixReplacementInfo) {
+ Comment.Range range = new Comment.Range(fixReplacementInfo.range);
+ return new FixReplacement(fixReplacementInfo.path, range, fixReplacementInfo.replacement);
}
}
diff --git a/java/com/google/gerrit/server/DeleteZombieComments.java b/java/com/google/gerrit/server/DeleteZombieComments.java
index 4532b04b8f..cf15c950a1 100644
--- a/java/com/google/gerrit/server/DeleteZombieComments.java
+++ b/java/com/google/gerrit/server/DeleteZombieComments.java
@@ -209,7 +209,7 @@ public abstract class DeleteZombieComments<KeyT> implements AutoCloseable {
draftCommentsReader.getDraftsByChangeAndDraftAuthor(changeId, accountId);
ChangeNotes notes = getChangeNotes(changeId);
List<HumanComment> published = commentsUtil.publishedHumanCommentsByChange(notes);
- Set<String> publishedIds = toUuid(published);
+ ImmutableSet<String> publishedIds = toUuid(published);
ImmutableList<HumanComment> zombieDraftsForChangeAndAuthor =
drafts.stream()
.filter(draft -> publishedIds.contains(draft.key.uuid))
@@ -284,7 +284,7 @@ public abstract class DeleteZombieComments<KeyT> implements AutoCloseable {
}
/** Map the list of input comments to their UUIDs. */
- private Set<String> toUuid(List<HumanComment> in) {
+ private ImmutableSet<String> toUuid(List<HumanComment> in) {
return in.stream().map(c -> c.key.uuid).collect(toImmutableSet());
}
diff --git a/java/com/google/gerrit/server/PublishCommentUtil.java b/java/com/google/gerrit/server/PublishCommentUtil.java
index de5f023e5d..d6722cce99 100644
--- a/java/com/google/gerrit/server/PublishCommentUtil.java
+++ b/java/com/google/gerrit/server/PublishCommentUtil.java
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import static java.util.stream.Collectors.toSet;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.HumanComment;
@@ -35,7 +36,6 @@ import com.google.inject.Singleton;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.HashSet;
-import java.util.Map;
import java.util.Set;
@Singleton
@@ -62,7 +62,7 @@ public class PublishCommentUtil {
return;
}
- Map<PatchSet.Id, PatchSet> patchSets =
+ ImmutableMap<PatchSet.Id, PatchSet> patchSets =
psUtil.getAsMap(notes, draftComments.stream().map(d -> psId(notes, d)).collect(toSet()));
Set<HumanComment> commentsToPublish = new HashSet<>();
for (HumanComment draftComment : draftComments) {
diff --git a/java/com/google/gerrit/server/ReviewerStatusUpdate.java b/java/com/google/gerrit/server/ReviewerStatusUpdate.java
index 1e0aa43cbf..5486359eab 100644
--- a/java/com/google/gerrit/server/ReviewerStatusUpdate.java
+++ b/java/com/google/gerrit/server/ReviewerStatusUpdate.java
@@ -16,22 +16,35 @@ package com.google.gerrit.server;
import com.google.auto.value.AutoValue;
import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import java.time.Instant;
+import java.util.Optional;
/** Change to a reviewer's status. */
@AutoValue
public abstract class ReviewerStatusUpdate {
- public static ReviewerStatusUpdate create(
+ public static ReviewerStatusUpdate createForReviewer(
Instant ts, Account.Id updatedBy, Account.Id reviewer, ReviewerStateInternal state) {
- return new AutoValue_ReviewerStatusUpdate(ts, updatedBy, reviewer, state);
+ return new AutoValue_ReviewerStatusUpdate(
+ ts, updatedBy, Optional.of(reviewer), Optional.empty(), state);
+ }
+
+ public static ReviewerStatusUpdate createForReviewerByEmail(
+ Instant ts, Account.Id updatedBy, Address reviewerByEmail, ReviewerStateInternal state) {
+ return new AutoValue_ReviewerStatusUpdate(
+ ts, updatedBy, Optional.empty(), Optional.of(reviewerByEmail), state);
}
public abstract Instant date();
public abstract Account.Id updatedBy();
- public abstract Account.Id reviewer();
+ /** Not set if a reviewer for which no Gerrit account exists is added by email. */
+ public abstract Optional<Account.Id> reviewer();
+
+ /** Only set for reviewers that have no Gerrit account and that have been added by email. */
+ public abstract Optional<Address> reviewerByEmail();
public abstract ReviewerStateInternal state();
}
diff --git a/java/com/google/gerrit/server/Sequence.java b/java/com/google/gerrit/server/Sequence.java
index 844b58377a..541ce251df 100644
--- a/java/com/google/gerrit/server/Sequence.java
+++ b/java/com/google/gerrit/server/Sequence.java
@@ -76,7 +76,4 @@ public interface Sequence {
* #current()}.
*/
void storeNew(int value);
-
- /** Returns the batch size that was used to initialize the sequence. */
- int getBatchSize();
}
diff --git a/java/com/google/gerrit/server/Sequences.java b/java/com/google/gerrit/server/Sequences.java
index 431a1b2762..ad7af45f29 100644
--- a/java/com/google/gerrit/server/Sequences.java
+++ b/java/com/google/gerrit/server/Sequences.java
@@ -93,18 +93,6 @@ public class Sequences {
}
}
- public int changeBatchSize() {
- return changeSeq.getBatchSize();
- }
-
- public int groupBatchSize() {
- return groupSeq.getBatchSize();
- }
-
- public int accountBatchSize() {
- return accountSeq.getBatchSize();
- }
-
public int currentChangeId() {
return changeSeq.current();
}
@@ -117,18 +105,6 @@ public class Sequences {
return groupSeq.current();
}
- public int lastChangeId() {
- return changeSeq.last();
- }
-
- public int lastGroupId() {
- return groupSeq.last();
- }
-
- public int lastAccountId() {
- return accountSeq.last();
- }
-
public void setChangeIdValue(int value) {
changeSeq.storeNew(value);
}
diff --git a/java/com/google/gerrit/server/StarredChangesReader.java b/java/com/google/gerrit/server/StarredChangesReader.java
index ddf0cd34b1..0386cb3b87 100644
--- a/java/com/google/gerrit/server/StarredChangesReader.java
+++ b/java/com/google/gerrit/server/StarredChangesReader.java
@@ -14,27 +14,60 @@
package com.google.gerrit.server;
-import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import java.util.List;
import java.util.Set;
-import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
+/** Interface for reading information about starred changes. */
public interface StarredChangesReader {
+
+ /**
+ * Checks if a specific change is starred by a given user.
+ *
+ * @param accountId the {@code Account.Id}.
+ * @param changeId the {@code Change.Id}.
+ * @return {@code true} if the change is starred by the user, {@code false} otherwise.
+ */
boolean isStarred(Account.Id accountId, Change.Id changeId);
/**
- * Returns a subset of change IDs among the input {@code changeIds} list that are starred by the
- * {@code caller} user.
+ * Returns a subset of {@code Change.Id}s among the input {@code changeIds} list that are starred
+ * by the {@code caller} user.
+ *
+ * @param allUsersRepo 'All-Users' repository.
+ * @param changeIds the list of {@code Change.Id}s to check.
+ * @param caller the {@code Account.Id} to check starred changes by a user.
+ * @return a set of {@code Change.Id}s that are starred by the specified user.
*/
Set<Change.Id> areStarred(Repository allUsersRepo, List<Change.Id> changeIds, Account.Id caller);
- ImmutableMap<Account.Id, Ref> byChange(Change.Id changeId);
+ /**
+ * Retrieves a list of {@code Account.Id} which starred a {@code Change.Id}.
+ *
+ * @param changeId the {@code Change.Id}.
+ * @return an immutable list of {@code Account.Id}s for the specified change.
+ */
+ ImmutableList<Account.Id> byChange(Change.Id changeId);
+ /**
+ * Retrieves a set of {@code changeIds} starred by {@code Account.Id}.
+ *
+ * @param accountId the {@code Account.Id}.
+ * @return an immutable set of {@code Change.Id}s associated with the specified user account.
+ */
ImmutableSet<Change.Id> byAccountId(Account.Id accountId);
+ /**
+ * Retrieves a set of {@code Change.Id}s associated with the specified user account, optionally
+ * skipping invalid changes.
+ *
+ * @param accountId the {@code Account.Id}.
+ * @param skipInvalidChanges {@code true} to skip invalid changes, {@code false} otherwise.
+ * @return an immutable set of {@code Change.Id}s associated with the specified user account.
+ */
ImmutableSet<Change.Id> byAccountId(Account.Id accountId, boolean skipInvalidChanges);
}
diff --git a/java/com/google/gerrit/server/StarredChangesWriter.java b/java/com/google/gerrit/server/StarredChangesWriter.java
index 6c14cc9189..eafe7f730d 100644
--- a/java/com/google/gerrit/server/StarredChangesWriter.java
+++ b/java/com/google/gerrit/server/StarredChangesWriter.java
@@ -18,9 +18,22 @@ import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import java.io.IOException;
+/** Interface for writing information about starred changes. */
public interface StarredChangesWriter {
+ /**
+ * Star the given change for a single {@code Account.Id}.
+ *
+ * @param changeId the {@code Change.Id}.
+ * @param accountId the {@code Account.Id}.
+ */
void star(Account.Id accountId, Change.Id changeId);
+ /**
+ * Unstar the given change for a single {@code Account.Id}.
+ *
+ * @param changeId the {@code Change.Id}.
+ * @param accountId the {@code Account.Id}.
+ */
void unstar(Account.Id accountId, Change.Id changeId);
/**
diff --git a/java/com/google/gerrit/server/account/AbstractRealm.java b/java/com/google/gerrit/server/account/AbstractRealm.java
index 380001d833..d2296008c5 100644
--- a/java/com/google/gerrit/server/account/AbstractRealm.java
+++ b/java/com/google/gerrit/server/account/AbstractRealm.java
@@ -15,13 +15,13 @@
package com.google.gerrit.server.account;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.gerrit.extensions.client.AccountFieldName;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.mail.send.EmailSender;
import com.google.inject.Inject;
-import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@@ -63,7 +63,7 @@ public abstract class AbstractRealm implements Realm {
@Override
public Set<String> getEmailAddresses(IdentifiedUser user) {
- Collection<ExternalId> ids = user.state().externalIds();
+ ImmutableSet<ExternalId> ids = user.state().externalIds();
Set<String> emails = Sets.newHashSetWithExpectedSize(ids.size());
for (ExternalId ext : ids) {
if (!Strings.isNullOrEmpty(ext.email())) {
diff --git a/java/com/google/gerrit/server/account/AccountCacheImpl.java b/java/com/google/gerrit/server/account/AccountCacheImpl.java
index d7d4938c97..9a535efb33 100644
--- a/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -16,6 +16,7 @@ package com.google.gerrit.server.account;
import static com.google.gerrit.server.account.AccountCacheImpl.AccountCacheModule.ACCOUNT_CACHE_MODULE;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+import static com.google.inject.Scopes.SINGLETON;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
@@ -25,6 +26,7 @@ import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ModuleImpl;
import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
import com.google.gerrit.server.account.externalids.ExternalIds;
@@ -33,10 +35,12 @@ import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.CachedPreferences;
import com.google.gerrit.server.config.DefaultPreferencesCache;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.logging.CallerFinder;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
@@ -51,8 +55,17 @@ import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
-/** Caches important (but small) account state to avoid database hits. */
-@Singleton
+/**
+ * Caches important (but small) account state to avoid database hits.
+ *
+ * <p>This class should be bounded as a Singleton. However, due to internal limitations in Google,
+ * it cannot be marked as a singleton. The common installation pattern should therefore be:
+ *
+ * <pre>{@code
+ * install(AccountCacheImpl.module());
+ * install(AccountCacheImpl.bindingModule());
+ * }</pre>
+ */
public class AccountCacheImpl implements AccountCache {
@ModuleImpl(name = ACCOUNT_CACHE_MODULE)
public static class AccountCacheModule extends CacheModule {
@@ -65,9 +78,14 @@ public class AccountCacheImpl implements AccountCache {
.keySerializer(CachedAccountDetails.Key.Serializer.INSTANCE)
.valueSerializer(CachedAccountDetails.Serializer.INSTANCE)
.loader(Loader.class);
+ }
+ }
- bind(AccountCacheImpl.class);
- bind(AccountCache.class).to(AccountCacheImpl.class);
+ public static class AccountCacheBindingModule extends AbstractModule {
+ @Override
+ protected void configure() {
+ bind(AccountCacheImpl.class).in(SINGLETON);
+ bind(AccountCache.class).to(AccountCacheImpl.class).in(SINGLETON);
}
}
@@ -79,6 +97,10 @@ public class AccountCacheImpl implements AccountCache {
return new AccountCacheModule();
}
+ public static Module bindingModule() {
+ return new AccountCacheBindingModule();
+ }
+
private final ExternalIds externalIds;
private final LoadingCache<CachedAccountDetails.Key, CachedAccountDetails> accountDetailsCache;
private final GitRepositoryManager repoManager;
@@ -127,7 +149,10 @@ public class AccountCacheImpl implements AccountCache {
@Override
public Map<Account.Id, AccountState> get(Set<Account.Id> accountIds) {
- try {
+ try (TraceTimer ignored =
+ TraceContext.newTimer(
+ "Loading accounts",
+ Metadata.builder().caller(findCaller()).resourceCount(accountIds.size()).build())) {
try (Repository allUsers = repoManager.openRepository(allUsersName)) {
Set<CachedAccountDetails.Key> keys =
Sets.newLinkedHashSetWithExpectedSize(accountIds.size());
@@ -172,6 +197,17 @@ public class AccountCacheImpl implements AccountCache {
return AccountState.forAccount(account.build());
}
+ private String findCaller() {
+ return CallerFinder.builder()
+ .addTarget(AccountLoader.class)
+ .addTarget(InternalAccountDirectory.class)
+ .addTarget(AccountResolver.class)
+ .addTarget(IdentifiedUser.class)
+ .addTarget(AccountCacheImpl.class)
+ .build()
+ .findCaller();
+ }
+
@Singleton
static class Loader extends CacheLoader<CachedAccountDetails.Key, CachedAccountDetails> {
private final GitRepositoryManager repoManager;
diff --git a/java/com/google/gerrit/server/account/AccountConfig.java b/java/com/google/gerrit/server/account/AccountConfig.java
index 2b0ba3f41d..8f611f0a06 100644
--- a/java/com/google/gerrit/server/account/AccountConfig.java
+++ b/java/com/google/gerrit/server/account/AccountConfig.java
@@ -21,6 +21,7 @@ import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.NotifyConfig.NotifyType;
import com.google.gerrit.entities.RefNames;
@@ -185,6 +186,7 @@ public class AccountConfig extends VersionedMetaData implements ValidationError.
return loadedAccountProperties.map(AccountProperties::getAccount).get();
}
+ @CanIgnoreReturnValue
public AccountConfig setAccountDelta(AccountDelta accountDelta) {
this.accountDelta = Optional.of(accountDelta);
return this;
@@ -240,6 +242,7 @@ public class AccountConfig extends VersionedMetaData implements ValidationError.
}
@Override
+ @CanIgnoreReturnValue
public RevCommit commit(MetaDataUpdate update) throws IOException {
RevCommit c = super.commit(update);
loadedAccountProperties.get().setMetaId(c);
diff --git a/java/com/google/gerrit/server/account/AccountLimits.java b/java/com/google/gerrit/server/account/AccountLimits.java
index d97563aa6a..a037046b41 100644
--- a/java/com/google/gerrit/server/account/AccountLimits.java
+++ b/java/com/google/gerrit/server/account/AccountLimits.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.account;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.entities.PermissionRange;
@@ -133,7 +134,7 @@ public class AccountLimits {
}
private List<PermissionRule> getRules(String permissionName) {
- List<PermissionRule> rules = capabilities.getPermission(permissionName);
+ ImmutableList<PermissionRule> rules = capabilities.getPermission(permissionName);
GroupMembership groups = user.getEffectiveGroups();
List<PermissionRule> mine = new ArrayList<>(rules.size());
diff --git a/java/com/google/gerrit/server/account/AccountManager.java b/java/com/google/gerrit/server/account/AccountManager.java
index 18260a41f0..61012d7c5d 100644
--- a/java/com/google/gerrit/server/account/AccountManager.java
+++ b/java/com/google/gerrit/server/account/AccountManager.java
@@ -26,6 +26,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.entities.AccessSection;
import com.google.gerrit.entities.Account;
@@ -58,7 +59,6 @@ import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
-import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -139,6 +139,7 @@ public class AccountManager {
* cannot be located, is unable to be activated or deactivated, or is inactive, or cannot be
* added to the admin group (only for the first account).
*/
+ @CanIgnoreReturnValue
public AuthResult authenticate(AuthRequest who) throws AccountException, IOException {
try {
who = realm.authenticate(who);
@@ -364,7 +365,8 @@ public class AccountManager {
+ e.getDuplicateKey().get()
+ "\" to account "
+ newId
- + "; external ID already in use.");
+ + "; external ID already in use.",
+ e);
} finally {
// If adding the account failed, it may be that it actually was the
// first account. So we reset the 'check for first account'-guard, as
@@ -419,7 +421,7 @@ public class AccountManager {
return;
}
- Set<ExternalId> existingExtIdsWithEmail = externalIds.byEmail(email);
+ ImmutableSet<ExternalId> existingExtIdsWithEmail = externalIds.byEmail(email);
if (existingExtIdsWithEmail.isEmpty()) {
return;
}
@@ -463,6 +465,7 @@ public class AccountManager {
* @throws AccountException the identity belongs to a different account, or it cannot be linked at
* this time.
*/
+ @CanIgnoreReturnValue
public AuthResult link(Account.Id to, AuthRequest who)
throws AccountException, IOException, ConfigInvalidException {
Optional<ExternalId> optionalExtId = externalIds.get(who.getExternalIdKey());
@@ -504,6 +507,7 @@ public class AccountManager {
* @throws AccountException the identity belongs to a different account, or it cannot be linked at
* this time.
*/
+ @CanIgnoreReturnValue
public AuthResult updateLink(Account.Id to, AuthRequest who)
throws AccountException, IOException, ConfigInvalidException {
Optional<ExternalId> optionalExtId = externalIds.get(who.getExternalIdKey());
@@ -518,7 +522,7 @@ public class AccountManager {
"Update External IDs on Update Link",
to,
(a, u) -> {
- Set<ExternalId> filteredExtIdsByScheme =
+ ImmutableSet<ExternalId> filteredExtIdsByScheme =
a.externalIds().stream()
.filter(e -> e.key().isScheme(who.getExternalIdKey().scheme()))
.collect(toImmutableSet());
diff --git a/java/com/google/gerrit/server/account/AccountProperties.java b/java/com/google/gerrit/server/account/AccountProperties.java
index 5f56aa3485..9cc20d42ab 100644
--- a/java/com/google/gerrit/server/account/AccountProperties.java
+++ b/java/com/google/gerrit/server/account/AccountProperties.java
@@ -97,6 +97,7 @@ public class AccountProperties {
accountBuilder.setStatus(get(accountConfig, KEY_STATUS));
accountBuilder.setMetaId(metaId != null ? metaId.name() : null);
+ accountBuilder.setUniqueTag(accountBuilder.metaId());
account = accountBuilder.build();
}
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java
index 65e9d9d3d4..7aa25b6187 100644
--- a/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/java/com/google/gerrit/server/account/AccountResolver.java
@@ -396,7 +396,7 @@ public class AccountResolver {
// TODO(dborowitz): This would probably work as a Searcher<Address>
int lt = nameOrEmail.indexOf('<');
int gt = nameOrEmail.indexOf('>');
- Set<Account.Id> ids = emails.getAccountFor(nameOrEmail.substring(lt + 1, gt));
+ ImmutableSet<Account.Id> ids = emails.getAccountFor(nameOrEmail.substring(lt + 1, gt));
ImmutableList<AccountState> allMatches = toAccountStates(ids).collect(toImmutableList());
if (allMatches.isEmpty() || allMatches.size() == 1) {
return allMatches.stream();
diff --git a/java/com/google/gerrit/server/account/AccountState.java b/java/com/google/gerrit/server/account/AccountState.java
index 8f2d66d415..feea4ba700 100644
--- a/java/com/google/gerrit/server/account/AccountState.java
+++ b/java/com/google/gerrit/server/account/AccountState.java
@@ -30,6 +30,7 @@ import com.google.gerrit.server.config.CachedPreferences;
import java.io.IOException;
import java.util.Collection;
import java.util.Optional;
+import java.util.StringJoiner;
/**
* Superset of all information related to an Account. This includes external IDs, project watches,
@@ -142,6 +143,21 @@ public abstract class AccountState {
return h.toString();
}
+ public final String debugString() {
+ // Most of the fields might have a large representation. Using a multiline format to ease the
+ // reading.
+ return "AccountState[\n\t"
+ + new StringJoiner(",\n\t")
+ .add("account: " + account().debugString())
+ .add("externalIds: " + externalIds())
+ .add("userName: " + userName())
+ .add("projectWatches: " + projectWatches())
+ .add("generalPreferences: " + generalPreferences())
+ .add("diffPreferences: " + diffPreferences())
+ .add("editPreferences: " + editPreferences())
+ + "\n]";
+ }
+
/** Gerrit's default preferences as stored in {@code preferences.config}. */
public abstract Optional<CachedPreferences> defaultPreferences();
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index 24e8ba59d8..5951a73a09 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -144,6 +144,7 @@ public abstract class AccountsUpdate {
* instead, i.e. the update does not depend on the current account state (which, for insertion,
* would only contain the account ID).
*/
+ @CanIgnoreReturnValue
public AccountState insert(
String message, Account.Id accountId, Consumer<AccountDelta.Builder> init)
throws IOException, ConfigInvalidException {
@@ -210,6 +211,7 @@ public abstract class AccountsUpdate {
* the error. Callers should be aware that a single "update of death" (or a set of updates that
* together have this property) will always prevent the entire batch from being executed.
*/
+ @CanIgnoreReturnValue
public ImmutableList<Optional<AccountState>> updateBatch(List<UpdateArguments> updates)
throws IOException, ConfigInvalidException {
checkArgument(
diff --git a/java/com/google/gerrit/server/account/CachedAccountDetails.java b/java/com/google/gerrit/server/account/CachedAccountDetails.java
index e167a23b3d..e6e27356d8 100644
--- a/java/com/google/gerrit/server/account/CachedAccountDetails.java
+++ b/java/com/google/gerrit/server/account/CachedAccountDetails.java
@@ -114,7 +114,8 @@ public abstract class CachedAccountDetails {
.setDisplayName(Strings.nullToEmpty(account.displayName()))
.setPreferredEmail(Strings.nullToEmpty(account.preferredEmail()))
.setStatus(Strings.nullToEmpty(account.status()))
- .setMetaId(Strings.nullToEmpty(account.metaId()));
+ .setMetaId(Strings.nullToEmpty(account.metaId()))
+ .setUniqueTag(Strings.nullToEmpty(account.uniqueTag()));
serialized.setAccount(accountProto);
for (Map.Entry<ProjectWatches.ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>> watch :
@@ -145,7 +146,7 @@ public abstract class CachedAccountDetails {
public CachedAccountDetails deserialize(byte[] in) {
Cache.AccountDetailsProto proto =
Protos.parseUnchecked(Cache.AccountDetailsProto.parser(), in);
- Account account =
+ Account.Builder builder =
Account.builder(
Account.id(proto.getAccount().getId()),
Instant.ofEpochMilli(proto.getAccount().getRegisteredOn()))
@@ -155,7 +156,11 @@ public abstract class CachedAccountDetails {
.setInactive(proto.getAccount().getInactive())
.setStatus(Strings.emptyToNull(proto.getAccount().getStatus()))
.setMetaId(Strings.emptyToNull(proto.getAccount().getMetaId()))
- .build();
+ .setUniqueTag(Strings.emptyToNull(proto.getAccount().getUniqueTag()));
+ if (Strings.isNullOrEmpty(builder.uniqueTag())) {
+ builder.setUniqueTag(builder.metaId());
+ }
+ Account account = builder.build();
ImmutableMap.Builder<ProjectWatches.ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>>
projectWatches = ImmutableMap.builder();
diff --git a/java/com/google/gerrit/server/account/DefaultRealm.java b/java/com/google/gerrit/server/account/DefaultRealm.java
index cfffceb11b..9ac55fbe5a 100644
--- a/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -16,6 +16,7 @@ package com.google.gerrit.server.account;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.exceptions.StorageException;
@@ -26,7 +27,6 @@ import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.Set;
@Singleton
public class DefaultRealm extends AbstractRealm {
@@ -85,7 +85,7 @@ public class DefaultRealm extends AbstractRealm {
public Account.Id lookup(String accountName) throws IOException {
if (emailExpander.canExpand(accountName)) {
try {
- Set<Account.Id> c = emails.get().getAccountFor(emailExpander.expand(accountName));
+ ImmutableSet<Account.Id> c = emails.get().getAccountFor(emailExpander.expand(accountName));
if (1 == c.size()) {
return c.iterator().next();
}
diff --git a/java/com/google/gerrit/server/account/Emails.java b/java/com/google/gerrit/server/account/Emails.java
index 13385d0156..38d95f60b7 100644
--- a/java/com/google/gerrit/server/account/Emails.java
+++ b/java/com/google/gerrit/server/account/Emails.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.account;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.MultimapBuilder;
@@ -30,8 +31,6 @@ import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Arrays;
-import java.util.List;
-import java.util.Set;
import org.eclipse.jgit.lib.PersonIdent;
/** Class to access accounts by email. */
@@ -90,7 +89,7 @@ public class Emails {
MultimapBuilder.hashKeys(emails.length).hashSetValues(1).build();
externalIds.byEmails(emails).entries().stream()
.forEach(e -> result.put(e.getKey(), e.getValue().accountId()));
- List<String> emailsToBackfill =
+ ImmutableList<String> emailsToBackfill =
Arrays.stream(emails).filter(e -> !result.containsKey(e)).collect(toImmutableList());
if (!emailsToBackfill.isEmpty()) {
retryHelper
@@ -122,7 +121,7 @@ public class Emails {
// If only one account has access to this email address, select it
// as the identity of the user.
//
- Set<Account.Id> a = getAccountFor(u.getEmail());
+ ImmutableSet<Account.Id> a = getAccountFor(u.getEmail());
if (a.size() == 1) {
u.setAccount(a.iterator().next());
}
diff --git a/java/com/google/gerrit/server/account/GroupCacheImpl.java b/java/com/google/gerrit/server/account/GroupCacheImpl.java
index fac2fd5135..aed73de1cb 100644
--- a/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -55,7 +55,6 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
-import java.util.Set;
import java.util.concurrent.ExecutionException;
import org.bouncycastle.util.Strings;
import org.eclipse.jgit.lib.ObjectId;
@@ -178,7 +177,7 @@ public class GroupCacheImpl implements GroupCache {
@Override
public Map<AccountGroup.UUID, InternalGroup> get(Collection<AccountGroup.UUID> groupUuids) {
try {
- Set<String> groupUuidsStringSet =
+ ImmutableSet<String> groupUuidsStringSet =
groupUuids.stream().map(u -> u.get()).collect(toImmutableSet());
return byUUID.getAll(groupUuidsStringSet).entrySet().stream()
.filter(g -> g.getValue().isPresent())
diff --git a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index fc6087b5be..914bdd2067 100644
--- a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -38,10 +38,9 @@ import com.google.gerrit.server.group.db.Groups;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.logging.TraceContext.TraceTimer;
-import com.google.gerrit.server.query.group.InternalGroupQuery;
+import com.google.gerrit.server.update.RetryHelper;
import com.google.inject.Inject;
import com.google.inject.Module;
-import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
@@ -191,11 +190,11 @@ public class GroupIncludeCacheImpl implements GroupIncludeCache {
static class GroupsWithMemberLoader
extends CacheLoader<Account.Id, ImmutableSet<AccountGroup.UUID>> {
- private final Provider<InternalGroupQuery> groupQueryProvider;
+ private final RetryHelper retryHelper;
@Inject
- GroupsWithMemberLoader(Provider<InternalGroupQuery> groupQueryProvider) {
- this.groupQueryProvider = groupQueryProvider;
+ GroupsWithMemberLoader(RetryHelper retryHelper) {
+ this.retryHelper = retryHelper;
}
@Override
@@ -203,9 +202,14 @@ public class GroupIncludeCacheImpl implements GroupIncludeCache {
try (TraceTimer timer =
TraceContext.newTimer(
"Loading groups with member", Metadata.builder().accountId(memberId.get()).build())) {
- return groupQueryProvider.get().byMember(memberId).stream()
- .map(InternalGroup::getGroupUUID)
- .collect(toImmutableSet());
+ return retryHelper
+ .groupIndexQuery(
+ "loadGroupWithMember",
+ q ->
+ q.byMember(memberId).stream()
+ .map(InternalGroup::getGroupUUID)
+ .collect(toImmutableSet()))
+ .call();
}
}
}
@@ -215,11 +219,11 @@ public class GroupIncludeCacheImpl implements GroupIncludeCache {
// Be conservative with batching: We don't want to exhaust the number of
// results per page and maximum terms per query. Both are usually 1000+.
private static final int MAX_BATCH_SIZE = 100;
- private final Provider<InternalGroupQuery> groupQueryProvider;
+ private final RetryHelper retryHelper;
@Inject
- ParentGroupsLoader(Provider<InternalGroupQuery> groupQueryProvider) {
- this.groupQueryProvider = groupQueryProvider;
+ ParentGroupsLoader(RetryHelper retryHelper) {
+ this.retryHelper = retryHelper;
}
@Override
@@ -238,10 +242,12 @@ public class GroupIncludeCacheImpl implements GroupIncludeCache {
Map<AccountGroup.UUID, ImmutableSet<AccountGroup.UUID>> result =
Maps.newHashMapWithExpectedSize(numKeys);
try (TraceTimer timer = TraceContext.newTimer("Loading " + numKeys + " parent groups")) {
+ Map<AccountGroup.UUID, ImmutableSet<AccountGroup.UUID>> bySubgroups =
+ retryHelper
+ .groupIndexQuery("loadParentGroups", q -> q.bySubgroups(ImmutableSet.copyOf(keys)))
+ .call();
Iterables.partition(keys, MAX_BATCH_SIZE)
- .forEach(
- keyPartition ->
- result.putAll(groupQueryProvider.get().bySubgroups(ImmutableSet.copyOf(keys))));
+ .forEach(keyPartition -> result.putAll(bySubgroups));
return result;
}
}
diff --git a/java/com/google/gerrit/server/account/GroupMembers.java b/java/com/google/gerrit/server/account/GroupMembers.java
index 3ed82a10ae..42f07f13d4 100644
--- a/java/com/google/gerrit/server/account/GroupMembers.java
+++ b/java/com/google/gerrit/server/account/GroupMembers.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.account;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.gerrit.server.project.ProjectCache.noSuchProject;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.gerrit.common.Nullable;
@@ -123,7 +124,7 @@ public class GroupMembers {
seen.add(group.getGroupUUID());
GroupControl groupControl = groupControlFactory.controlFor(new InternalGroupDescription(group));
- Set<Account> directMembers =
+ ImmutableSet<Account> directMembers =
group.getMembers().stream()
.filter(groupControl::canSeeMember)
.map(accountCache::get)
diff --git a/java/com/google/gerrit/server/account/IncludingGroupMembership.java b/java/com/google/gerrit/server/account/IncludingGroupMembership.java
index e1edf10b9c..881a0687b2 100644
--- a/java/com/google/gerrit/server/account/IncludingGroupMembership.java
+++ b/java/com/google/gerrit/server/account/IncludingGroupMembership.java
@@ -45,7 +45,7 @@ public class IncludingGroupMembership implements GroupMembership {
private final GroupIncludeCache includeCache;
private final CurrentUser user;
private final Map<AccountGroup.UUID, Boolean> memberOf;
- private Set<AccountGroup.UUID> knownGroups;
+ private ImmutableSet<AccountGroup.UUID> knownGroups;
@Inject
IncludingGroupMembership(
diff --git a/java/com/google/gerrit/server/account/ListGroupMembership.java b/java/com/google/gerrit/server/account/ListGroupMembership.java
index 0f4fb78c6f..67e168eca1 100644
--- a/java/com/google/gerrit/server/account/ListGroupMembership.java
+++ b/java/com/google/gerrit/server/account/ListGroupMembership.java
@@ -21,7 +21,7 @@ import java.util.Set;
/** GroupMembership over an explicit list. */
public class ListGroupMembership implements GroupMembership {
- private final Set<AccountGroup.UUID> groups;
+ private final ImmutableSet<AccountGroup.UUID> groups;
public ListGroupMembership(Iterable<AccountGroup.UUID> groupIds) {
this.groups = ImmutableSet.copyOf(groupIds);
diff --git a/java/com/google/gerrit/server/account/SetInactiveFlag.java b/java/com/google/gerrit/server/account/SetInactiveFlag.java
index 5babebdfcb..4da4c5b5a7 100644
--- a/java/com/google/gerrit/server/account/SetInactiveFlag.java
+++ b/java/com/google/gerrit/server/account/SetInactiveFlag.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.account;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.events.AccountActivationListener;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -51,6 +52,7 @@ public class SetInactiveFlag {
this.accountActivationListeners = accountActivationListeners;
}
+ @CanIgnoreReturnValue
public Response<?> deactivate(Account.Id accountId)
throws RestApiException, IOException, ConfigInvalidException {
AtomicBoolean alreadyInactive = new AtomicBoolean(false);
@@ -94,6 +96,7 @@ public class SetInactiveFlag {
return Response.none();
}
+ @CanIgnoreReturnValue
public Response<String> activate(Account.Id accountId)
throws RestApiException, IOException, ConfigInvalidException {
AtomicBoolean alreadyActive = new AtomicBoolean(false);
diff --git a/java/com/google/gerrit/server/account/UniversalGroupBackend.java b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
index 476ca79d00..38b79f00dc 100644
--- a/java/com/google/gerrit/server/account/UniversalGroupBackend.java
+++ b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -163,7 +163,7 @@ public class UniversalGroupBackend implements GroupBackend {
}
private class UniversalGroupMembership implements GroupMembership {
- private final Map<GroupBackend, GroupMembership> memberships;
+ private final ImmutableMap<GroupBackend, GroupMembership> memberships;
private UniversalGroupMembership(CurrentUser user) {
ImmutableMap.Builder<GroupBackend, GroupMembership> builder = ImmutableMap.builder();
diff --git a/java/com/google/gerrit/server/account/VersionedAccountDestinations.java b/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
index 41a02a956d..1de138f7a5 100644
--- a/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
+++ b/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
@@ -51,6 +51,7 @@ public class VersionedAccountDestinations extends VersionedMetaData {
if (revision == null) {
return;
}
+ logger.atFine().log("Loading named destinations from ref %s", ref);
String prefix = DestinationList.DIR_NAME + "/";
for (PathInfo p : getPathInfos(true)) {
if (p.fileMode == FileMode.REGULAR_FILE) {
diff --git a/java/com/google/gerrit/server/account/VersionedAccountQueries.java b/java/com/google/gerrit/server/account/VersionedAccountQueries.java
index 0269ccf5cd..6d89cfae0e 100644
--- a/java/com/google/gerrit/server/account/VersionedAccountQueries.java
+++ b/java/com/google/gerrit/server/account/VersionedAccountQueries.java
@@ -68,6 +68,7 @@ public class VersionedAccountQueries extends VersionedMetaData {
@Override
protected void onLoad() throws IOException, ConfigInvalidException {
+ logger.atFine().log("Loading named queries from ref %s", ref);
queryList =
QueryList.parse(
readUTF8(QueryList.FILE_NAME),
diff --git a/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java b/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
index 1fce3d5313..61199384a8 100644
--- a/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
+++ b/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
@@ -20,6 +20,7 @@ import static java.util.stream.Collectors.toList;
import com.google.common.base.Strings;
import com.google.common.collect.Ordering;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.RefNames;
@@ -96,6 +97,7 @@ public class VersionedAuthorizedKeys extends VersionedMetaData {
return read(accountId).getKey(seq);
}
+ @CanIgnoreReturnValue
public synchronized AccountSshKey addKey(Account.Id accountId, String pub)
throws IOException, ConfigInvalidException, InvalidSshKeyException {
VersionedAuthorizedKeys authorizedKeys = read(accountId);
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java b/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
index d226565dda..00a7f6ccaa 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
@@ -127,4 +127,7 @@ public interface ExternalIdFactory {
* @return the created external ID
*/
ExternalId createEmail(Account.Id accountId, String email);
+
+ /** Whether this {@link ExternalIdFactory} supports passwords. */
+ boolean arePasswordsAllowed();
}
diff --git a/java/com/google/gerrit/server/account/externalids/package-info.java b/java/com/google/gerrit/server/account/externalids/package-info.java
new file mode 100644
index 0000000000..daf6920edb
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.account.externalids;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java
index 3462c765bf..d3de7159b8 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java
@@ -223,6 +223,11 @@ public class ExternalIdFactoryNoteDbImpl implements ExternalIdFactory {
blobId);
}
+ @Override
+ public boolean arePasswordsAllowed() {
+ return true;
+ }
+
private static int readAccountId(String noteId, Config externalIdConfig, String externalIdKeyStr)
throws ConfigInvalidException {
String accountIdStr =
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNotes.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNotes.java
index 534625282a..f0632e4185 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNotes.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNotes.java
@@ -157,8 +157,8 @@ public class ExternalIdNotes extends VersionedMetaData {
* @param externalIdNotes the committed updates that should be applied to the cache. This first
* and last element must be the updates commited first and last, respectively.
* @param accountsToSkipForReindex accounts that should not be reindexed. This is to avoid
- * double reindexing when updated accounts will already be reindexed by
- * ReindexAfterRefUpdate.
+ * double reindexing when updated accounts will already be reindexed by {@link
+ * com.google.gerrit.server.index.account.ReindexAccountsAfterRefUpdate}.
*/
public void updateExternalIdCacheAndMaybeReindexAccounts(
ExternalIdNotes externalIdNotes, Collection<Account.Id> accountsToSkipForReindex)
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsConsistencyCheckerNoteDbImpl.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsConsistencyCheckerNoteDbImpl.java
index 83c72f1d93..db6fdceb6d 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsConsistencyCheckerNoteDbImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsConsistencyCheckerNoteDbImpl.java
@@ -14,7 +14,6 @@
package com.google.gerrit.server.account.externalids.storage.notedb;
-import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import static java.util.stream.Collectors.joining;
@@ -24,7 +23,6 @@ import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyP
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.HashedPassword;
import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdFactory;
import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -53,14 +51,11 @@ public class ExternalIdsConsistencyCheckerNoteDbImpl implements ExternalIdsConsi
GitRepositoryManager repoManager,
AllUsersName allUsers,
OutgoingEmailValidator validator,
- ExternalIdFactory externalIdFactory) {
+ ExternalIdFactoryNoteDbImpl externalIdFactory) {
this.repoManager = repoManager;
this.allUsers = allUsers;
this.validator = validator;
- checkState(
- externalIdFactory instanceof ExternalIdFactoryNoteDbImpl,
- "ExternalIdsConsistencyCheckerNoteDbImpl must be initiated with ExternalIdFactoryNoteDbImpl.");
- this.externalIdFactory = (ExternalIdFactoryNoteDbImpl) externalIdFactory;
+ this.externalIdFactory = externalIdFactory;
}
@Override
@@ -148,7 +143,8 @@ public class ExternalIdsConsistencyCheckerNoteDbImpl implements ExternalIdsConsi
if (extId.password() != null && extId.isScheme(SCHEME_USERNAME)) {
try {
- HashedPassword.decode(extId.password());
+ @SuppressWarnings("unused")
+ var unused = HashedPassword.decode(extId.password());
} catch (HashedPassword.DecoderException e) {
addError(
String.format(
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/OnlineExternalIdCaseSensivityMigrator.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/OnlineExternalIdCaseSensivityMigrator.java
index ab56c949d9..eaded123e4 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/OnlineExternalIdCaseSensivityMigrator.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/OnlineExternalIdCaseSensivityMigrator.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.account.externalids.storage.notedb;
+import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIds;
@@ -26,7 +27,6 @@ import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.nio.file.Path;
-import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -81,7 +81,7 @@ public class OnlineExternalIdCaseSensivityMigrator {
executor.execute(
() -> {
try {
- Set<ExternalId> todo = externalIds.all();
+ ImmutableSet<ExternalId> todo = externalIds.all();
try {
monitor.beginTask("Converting external ID note names", todo.size());
migratorFactory
@@ -93,7 +93,9 @@ public class OnlineExternalIdCaseSensivityMigrator {
try {
updateGerritConfig();
monitor.beginTask("Reindex accounts", ProgressMonitor.UNKNOWN);
- versionManager.startReindexer("accounts", true);
+
+ @SuppressWarnings("unused")
+ var unused = versionManager.startReindexer("accounts", true);
} finally {
monitor.endTask();
}
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/package-info.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/package-info.java
new file mode 100644
index 0000000000..e3a6d9e488
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.account.externalids.storage.notedb;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/account/externalids/testing/BUILD b/java/com/google/gerrit/server/account/externalids/testing/BUILD
index e2de6da3b8..7202ca006f 100644
--- a/java/com/google/gerrit/server/account/externalids/testing/BUILD
+++ b/java/com/google/gerrit/server/account/externalids/testing/BUILD
@@ -10,5 +10,6 @@ java_library(
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/testing:test-ref-update-context",
"//lib:jgit",
+ "//lib/errorprone:annotations",
],
)
diff --git a/java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java b/java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java
index dc2fd3ccbb..eea78e588f 100644
--- a/java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java
+++ b/java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java
@@ -19,6 +19,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.server.account.externalids.ExternalId;
@@ -38,6 +39,7 @@ import org.eclipse.jgit.revwalk.RevWalk;
/** Common methods for dealing with external IDs in tests. */
public class ExternalIdTestUtil {
+ @CanIgnoreReturnValue
public static String insertExternalIdWithoutAccountId(
Repository repo, RevWalk rw, PersonIdent ident, Account.Id accountId, String externalId)
throws IOException {
@@ -60,6 +62,7 @@ public class ExternalIdTestUtil {
});
}
+ @CanIgnoreReturnValue
public static String insertExternalIdWithKeyThatDoesntMatchNoteId(
Repository repo, RevWalk rw, PersonIdent ident, Account.Id accountId, String externalId)
throws IOException {
@@ -81,6 +84,7 @@ public class ExternalIdTestUtil {
});
}
+ @CanIgnoreReturnValue
public static String insertExternalIdWithInvalidConfig(
Repository repo, RevWalk rw, PersonIdent ident, String externalId) throws IOException {
return insertExternalId(
@@ -96,6 +100,7 @@ public class ExternalIdTestUtil {
});
}
+ @CanIgnoreReturnValue
public static String insertExternalIdWithEmptyNote(
Repository repo, RevWalk rw, PersonIdent ident, String externalId) throws IOException {
return insertExternalId(
diff --git a/java/com/google/gerrit/server/account/externalids/testing/package-info.java b/java/com/google/gerrit/server/account/externalids/testing/package-info.java
new file mode 100644
index 0000000000..a752fea0aa
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.account.externalids.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/account/package-info.java b/java/com/google/gerrit/server/account/package-info.java
new file mode 100644
index 0000000000..786f0b0432
--- /dev/null
+++ b/java/com/google/gerrit/server/account/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.account;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/account/storage/notedb/AccountNoteDbReadStorageModule.java b/java/com/google/gerrit/server/account/storage/notedb/AccountNoteDbReadStorageModule.java
index 9253133071..c861bc0736 100644
--- a/java/com/google/gerrit/server/account/storage/notedb/AccountNoteDbReadStorageModule.java
+++ b/java/com/google/gerrit/server/account/storage/notedb/AccountNoteDbReadStorageModule.java
@@ -16,6 +16,8 @@ package com.google.gerrit.server.account.storage.notedb;
import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNoteDbReadStorageModule;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
+import com.google.gerrit.server.mail.send.MessageIdGeneratorImpl;
import com.google.inject.AbstractModule;
import com.google.inject.Singleton;
@@ -25,5 +27,6 @@ public class AccountNoteDbReadStorageModule extends AbstractModule {
install(new ExternalIdNoteDbReadStorageModule());
bind(Accounts.class).to(AccountsNoteDbImpl.class).in(Singleton.class);
+ bind(MessageIdGenerator.class).to(MessageIdGeneratorImpl.class);
}
}
diff --git a/java/com/google/gerrit/server/account/storage/notedb/AccountNoteDbWriteStorageModule.java b/java/com/google/gerrit/server/account/storage/notedb/AccountNoteDbWriteStorageModule.java
index 24eabb1bfd..6987de5542 100644
--- a/java/com/google/gerrit/server/account/storage/notedb/AccountNoteDbWriteStorageModule.java
+++ b/java/com/google/gerrit/server/account/storage/notedb/AccountNoteDbWriteStorageModule.java
@@ -14,8 +14,11 @@
package com.google.gerrit.server.account.storage.notedb;
+import com.google.gerrit.extensions.events.GitBatchRefUpdateListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNoteDbWriteStorageModule;
+import com.google.gerrit.server.index.account.ReindexAccountsAfterRefUpdate;
import com.google.inject.AbstractModule;
/** Module that binds {@link AccountsUpdate} */
@@ -29,5 +32,7 @@ public class AccountNoteDbWriteStorageModule extends AbstractModule {
bind(AccountsUpdate.AccountsUpdateLoader.class)
.annotatedWith(AccountsUpdate.AccountsUpdateLoader.NoReindex.class)
.to(AccountsUpdateNoteDbImpl.FactoryNoReindex.class);
+ DynamicSet.bind(binder(), GitBatchRefUpdateListener.class)
+ .to(ReindexAccountsAfterRefUpdate.class);
}
}
diff --git a/java/com/google/gerrit/server/account/storage/notedb/AccountsUpdateNoteDbImpl.java b/java/com/google/gerrit/server/account/storage/notedb/AccountsUpdateNoteDbImpl.java
index 265d036a0c..ad3681d5cf 100644
--- a/java/com/google/gerrit/server/account/storage/notedb/AccountsUpdateNoteDbImpl.java
+++ b/java/com/google/gerrit/server/account/storage/notedb/AccountsUpdateNoteDbImpl.java
@@ -49,7 +49,7 @@ import com.google.gerrit.server.config.VersionedDefaultPreferences;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.index.change.ReindexAfterRefUpdate;
+import com.google.gerrit.server.index.account.ReindexAccountsAfterRefUpdate;
import com.google.gerrit.server.update.RetryHelper;
import com.google.gerrit.server.update.RetryableAction.Action;
import com.google.gerrit.server.update.context.RefUpdateContext;
@@ -102,21 +102,24 @@ import org.eclipse.jgit.transport.ReceiveCommand;
* branch (see {@link ExternalIdNotes}).
*
* <p>On updating an account the account is evicted from the account cache and reindexed. The
- * eviction from the account cache and the reindexing is done by the {@link ReindexAfterRefUpdate}
- * class which receives the event about updating the user branch that is triggered by this class.
+ * eviction from the account cache and the reindexing is done by the {@link
+ * ReindexAccountsAfterRefUpdate} class which receives the event about updating the user branch that
+ * is triggered by this class.
*
* <p>If external IDs are updated, the ExternalIdCache is automatically updated by {@link
* ExternalIdNotes}. In addition {@link ExternalIdNotes} takes care about evicting and reindexing
* corresponding accounts. This is needed because external ID updates don't touch the user branches.
- * Hence in this case the accounts are not evicted and reindexed via {@link ReindexAfterRefUpdate}.
+ * Hence, in this case the accounts are not evicted and reindexed via {@link
+ * ReindexAccountsAfterRefUpdate}.
*
- * <p>Reindexing and flushing accounts from the account cache can be disabled by
+ * <p>Reindexing and flushing accounts from the account cache can be disabled by-
*
* <ul>
* <li>using {@link
* com.google.gerrit.server.account.storage.notedb.AccountsUpdateNoteDbImpl.FactoryNoReindex}
* and
- * <li>binding {@link GitReferenceUpdated#DISABLED}
+ * <li>binding {@link GitReferenceUpdated#DISABLED}, or avoid binding {@link
+ * ReindexAccountsAfterRefUpdate}.
* </ul>
*
* <p>If there are concurrent account updates which updating the user branch in NoteDb may fail with
@@ -156,7 +159,7 @@ public class AccountsUpdateNoteDbImpl extends AccountsUpdate {
}
@Override
- public AccountsUpdate create(IdentifiedUser currentUser) {
+ public AccountsUpdateNoteDbImpl create(IdentifiedUser currentUser) {
PersonIdent serverIdent = serverIdentProvider.get();
return new AccountsUpdateNoteDbImpl(
repoManager,
@@ -173,7 +176,7 @@ public class AccountsUpdateNoteDbImpl extends AccountsUpdate {
}
@Override
- public AccountsUpdate createWithServerIdent() {
+ public AccountsUpdateNoteDbImpl createWithServerIdent() {
PersonIdent serverIdent = serverIdentProvider.get();
return new AccountsUpdateNoteDbImpl(
repoManager,
diff --git a/java/com/google/gerrit/server/account/storage/notedb/package-info.java b/java/com/google/gerrit/server/account/storage/notedb/package-info.java
new file mode 100644
index 0000000000..9eaa284cdf
--- /dev/null
+++ b/java/com/google/gerrit/server/account/storage/notedb/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.account.storage.notedb;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/api/BUILD b/java/com/google/gerrit/server/api/BUILD
index 0275c797ba..d21ba45395 100644
--- a/java/com/google/gerrit/server/api/BUILD
+++ b/java/com/google/gerrit/server/api/BUILD
@@ -19,6 +19,7 @@ java_library(
"//lib:guava",
"//lib:jgit",
"//lib:servlet-api",
+ "//lib/errorprone:annotations",
"//lib/guice",
"//lib/guice:guice-assistedinject",
],
diff --git a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index 400521b4df..55aa5d7404 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -257,9 +257,11 @@ public class AccountApiImpl implements AccountApi {
public void setActive(boolean active) throws RestApiException {
try {
if (active) {
- putActive.apply(account, new Input());
+ @SuppressWarnings("unused")
+ var unused = putActive.apply(account, new Input());
} else {
- deleteActive.apply(account, new Input());
+ @SuppressWarnings("unused")
+ var unused = deleteActive.apply(account, new Input());
}
} catch (Exception e) {
throw asRestApiException("Cannot set active", e);
@@ -348,7 +350,8 @@ public class AccountApiImpl implements AccountApi {
@Override
public void deleteWatchedProjects(List<ProjectWatchInfo> in) throws RestApiException {
try {
- deleteWatchedProjects.apply(account, in);
+ @SuppressWarnings("unused")
+ var unused = deleteWatchedProjects.apply(account, in);
} catch (Exception e) {
throw asRestApiException("Cannot delete watched projects", e);
}
@@ -357,7 +360,8 @@ public class AccountApiImpl implements AccountApi {
@Override
public void starChange(String changeId) throws RestApiException {
try {
- starredChangesCreate.apply(account, IdString.fromUrl(changeId), new Input());
+ @SuppressWarnings("unused")
+ var unused = starredChangesCreate.apply(account, IdString.fromUrl(changeId), new Input());
} catch (Exception e) {
throw asRestApiException("Cannot star change", e);
}
@@ -369,7 +373,9 @@ public class AccountApiImpl implements AccountApi {
ChangeResource rsrc = changes.parse(TopLevelResource.INSTANCE, IdString.fromUrl(changeId));
AccountResource.StarredChange starredChange =
new AccountResource.StarredChange(account.getUser(), rsrc);
- starredChangesDelete.apply(starredChange, new Input());
+
+ @SuppressWarnings("unused")
+ var unused = starredChangesDelete.apply(starredChange, new Input());
} catch (Exception e) {
throw asRestApiException("Cannot unstar change", e);
}
@@ -397,7 +403,8 @@ public class AccountApiImpl implements AccountApi {
public void addEmail(EmailInput input) throws RestApiException {
AccountResource.Email rsrc = new AccountResource.Email(account.getUser(), input.email);
try {
- createEmail.apply(rsrc, IdString.fromDecoded(input.email), input);
+ @SuppressWarnings("unused")
+ var unused = createEmail.apply(rsrc, IdString.fromDecoded(input.email), input);
} catch (Exception e) {
throw asRestApiException("Cannot add email", e);
}
@@ -407,7 +414,8 @@ public class AccountApiImpl implements AccountApi {
public void deleteEmail(String email) throws RestApiException {
AccountResource.Email rsrc = new AccountResource.Email(account.getUser(), email);
try {
- deleteEmail.apply(rsrc, null);
+ @SuppressWarnings("unused")
+ var unused = deleteEmail.apply(rsrc, null);
} catch (Exception e) {
throw asRestApiException("Cannot delete email", e);
}
@@ -417,7 +425,8 @@ public class AccountApiImpl implements AccountApi {
public EmailApi createEmail(EmailInput input) throws RestApiException {
AccountResource.Email rsrc = new AccountResource.Email(account.getUser(), input.email);
try {
- createEmail.apply(rsrc, IdString.fromDecoded(input.email), input);
+ @SuppressWarnings("unused")
+ var unused = createEmail.apply(rsrc, IdString.fromDecoded(input.email), input);
return email(rsrc.getEmail());
} catch (Exception e) {
throw asRestApiException("Cannot create email", e);
@@ -437,7 +446,8 @@ public class AccountApiImpl implements AccountApi {
public void setStatus(String status) throws RestApiException {
StatusInput in = new StatusInput(status);
try {
- putStatus.apply(account, in);
+ @SuppressWarnings("unused")
+ var unused = putStatus.apply(account, in);
} catch (Exception e) {
throw asRestApiException("Cannot set status", e);
}
@@ -447,7 +457,8 @@ public class AccountApiImpl implements AccountApi {
public void setDisplayName(String displayName) throws RestApiException {
DisplayNameInput in = new DisplayNameInput(displayName);
try {
- putDisplayName.apply(account, in);
+ @SuppressWarnings("unused")
+ var unused = putDisplayName.apply(account, in);
} catch (Exception e) {
throw asRestApiException("Cannot set display name", e);
}
@@ -478,7 +489,9 @@ public class AccountApiImpl implements AccountApi {
try {
AccountResource.SshKey sshKeyRes =
sshKeys.parse(account, IdString.fromDecoded(Integer.toString(seq)));
- deleteSshKey.apply(sshKeyRes, null);
+
+ @SuppressWarnings("unused")
+ var unused = deleteSshKey.apply(sshKeyRes, null);
} catch (Exception e) {
throw asRestApiException("Cannot delete SSH key", e);
}
@@ -526,7 +539,9 @@ public class AccountApiImpl implements AccountApi {
try {
AgreementInput input = new AgreementInput();
input.name = agreementName;
- putAgreement.apply(account, input);
+
+ @SuppressWarnings("unused")
+ var unused = putAgreement.apply(account, input);
} catch (Exception e) {
throw asRestApiException("Cannot sign agreement", e);
}
@@ -535,7 +550,8 @@ public class AccountApiImpl implements AccountApi {
@Override
public void index() throws RestApiException {
try {
- index.apply(account, new Input());
+ @SuppressWarnings("unused")
+ var unused = index.apply(account, new Input());
} catch (Exception e) {
throw asRestApiException("Cannot index account", e);
}
@@ -553,7 +569,8 @@ public class AccountApiImpl implements AccountApi {
@Override
public void deleteExternalIds(List<String> externalIds) throws RestApiException {
try {
- deleteExternalIds.apply(account, externalIds);
+ @SuppressWarnings("unused")
+ var unused = deleteExternalIds.apply(account, externalIds);
} catch (Exception e) {
throw asRestApiException("Cannot delete external IDs", e);
}
@@ -574,7 +591,8 @@ public class AccountApiImpl implements AccountApi {
NameInput input = new NameInput();
input.name = name;
try {
- putName.apply(account, input);
+ @SuppressWarnings("unused")
+ var unused = putName.apply(account, input);
} catch (Exception e) {
throw asRestApiException("Cannot set account name", e);
}
diff --git a/java/com/google/gerrit/server/api/accounts/EmailApiImpl.java b/java/com/google/gerrit/server/api/accounts/EmailApiImpl.java
index f68142f982..06a74f4239 100644
--- a/java/com/google/gerrit/server/api/accounts/EmailApiImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/EmailApiImpl.java
@@ -70,7 +70,8 @@ public class EmailApiImpl implements EmailApi {
@Override
public void delete() throws RestApiException {
try {
- delete.apply(resource(), new Input());
+ @SuppressWarnings("unused")
+ var unused = delete.apply(resource(), new Input());
} catch (Exception e) {
throw asRestApiException("Cannot delete email", e);
}
@@ -79,7 +80,8 @@ public class EmailApiImpl implements EmailApi {
@Override
public void setPreferred() throws RestApiException {
try {
- putPreferred.apply(resource(), new Input());
+ @SuppressWarnings("unused")
+ var unused = putPreferred.apply(resource(), new Input());
} catch (Exception e) {
throw asRestApiException(String.format("Cannot set %s as preferred email", email), e);
}
diff --git a/java/com/google/gerrit/server/api/accounts/package-info.java b/java/com/google/gerrit/server/api/accounts/package-info.java
new file mode 100644
index 0000000000..9edd8d77c6
--- /dev/null
+++ b/java/com/google/gerrit/server/api/accounts/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.api.accounts;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/api/changes/AttentionSetApiImpl.java b/java/com/google/gerrit/server/api/changes/AttentionSetApiImpl.java
index 6c792965cb..534bffb996 100644
--- a/java/com/google/gerrit/server/api/changes/AttentionSetApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/AttentionSetApiImpl.java
@@ -43,7 +43,8 @@ public class AttentionSetApiImpl implements AttentionSetApi {
@Override
public void remove(AttentionSetInput input) throws RestApiException {
try {
- removeFromAttentionSet.apply(attentionSetEntryResource, input);
+ @SuppressWarnings("unused")
+ var unused = removeFromAttentionSet.apply(attentionSetEntryResource, input);
} catch (Exception e) {
throw asRestApiException("Cannot remove from attention set", e);
}
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 62650d2835..a3df786d27 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -50,6 +50,7 @@ import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInfoDifference;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.common.CommitMessageInfo;
import com.google.gerrit.extensions.common.CommitMessageInput;
import com.google.gerrit.extensions.common.Input;
import com.google.gerrit.extensions.common.InputWithMessage;
@@ -84,6 +85,7 @@ import com.google.gerrit.server.restapi.change.DeletePrivate;
import com.google.gerrit.server.restapi.change.GetChange;
import com.google.gerrit.server.restapi.change.GetCustomKeyedValues;
import com.google.gerrit.server.restapi.change.GetHashtags;
+import com.google.gerrit.server.restapi.change.GetMessage;
import com.google.gerrit.server.restapi.change.GetMetaDiff;
import com.google.gerrit.server.restapi.change.GetPureRevert;
import com.google.gerrit.server.restapi.change.GetTopic;
@@ -172,6 +174,7 @@ class ChangeApiImpl implements ChangeApi {
private final DeletePrivate deletePrivate;
private final SetWorkInProgress setWip;
private final SetReadyForReview setReady;
+ private final GetMessage getMessage;
private final PutMessage putMessage;
private final Provider<GetPureRevert> getPureRevertProvider;
private final DynamicOptionParser dynamicOptionParser;
@@ -224,6 +227,7 @@ class ChangeApiImpl implements ChangeApi {
DeletePrivate deletePrivate,
SetWorkInProgress setWip,
SetReadyForReview setReady,
+ GetMessage getMessage,
PutMessage putMessage,
Provider<GetPureRevert> getPureRevertProvider,
DynamicOptionParser dynamicOptionParser,
@@ -274,6 +278,7 @@ class ChangeApiImpl implements ChangeApi {
this.deletePrivate = deletePrivate;
this.setWip = setWip;
this.setReady = setReady;
+ this.getMessage = getMessage;
this.putMessage = putMessage;
this.getPureRevertProvider = getPureRevertProvider;
this.dynamicOptionParser = dynamicOptionParser;
@@ -308,7 +313,8 @@ class ChangeApiImpl implements ChangeApi {
@Override
public void abandon(AbandonInput in) throws RestApiException {
try {
- abandon.apply(change, in);
+ @SuppressWarnings("unused")
+ var unused = abandon.apply(change, in);
} catch (Exception e) {
throw asRestApiException("Cannot abandon change", e);
}
@@ -317,7 +323,8 @@ class ChangeApiImpl implements ChangeApi {
@Override
public void restore(RestoreInput in) throws RestApiException {
try {
- restore.apply(change, in);
+ @SuppressWarnings("unused")
+ var unused = restore.apply(change, in);
} catch (Exception e) {
throw asRestApiException("Cannot restore change", e);
}
@@ -326,7 +333,8 @@ class ChangeApiImpl implements ChangeApi {
@Override
public void move(MoveInput in) throws RestApiException {
try {
- move.apply(change, in);
+ @SuppressWarnings("unused")
+ var unused = move.apply(change, in);
} catch (Exception e) {
throw asRestApiException("Cannot move change", e);
}
@@ -337,9 +345,11 @@ class ChangeApiImpl implements ChangeApi {
try {
InputWithMessage input = new InputWithMessage(message);
if (value) {
- postPrivate.apply(change, input);
+ @SuppressWarnings("unused")
+ var unused = postPrivate.apply(change, input);
} else {
- deletePrivate.apply(change, input);
+ @SuppressWarnings("unused")
+ var unused = deletePrivate.apply(change, input);
}
} catch (Exception e) {
throw asRestApiException("Cannot change private status", e);
@@ -349,7 +359,8 @@ class ChangeApiImpl implements ChangeApi {
@Override
public void setWorkInProgress(@Nullable String message) throws RestApiException {
try {
- setWip.apply(change, new WorkInProgressOp.Input(message));
+ @SuppressWarnings("unused")
+ var unused = setWip.apply(change, new WorkInProgressOp.Input(message));
} catch (Exception e) {
throw asRestApiException("Cannot set work in progress state", e);
}
@@ -358,7 +369,8 @@ class ChangeApiImpl implements ChangeApi {
@Override
public void setReadyForReview(@Nullable String message) throws RestApiException {
try {
- setReady.apply(change, new WorkInProgressOp.Input(message));
+ @SuppressWarnings("unused")
+ var unused = setReady.apply(change, new WorkInProgressOp.Input(message));
} catch (Exception e) {
throw asRestApiException("Cannot set ready for review state", e);
}
@@ -418,7 +430,8 @@ class ChangeApiImpl implements ChangeApi {
@Override
public void rebase(RebaseInput in) throws RestApiException {
try {
- rebase.apply(change, in);
+ @SuppressWarnings("unused")
+ var unused = rebase.apply(change, in);
} catch (Exception e) {
throw asRestApiException("Cannot rebase change", e);
}
@@ -436,7 +449,8 @@ class ChangeApiImpl implements ChangeApi {
@Override
public void delete() throws RestApiException {
try {
- deleteChange.apply(change, null);
+ @SuppressWarnings("unused")
+ var unused = deleteChange.apply(change, null);
} catch (Exception e) {
throw asRestApiException("Cannot delete change", e);
}
@@ -456,7 +470,8 @@ class ChangeApiImpl implements ChangeApi {
TopicInput in = new TopicInput();
in.topic = topic;
try {
- putTopic.apply(change, in);
+ @SuppressWarnings("unused")
+ var unused = putTopic.apply(change, in);
} catch (Exception e) {
throw asRestApiException("Cannot set topic", e);
}
@@ -551,9 +566,19 @@ class ChangeApiImpl implements ChangeApi {
}
@Override
+ public CommitMessageInfo getMessage() throws RestApiException {
+ try {
+ return getMessage.apply(change).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot get message", e);
+ }
+ }
+
+ @Override
public void setMessage(CommitMessageInput in) throws RestApiException {
try {
- putMessage.apply(change, in);
+ @SuppressWarnings("unused")
+ var unused = putMessage.apply(change, in);
} catch (Exception e) {
throw asRestApiException("Cannot edit commit message", e);
}
@@ -562,7 +587,8 @@ class ChangeApiImpl implements ChangeApi {
@Override
public void setHashtags(HashtagsInput input) throws RestApiException {
try {
- postHashtags.apply(change, input);
+ @SuppressWarnings("unused")
+ var unused = postHashtags.apply(change, input);
} catch (Exception e) {
throw asRestApiException("Cannot post hashtags", e);
}
@@ -731,7 +757,8 @@ class ChangeApiImpl implements ChangeApi {
@Override
public void index() throws RestApiException {
try {
- index.apply(change, new Input());
+ @SuppressWarnings("unused")
+ var unused = index.apply(change, new Input());
} catch (Exception e) {
throw asRestApiException("Cannot index change", e);
}
diff --git a/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
index 1b0f0c556c..c76eeeb96c 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.api.changes;
import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
import com.google.gerrit.extensions.api.changes.ChangeEditApi;
+import com.google.gerrit.extensions.api.changes.ChangeEditIdentityType;
import com.google.gerrit.extensions.api.changes.FileContentInput;
import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
import com.google.gerrit.extensions.client.ChangeEditDetailOption;
@@ -55,6 +56,7 @@ public class ChangeEditApiImpl implements ChangeEditApi {
private final ChangeEdits.DeleteContent changeEditDeleteContent;
private final Provider<ChangeEdits.GetMessage> getChangeEditCommitMessageProvider;
private final ChangeEdits.EditMessage modifyChangeEditCommitMessage;
+ private final ChangeEdits.EditIdentity modifyIdentity;
private final ChangeEdits changeEdits;
private final ChangeResource changeResource;
@@ -70,6 +72,7 @@ public class ChangeEditApiImpl implements ChangeEditApi {
ChangeEdits.DeleteContent changeEditDeleteContent,
Provider<ChangeEdits.GetMessage> getChangeEditCommitMessageProvider,
ChangeEdits.EditMessage modifyChangeEditCommitMessage,
+ ChangeEdits.EditIdentity modifyIdentity,
ChangeEdits changeEdits,
@Assisted ChangeResource changeResource) {
this.editDetailProvider = editDetailProvider;
@@ -82,6 +85,7 @@ public class ChangeEditApiImpl implements ChangeEditApi {
this.changeEditDeleteContent = changeEditDeleteContent;
this.getChangeEditCommitMessageProvider = getChangeEditCommitMessageProvider;
this.modifyChangeEditCommitMessage = modifyChangeEditCommitMessage;
+ this.modifyIdentity = modifyIdentity;
this.changeEdits = changeEdits;
this.changeResource = changeResource;
}
@@ -127,7 +131,8 @@ public class ChangeEditApiImpl implements ChangeEditApi {
@Override
public void create() throws RestApiException {
try {
- changeEditsPost.apply(changeResource, null);
+ @SuppressWarnings("unused")
+ var unused = changeEditsPost.apply(changeResource, null);
} catch (Exception e) {
throw asRestApiException("Cannot create change edit", e);
}
@@ -136,7 +141,8 @@ public class ChangeEditApiImpl implements ChangeEditApi {
@Override
public void delete() throws RestApiException {
try {
- deleteChangeEdit.apply(changeResource, new Input());
+ @SuppressWarnings("unused")
+ var unused = deleteChangeEdit.apply(changeResource, new Input());
} catch (Exception e) {
throw asRestApiException("Cannot delete change edit", e);
}
@@ -145,7 +151,8 @@ public class ChangeEditApiImpl implements ChangeEditApi {
@Override
public void rebase() throws RestApiException {
try {
- rebaseChangeEdit.apply(changeResource, null);
+ @SuppressWarnings("unused")
+ var unused = rebaseChangeEdit.apply(changeResource, null);
} catch (Exception e) {
throw asRestApiException("Cannot rebase change edit", e);
}
@@ -159,7 +166,8 @@ public class ChangeEditApiImpl implements ChangeEditApi {
@Override
public void publish(PublishChangeEditInput publishChangeEditInput) throws RestApiException {
try {
- publishChangeEdit.apply(changeResource, publishChangeEditInput);
+ @SuppressWarnings("unused")
+ var unused = publishChangeEdit.apply(changeResource, publishChangeEditInput);
} catch (Exception e) {
throw asRestApiException("Cannot publish change edit", e);
}
@@ -182,7 +190,9 @@ public class ChangeEditApiImpl implements ChangeEditApi {
ChangeEdits.Post.Input renameInput = new ChangeEdits.Post.Input();
renameInput.oldPath = oldFilePath;
renameInput.newPath = newFilePath;
- changeEditsPost.apply(changeResource, renameInput);
+
+ @SuppressWarnings("unused")
+ var unused = changeEditsPost.apply(changeResource, renameInput);
} catch (Exception e) {
throw asRestApiException("Cannot rename file of change edit", e);
}
@@ -193,7 +203,9 @@ public class ChangeEditApiImpl implements ChangeEditApi {
try {
ChangeEdits.Post.Input restoreInput = new ChangeEdits.Post.Input();
restoreInput.restorePath = filePath;
- changeEditsPost.apply(changeResource, restoreInput);
+
+ @SuppressWarnings("unused")
+ var unused = changeEditsPost.apply(changeResource, restoreInput);
} catch (Exception e) {
throw asRestApiException("Cannot restore file of change edit", e);
}
@@ -202,7 +214,8 @@ public class ChangeEditApiImpl implements ChangeEditApi {
@Override
public void modifyFile(String filePath, FileContentInput input) throws RestApiException {
try {
- changeEditsPut.apply(changeResource, filePath, input);
+ @SuppressWarnings("unused")
+ var unused = changeEditsPut.apply(changeResource, filePath, input);
} catch (Exception e) {
throw asRestApiException("Cannot modify file of change edit", e);
}
@@ -211,7 +224,8 @@ public class ChangeEditApiImpl implements ChangeEditApi {
@Override
public void deleteFile(String filePath) throws RestApiException {
try {
- changeEditDeleteContent.apply(changeResource, filePath);
+ @SuppressWarnings("unused")
+ var unused = changeEditDeleteContent.apply(changeResource, filePath);
} catch (Exception e) {
throw asRestApiException("Cannot delete file of change edit", e);
}
@@ -234,12 +248,28 @@ public class ChangeEditApiImpl implements ChangeEditApi {
ChangeEdits.EditMessage.Input input = new ChangeEdits.EditMessage.Input();
input.message = newCommitMessage;
try {
- modifyChangeEditCommitMessage.apply(changeResource, input);
+ @SuppressWarnings("unused")
+ var unused = modifyChangeEditCommitMessage.apply(changeResource, input);
} catch (Exception e) {
throw asRestApiException("Cannot modify commit message of change edit", e);
}
}
+ @Override
+ public void modifyIdentity(String name, String email, ChangeEditIdentityType type)
+ throws RestApiException {
+ ChangeEdits.EditIdentity.Input input = new ChangeEdits.EditIdentity.Input();
+ input.name = name;
+ input.email = email;
+ input.type = type;
+ try {
+ @SuppressWarnings("unused")
+ var unused = modifyIdentity.apply(changeResource, input);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot edit identity of change", e);
+ }
+ }
+
private ChangeEditResource getChangeEditResource(String filePath)
throws ResourceNotFoundException, AuthException, IOException {
return changeEdits.parse(changeResource, IdString.fromDecoded(filePath));
diff --git a/java/com/google/gerrit/server/api/changes/ChangesImpl.java b/java/com/google/gerrit/server/api/changes/ChangesImpl.java
index 16668204ab..cf08aeee49 100644
--- a/java/com/google/gerrit/server/api/changes/ChangesImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangesImpl.java
@@ -126,7 +126,7 @@ class ChangesImpl implements Changes {
public QueryRequest query() {
return new QueryRequest() {
@Override
- public List<ChangeInfo> get() throws RestApiException {
+ public ImmutableList<ChangeInfo> get() throws RestApiException {
return ChangesImpl.this.get(this);
}
};
@@ -137,7 +137,7 @@ class ChangesImpl implements Changes {
return query().withQuery(query);
}
- private List<ChangeInfo> get(QueryRequest q) throws RestApiException {
+ private ImmutableList<ChangeInfo> get(QueryRequest q) throws RestApiException {
try (DynamicOptions dynamicOptions = new DynamicOptions(injector, dynamicBeans)) {
QueryChanges qc = queryProvider.get();
if (q.getQuery() != null) {
diff --git a/java/com/google/gerrit/server/api/changes/DraftApiImpl.java b/java/com/google/gerrit/server/api/changes/DraftApiImpl.java
index f6eb3c53e0..67f98ca9fb 100644
--- a/java/com/google/gerrit/server/api/changes/DraftApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/DraftApiImpl.java
@@ -72,7 +72,8 @@ class DraftApiImpl implements DraftApi {
@Override
public void delete() throws RestApiException {
try {
- deleteDraft.apply(draft, null);
+ @SuppressWarnings("unused")
+ var unused = deleteDraft.apply(draft, null);
} catch (Exception e) {
throw asRestApiException("Cannot delete draft", e);
}
diff --git a/java/com/google/gerrit/server/api/changes/FileApiImpl.java b/java/com/google/gerrit/server/api/changes/FileApiImpl.java
index cf9f2432b7..6aa2cf12fc 100644
--- a/java/com/google/gerrit/server/api/changes/FileApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/FileApiImpl.java
@@ -110,9 +110,11 @@ class FileApiImpl implements FileApi {
public void setReviewed(boolean reviewed) throws RestApiException {
try {
if (reviewed) {
- putReviewed.apply(file, new Input());
+ @SuppressWarnings("unused")
+ var unused = putReviewed.apply(file, new Input());
} else {
- deleteReviewed.apply(file, new Input());
+ @SuppressWarnings("unused")
+ var unused = deleteReviewed.apply(file, new Input());
}
} catch (Exception e) {
throw asRestApiException(String.format("Cannot set %sreviewed", reviewed ? "" : "un"), e);
diff --git a/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java b/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java
index 2174ef0d43..10973cac97 100644
--- a/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java
@@ -63,7 +63,8 @@ public class ReviewerApiImpl implements ReviewerApi {
@Override
public void deleteVote(String label) throws RestApiException {
try {
- deleteVote.apply(new VoteResource(reviewer, label), null);
+ @SuppressWarnings("unused")
+ var unused = deleteVote.apply(new VoteResource(reviewer, label), null);
} catch (Exception e) {
throw asRestApiException("Cannot delete vote", e);
}
@@ -72,7 +73,8 @@ public class ReviewerApiImpl implements ReviewerApi {
@Override
public void deleteVote(DeleteVoteInput input) throws RestApiException {
try {
- deleteVote.apply(new VoteResource(reviewer, input.label), input);
+ @SuppressWarnings("unused")
+ var unused = deleteVote.apply(new VoteResource(reviewer, input.label), input);
} catch (Exception e) {
throw asRestApiException("Cannot delete vote", e);
}
@@ -86,7 +88,8 @@ public class ReviewerApiImpl implements ReviewerApi {
@Override
public void remove(DeleteReviewerInput input) throws RestApiException {
try {
- deleteReviewer.apply(reviewer, input);
+ @SuppressWarnings("unused")
+ var unused = deleteReviewer.apply(reviewer, input);
} catch (Exception e) {
throw asRestApiException("Cannot remove reviewer", e);
}
diff --git a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index a7931f1289..38510e338b 100644
--- a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -339,7 +339,9 @@ class RevisionApiImpl extends RevisionApi.NotImplemented {
} else {
view = deleteReviewed;
}
- view.apply(files.parse(revision, IdString.fromDecoded(path)), new Input());
+
+ @SuppressWarnings("unused")
+ var unused = view.apply(files.parse(revision, IdString.fromDecoded(path)), new Input());
} catch (Exception e) {
throw asRestApiException("Cannot update reviewed flag", e);
}
@@ -691,7 +693,8 @@ class RevisionApiImpl extends RevisionApi.NotImplemented {
DescriptionInput in = new DescriptionInput();
in.description = description;
try {
- putDescription.apply(revision, in);
+ @SuppressWarnings("unused")
+ var unused = putDescription.apply(revision, in);
} catch (Exception e) {
throw asRestApiException("Cannot set description", e);
}
diff --git a/java/com/google/gerrit/server/api/changes/RevisionReviewerApiImpl.java b/java/com/google/gerrit/server/api/changes/RevisionReviewerApiImpl.java
index 49c2d4931c..fcce2ddf40 100644
--- a/java/com/google/gerrit/server/api/changes/RevisionReviewerApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/RevisionReviewerApiImpl.java
@@ -56,7 +56,8 @@ public class RevisionReviewerApiImpl implements RevisionReviewerApi {
@Override
public void deleteVote(String label) throws RestApiException {
try {
- deleteVote.apply(new VoteResource(reviewer, label), null);
+ @SuppressWarnings("unused")
+ var unused = deleteVote.apply(new VoteResource(reviewer, label), null);
} catch (Exception e) {
throw asRestApiException("Cannot delete vote", e);
}
@@ -65,7 +66,8 @@ public class RevisionReviewerApiImpl implements RevisionReviewerApi {
@Override
public void deleteVote(DeleteVoteInput input) throws RestApiException {
try {
- deleteVote.apply(new VoteResource(reviewer, input.label), input);
+ @SuppressWarnings("unused")
+ var unused = deleteVote.apply(new VoteResource(reviewer, input.label), input);
} catch (Exception e) {
throw asRestApiException("Cannot delete vote", e);
}
diff --git a/java/com/google/gerrit/server/api/changes/package-info.java b/java/com/google/gerrit/server/api/changes/package-info.java
new file mode 100644
index 0000000000..ccd493cfc6
--- /dev/null
+++ b/java/com/google/gerrit/server/api/changes/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.api.changes;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/api/config/ConfigModule.java b/java/com/google/gerrit/server/api/config/ConfigModule.java
index 9340ae11f1..0eae2ad185 100644
--- a/java/com/google/gerrit/server/api/config/ConfigModule.java
+++ b/java/com/google/gerrit/server/api/config/ConfigModule.java
@@ -23,5 +23,7 @@ public class ConfigModule extends FactoryModule {
protected void configure() {
bind(Config.class).to(ConfigImpl.class);
bind(Server.class).to(ServerImpl.class);
+
+ factory(ExperimentApiImpl.Factory.class);
}
}
diff --git a/java/com/google/gerrit/server/api/config/ExperimentApiImpl.java b/java/com/google/gerrit/server/api/config/ExperimentApiImpl.java
new file mode 100644
index 0000000000..7eacf201e0
--- /dev/null
+++ b/java/com/google/gerrit/server/api/config/ExperimentApiImpl.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.api.config;
+
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
+import com.google.gerrit.extensions.api.config.ExperimentApi;
+import com.google.gerrit.extensions.common.ExperimentInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.config.ExperimentResource;
+import com.google.gerrit.server.restapi.config.GetExperiment;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+public class ExperimentApiImpl implements ExperimentApi {
+ interface Factory {
+ ExperimentApiImpl create(ExperimentResource r);
+ }
+
+ private final ExperimentResource experiment;
+ private final GetExperiment getExperiment;
+
+ @Inject
+ ExperimentApiImpl(GetExperiment getExperiment, @Assisted ExperimentResource r) {
+ this.getExperiment = getExperiment;
+ this.experiment = r;
+ }
+
+ @Override
+ public ExperimentInfo get() throws RestApiException {
+ try {
+ return getExperiment.apply(experiment).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot get experiment", e);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/api/config/ServerImpl.java b/java/com/google/gerrit/server/api/config/ServerImpl.java
index ab40ec8743..3928387079 100644
--- a/java/com/google/gerrit/server/api/config/ServerImpl.java
+++ b/java/com/google/gerrit/server/api/config/ServerImpl.java
@@ -16,22 +16,28 @@ package com.google.gerrit.server.api.config;
import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+import com.google.common.collect.ImmutableMap;
import com.google.gerrit.common.Version;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInput;
+import com.google.gerrit.extensions.api.config.ExperimentApi;
import com.google.gerrit.extensions.api.config.Server;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.EditPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.common.ExperimentInfo;
import com.google.gerrit.extensions.common.ServerInfo;
+import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.webui.TopMenu;
import com.google.gerrit.server.config.ConfigResource;
import com.google.gerrit.server.restapi.config.CheckConsistency;
+import com.google.gerrit.server.restapi.config.ExperimentsCollection;
import com.google.gerrit.server.restapi.config.GetDiffPreferences;
import com.google.gerrit.server.restapi.config.GetEditPreferences;
import com.google.gerrit.server.restapi.config.GetPreferences;
import com.google.gerrit.server.restapi.config.GetServerInfo;
+import com.google.gerrit.server.restapi.config.ListExperiments;
import com.google.gerrit.server.restapi.config.ListTopMenus;
import com.google.gerrit.server.restapi.config.SetDiffPreferences;
import com.google.gerrit.server.restapi.config.SetEditPreferences;
@@ -52,6 +58,9 @@ public class ServerImpl implements Server {
private final GetServerInfo getServerInfo;
private final Provider<CheckConsistency> checkConsistency;
private final ListTopMenus listTopMenus;
+ private final ExperimentApiImpl.Factory experimentApi;
+ private final ExperimentsCollection experimentsCollection;
+ private final Provider<ListExperiments> listExperimentsProvider;
@Inject
ServerImpl(
@@ -63,7 +72,10 @@ public class ServerImpl implements Server {
SetEditPreferences setEditPreferences,
GetServerInfo getServerInfo,
Provider<CheckConsistency> checkConsistency,
- ListTopMenus listTopMenus) {
+ ListTopMenus listTopMenus,
+ ExperimentApiImpl.Factory experimentApi,
+ ExperimentsCollection experimentsCollection,
+ Provider<ListExperiments> listExperimentsProvider) {
this.getPreferences = getPreferences;
this.setPreferences = setPreferences;
this.getDiffPreferences = getDiffPreferences;
@@ -73,6 +85,9 @@ public class ServerImpl implements Server {
this.getServerInfo = getServerInfo;
this.checkConsistency = checkConsistency;
this.listTopMenus = listTopMenus;
+ this.experimentApi = experimentApi;
+ this.experimentsCollection = experimentsCollection;
+ this.listExperimentsProvider = listExperimentsProvider;
}
@Override
@@ -163,4 +178,37 @@ public class ServerImpl implements Server {
throw asRestApiException("Cannot get top menus", e);
}
}
+
+ @Override
+ public ExperimentApi experiment(String name) throws RestApiException {
+ try {
+ return experimentApi.create(
+ experimentsCollection.parse(new ConfigResource(), IdString.fromDecoded(name)));
+ } catch (Exception e) {
+ throw asRestApiException("Cannot parse experiment", e);
+ }
+ }
+
+ @Override
+ public ListExperimentsRequest listExperiments() throws RestApiException {
+ return new ListExperimentsRequest() {
+ @Override
+ public ImmutableMap<String, ExperimentInfo> get() throws RestApiException {
+ return ServerImpl.this.listExperiments(this);
+ }
+ };
+ }
+
+ private ImmutableMap<String, ExperimentInfo> listExperiments(ListExperimentsRequest r)
+ throws RestApiException {
+ try {
+ ListExperiments listExperiments = listExperimentsProvider.get();
+ if (r.getEnabledOnly()) {
+ listExperiments.setEnabledOnly(true);
+ }
+ return listExperiments.apply(new ConfigResource()).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot retrieve experiments", e);
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/api/config/package-info.java b/java/com/google/gerrit/server/api/config/package-info.java
new file mode 100644
index 0000000000..686e656036
--- /dev/null
+++ b/java/com/google/gerrit/server/api/config/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.api.config;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/api/groups/GroupApiImpl.java b/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
index bb04ab4da2..db906a9044 100644
--- a/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
+++ b/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
@@ -148,7 +148,8 @@ class GroupApiImpl implements GroupApi {
NameInput in = new NameInput();
in.name = name;
try {
- putName.apply(rsrc, in);
+ @SuppressWarnings("unused")
+ var unused = putName.apply(rsrc, in);
} catch (Exception e) {
throw asRestApiException("Cannot put group name", e);
}
@@ -168,7 +169,8 @@ class GroupApiImpl implements GroupApi {
OwnerInput in = new OwnerInput();
in.owner = owner;
try {
- putOwner.apply(rsrc, in);
+ @SuppressWarnings("unused")
+ var unused = putOwner.apply(rsrc, in);
} catch (Exception e) {
throw asRestApiException("Cannot put group owner", e);
}
@@ -188,7 +190,8 @@ class GroupApiImpl implements GroupApi {
DescriptionInput in = new DescriptionInput();
in.description = description;
try {
- putDescription.apply(rsrc, in);
+ @SuppressWarnings("unused")
+ var unused = putDescription.apply(rsrc, in);
} catch (Exception e) {
throw asRestApiException("Cannot put group description", e);
}
@@ -206,7 +209,8 @@ class GroupApiImpl implements GroupApi {
@Override
public void options(GroupOptionsInfo options) throws RestApiException {
try {
- putOptions.apply(rsrc, options);
+ @SuppressWarnings("unused")
+ var unused = putOptions.apply(rsrc, options);
} catch (Exception e) {
throw asRestApiException("Cannot put group options", e);
}
@@ -230,7 +234,8 @@ class GroupApiImpl implements GroupApi {
@Override
public void addMembers(List<String> members) throws RestApiException {
try {
- addMembers.apply(rsrc, AddMembers.Input.fromMembers(members));
+ @SuppressWarnings("unused")
+ var unused = addMembers.apply(rsrc, AddMembers.Input.fromMembers(members));
} catch (Exception e) {
throw asRestApiException("Cannot add group members", e);
}
@@ -239,7 +244,8 @@ class GroupApiImpl implements GroupApi {
@Override
public void removeMembers(List<String> members) throws RestApiException {
try {
- deleteMembers.apply(rsrc, AddMembers.Input.fromMembers(members));
+ @SuppressWarnings("unused")
+ var unused = deleteMembers.apply(rsrc, AddMembers.Input.fromMembers(members));
} catch (Exception e) {
throw asRestApiException("Cannot remove group members", e);
}
@@ -257,7 +263,8 @@ class GroupApiImpl implements GroupApi {
@Override
public void addGroups(List<String> groups) throws RestApiException {
try {
- addSubgroups.apply(rsrc, AddSubgroups.Input.fromGroups(groups));
+ @SuppressWarnings("unused")
+ var unused = addSubgroups.apply(rsrc, AddSubgroups.Input.fromGroups(groups));
} catch (Exception e) {
throw asRestApiException("Cannot add subgroups", e);
}
@@ -266,7 +273,8 @@ class GroupApiImpl implements GroupApi {
@Override
public void removeGroups(List<String> groups) throws RestApiException {
try {
- deleteSubgroups.apply(rsrc, AddSubgroups.Input.fromGroups(groups));
+ @SuppressWarnings("unused")
+ var unused = deleteSubgroups.apply(rsrc, AddSubgroups.Input.fromGroups(groups));
} catch (Exception e) {
throw asRestApiException("Cannot remove subgroups", e);
}
@@ -284,7 +292,8 @@ class GroupApiImpl implements GroupApi {
@Override
public void index() throws RestApiException {
try {
- index.apply(rsrc, new Input());
+ @SuppressWarnings("unused")
+ var unused = index.apply(rsrc, new Input());
} catch (Exception e) {
throw asRestApiException("Cannot index group", e);
}
diff --git a/java/com/google/gerrit/server/api/groups/package-info.java b/java/com/google/gerrit/server/api/groups/package-info.java
new file mode 100644
index 0000000000..69f84c62b5
--- /dev/null
+++ b/java/com/google/gerrit/server/api/groups/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.api.groups;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/api/package-info.java b/java/com/google/gerrit/server/api/package-info.java
new file mode 100644
index 0000000000..6e4c05d36b
--- /dev/null
+++ b/java/com/google/gerrit/server/api/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.api;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/api/plugins/PluginApiImpl.java b/java/com/google/gerrit/server/api/plugins/PluginApiImpl.java
index 59c396a56d..347094f9cf 100644
--- a/java/com/google/gerrit/server/api/plugins/PluginApiImpl.java
+++ b/java/com/google/gerrit/server/api/plugins/PluginApiImpl.java
@@ -64,13 +64,15 @@ public class PluginApiImpl implements PluginApi {
@Override
public void enable() throws RestApiException {
- enable.apply(resource, new Input());
+ @SuppressWarnings("unused")
+ var unused = enable.apply(resource, new Input());
}
@Override
public void disable() throws RestApiException {
try {
- disable.apply(resource, new Input());
+ @SuppressWarnings("unused")
+ var unused = disable.apply(resource, new Input());
} catch (Exception e) {
throw asRestApiException("Cannot disable plugin", e);
}
@@ -78,6 +80,7 @@ public class PluginApiImpl implements PluginApi {
@Override
public void reload() throws RestApiException {
- reload.apply(resource, new Input());
+ @SuppressWarnings("unused")
+ var unused = reload.apply(resource, new Input());
}
}
diff --git a/java/com/google/gerrit/server/api/plugins/package-info.java b/java/com/google/gerrit/server/api/plugins/package-info.java
new file mode 100644
index 0000000000..cd6ad53e34
--- /dev/null
+++ b/java/com/google/gerrit/server/api/plugins/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.api.plugins;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/api/projects/BranchApiImpl.java b/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
index 1fad91d8e6..549c8f3e01 100644
--- a/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
@@ -88,7 +88,8 @@ public class BranchApiImpl implements BranchApi {
@Override
public BranchApi create(BranchInput input) throws RestApiException {
try {
- createBranch.apply(project, IdString.fromDecoded(ref), input);
+ @SuppressWarnings("unused")
+ var unused = createBranch.apply(project, IdString.fromDecoded(ref), input);
return this;
} catch (Exception e) {
throw asRestApiException("Cannot create branch", e);
@@ -107,7 +108,8 @@ public class BranchApiImpl implements BranchApi {
@Override
public void delete() throws RestApiException {
try {
- deleteBranch.apply(resource(), new Input());
+ @SuppressWarnings("unused")
+ var unused = deleteBranch.apply(resource(), new Input());
} catch (Exception e) {
throw asRestApiException("Cannot delete branch", e);
}
diff --git a/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java b/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java
index 61736f6d5c..1bb0b76c7e 100644
--- a/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java
@@ -78,8 +78,11 @@ public class DashboardApiImpl implements DashboardApi {
SetDashboardInput input = new SetDashboardInput();
input.id = id;
try {
- set.apply(
- DashboardResource.projectDefault(project.getProjectState(), project.getUser()), input);
+ @SuppressWarnings("unused")
+ var unused =
+ set.apply(
+ DashboardResource.projectDefault(project.getProjectState(), project.getUser()),
+ input);
} catch (Exception e) {
String msg = String.format("Cannot %s default dashboard", id != null ? "set" : "remove");
throw asRestApiException(msg, e);
diff --git a/java/com/google/gerrit/server/api/projects/LabelApiImpl.java b/java/com/google/gerrit/server/api/projects/LabelApiImpl.java
index d00f44773e..2a7ad0abb9 100644
--- a/java/com/google/gerrit/server/api/projects/LabelApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/LabelApiImpl.java
@@ -74,7 +74,8 @@ public class LabelApiImpl implements LabelApi {
@Override
public LabelApi create(LabelDefinitionInput input) throws RestApiException {
try {
- createLabel.apply(project, IdString.fromDecoded(label), input);
+ @SuppressWarnings("unused")
+ var unused = createLabel.apply(project, IdString.fromDecoded(label), input);
// recreate project resource because project state was updated by creating the new label and
// needs to be reloaded
@@ -111,7 +112,8 @@ public class LabelApiImpl implements LabelApi {
@Override
public void delete(@Nullable String commitMessage) throws RestApiException {
try {
- deleteLabel.apply(resource(), new InputWithCommitMessage(commitMessage));
+ @SuppressWarnings("unused")
+ var unused = deleteLabel.apply(resource(), new InputWithCommitMessage(commitMessage));
} catch (Exception e) {
throw asRestApiException("Cannot delete label", e);
}
diff --git a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index ad42ae61ed..5c24ddcf61 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -20,6 +20,7 @@ import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdate
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.HEAD_MODIFICATION;
import static java.util.stream.Collectors.toList;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
import com.google.gerrit.extensions.api.access.ProjectAccessInput;
import com.google.gerrit.extensions.api.config.AccessCheckInfo;
@@ -417,7 +418,10 @@ public class ProjectApiImpl implements ProjectApi {
permissionBackend
.currentUser()
.checkAny(GlobalPermission.fromAnnotation(createProject.getClass()));
- createProject.apply(TopLevelResource.INSTANCE, IdString.fromDecoded(name), in);
+
+ @SuppressWarnings("unused")
+ var unused = createProject.apply(TopLevelResource.INSTANCE, IdString.fromDecoded(name), in);
+
return projectApi.create(projects.parse(name));
} catch (Exception e) {
throw asRestApiException("Cannot create project: " + e.getMessage(), e);
@@ -489,7 +493,8 @@ public class ProjectApiImpl implements ProjectApi {
@Override
public void description(DescriptionInput in) throws RestApiException {
try {
- putDescription.apply(checkExists(), in);
+ @SuppressWarnings("unused")
+ var unused = putDescription.apply(checkExists(), in);
} catch (Exception e) {
throw asRestApiException("Cannot put project description", e);
}
@@ -529,7 +534,7 @@ public class ProjectApiImpl implements ProjectApi {
public ListRefsRequest<BranchInfo> branches() {
return new ListRefsRequest<>() {
@Override
- public List<BranchInfo> get() throws RestApiException {
+ public ImmutableList<BranchInfo> get() throws RestApiException {
try {
return listBranches.get().request(this).apply(checkExists()).value();
} catch (Exception e) {
@@ -543,7 +548,7 @@ public class ProjectApiImpl implements ProjectApi {
public ListRefsRequest<TagInfo> tags() {
return new ListRefsRequest<>() {
@Override
- public List<TagInfo> get() throws RestApiException {
+ public ImmutableList<TagInfo> get() throws RestApiException {
try {
return listTags.get().request(this).apply(checkExists()).value();
} catch (Exception e) {
@@ -599,7 +604,8 @@ public class ProjectApiImpl implements ProjectApi {
public void deleteBranches(DeleteBranchesInput in) throws RestApiException {
try {
try (RefUpdateContext ctx = RefUpdateContext.open(BRANCH_MODIFICATION)) {
- deleteBranches.apply(checkExists(), in);
+ @SuppressWarnings("unused")
+ var unused = deleteBranches.apply(checkExists(), in);
}
} catch (Exception e) {
throw asRestApiException("Cannot delete branches", e);
@@ -609,7 +615,8 @@ public class ProjectApiImpl implements ProjectApi {
@Override
public void deleteTags(DeleteTagsInput in) throws RestApiException {
try {
- deleteTags.apply(checkExists(), in);
+ @SuppressWarnings("unused")
+ var unused = deleteTags.apply(checkExists(), in);
} catch (Exception e) {
throw asRestApiException("Cannot delete tags", e);
}
@@ -692,7 +699,8 @@ public class ProjectApiImpl implements ProjectApi {
input.ref = head;
try {
try (RefUpdateContext ctx = RefUpdateContext.open(HEAD_MODIFICATION)) {
- setHead.apply(checkExists(), input);
+ @SuppressWarnings("unused")
+ var unused = setHead.apply(checkExists(), input);
}
} catch (Exception e) {
throw asRestApiException("Cannot set HEAD", e);
@@ -713,7 +721,9 @@ public class ProjectApiImpl implements ProjectApi {
try {
ParentInput input = new ParentInput();
input.parent = parent;
- setParent.apply(checkExists(), input);
+
+ @SuppressWarnings("unused")
+ var unused = setParent.apply(checkExists(), input);
} catch (Exception e) {
throw asRestApiException("Cannot set parent", e);
}
@@ -724,7 +734,9 @@ public class ProjectApiImpl implements ProjectApi {
try {
IndexProjectInput input = new IndexProjectInput();
input.indexChildren = indexChildren;
- index.apply(checkExists(), input);
+
+ @SuppressWarnings("unused")
+ var unused = index.apply(checkExists(), input);
} catch (Exception e) {
throw asRestApiException("Cannot index project", e);
}
@@ -733,7 +745,8 @@ public class ProjectApiImpl implements ProjectApi {
@Override
public void indexChanges() throws RestApiException {
try {
- indexChanges.apply(checkExists(), new Input());
+ @SuppressWarnings("unused")
+ var unused = indexChanges.apply(checkExists(), new Input());
} catch (Exception e) {
throw asRestApiException("Cannot index changes", e);
}
@@ -795,7 +808,8 @@ public class ProjectApiImpl implements ProjectApi {
@Override
public void labels(BatchLabelInput input) throws RestApiException {
try {
- postLabels.apply(checkExists(), input);
+ @SuppressWarnings("unused")
+ var unused = postLabels.apply(checkExists(), input);
} catch (Exception e) {
throw asRestApiException("Cannot update labels", e);
}
diff --git a/java/com/google/gerrit/server/api/projects/SubmitRequirementApiImpl.java b/java/com/google/gerrit/server/api/projects/SubmitRequirementApiImpl.java
index aa6ef716a2..73be088d12 100644
--- a/java/com/google/gerrit/server/api/projects/SubmitRequirementApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/SubmitRequirementApiImpl.java
@@ -73,7 +73,8 @@ public class SubmitRequirementApiImpl implements SubmitRequirementApi {
@Override
public SubmitRequirementApi create(SubmitRequirementInput input) throws RestApiException {
try {
- createSubmitRequirement.apply(project, IdString.fromDecoded(name), input);
+ @SuppressWarnings("unused")
+ var unused = createSubmitRequirement.apply(project, IdString.fromDecoded(name), input);
// recreate project resource because project state was updated
project =
@@ -110,7 +111,8 @@ public class SubmitRequirementApiImpl implements SubmitRequirementApi {
@Override
public void delete() throws RestApiException {
try {
- deleteSubmitRequirement.apply(resource(), new Input());
+ @SuppressWarnings("unused")
+ var unused = deleteSubmitRequirement.apply(resource(), new Input());
} catch (Exception e) {
throw asRestApiException("Cannot delete submit requirement", e);
}
diff --git a/java/com/google/gerrit/server/api/projects/TagApiImpl.java b/java/com/google/gerrit/server/api/projects/TagApiImpl.java
index f9bd048a24..658509348e 100644
--- a/java/com/google/gerrit/server/api/projects/TagApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/TagApiImpl.java
@@ -66,7 +66,8 @@ public class TagApiImpl implements TagApi {
@Override
public TagApi create(TagInput input) throws RestApiException {
try {
- createTag.apply(project, IdString.fromDecoded(ref), input);
+ @SuppressWarnings("unused")
+ var unused = createTag.apply(project, IdString.fromDecoded(ref), input);
return this;
} catch (Exception e) {
throw asRestApiException("Cannot create tag", e);
@@ -86,7 +87,8 @@ public class TagApiImpl implements TagApi {
public void delete() throws RestApiException {
try {
try (RefUpdateContext ctx = RefUpdateContext.open(TAG_MODIFICATION)) {
- deleteTag.apply(resource(), new Input());
+ @SuppressWarnings("unused")
+ var unused = deleteTag.apply(resource(), new Input());
}
} catch (Exception e) {
throw asRestApiException("Cannot delete tag", e);
diff --git a/java/com/google/gerrit/server/api/projects/package-info.java b/java/com/google/gerrit/server/api/projects/package-info.java
new file mode 100644
index 0000000000..6430a05302
--- /dev/null
+++ b/java/com/google/gerrit/server/api/projects/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.api.projects;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/approval/ApprovalCopier.java b/java/com/google/gerrit/server/approval/ApprovalCopier.java
index a1889dacd3..9bb3bc9aa7 100644
--- a/java/com/google/gerrit/server/approval/ApprovalCopier.java
+++ b/java/com/google/gerrit/server/approval/ApprovalCopier.java
@@ -46,6 +46,7 @@ import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.approval.ApprovalContext;
import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
+import com.google.gerrit.server.update.RepoView;
import com.google.gerrit.server.util.LabelVote;
import com.google.gerrit.server.util.ManualRequestContext;
import com.google.gerrit.server.util.OneOffRequestContext;
@@ -54,7 +55,8 @@ import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
-import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -208,7 +210,7 @@ public class ApprovalCopier {
* </ul>
*/
@VisibleForTesting
- public Result forPatchSet(ChangeNotes notes, PatchSet ps, RevWalk rw, Config repoConfig) {
+ public Result forPatchSet(ChangeNotes notes, PatchSet ps, RepoView repoView) {
ProjectState project;
try (TraceTimer traceTimer =
TraceContext.newTimer(
@@ -221,7 +223,7 @@ public class ApprovalCopier {
projectCache
.get(notes.getProjectName())
.orElseThrow(illegalState(notes.getProjectName()));
- return computeForPatchSet(project.getLabelTypes(), notes, ps, rw, repoConfig);
+ return computeForPatchSet(project.getLabelTypes(), notes, ps, repoView);
}
}
@@ -267,7 +269,9 @@ public class ApprovalCopier {
}
try (Repository repo = repoManager.openRepository(changeNotes.getProjectName());
- RevWalk revWalk = new RevWalk(repo)) {
+ ObjectInserter ins = repo.newObjectInserter();
+ ObjectReader reader = ins.newReader();
+ RevWalk revWalk = new RevWalk(reader)) {
ImmutableList<PatchSet.Id> followUpPatchSets =
changeNotes.getPatchSets().keySet().stream()
.filter(psId -> psId.get() > sourcePatchSet.id().get())
@@ -296,8 +300,7 @@ public class ApprovalCopier {
approvalValue,
changeKind,
isMerge,
- revWalk,
- repo.getConfig())
+ new RepoView(repo, revWalk, ins))
.canCopy()) {
targetPatchSetsBuilder.add(followUpPatchSetId);
} else {
@@ -328,8 +331,7 @@ public class ApprovalCopier {
short approvalValue,
ChangeKind changeKind,
boolean isMerge,
- RevWalk revWalk,
- Config repoConfig) {
+ RepoView repoView) {
if (!labelType.getCopyCondition().isPresent()) {
return ApprovalCopyResult.createForMissingCopyCondition();
}
@@ -343,8 +345,7 @@ public class ApprovalCopier {
targetPatchSet,
changeKind,
isMerge,
- revWalk,
- repoConfig);
+ repoView);
try {
// Use a request context to run checks as an internal user with expanded visibility. This is
// so that the output of the copy condition does not depend on who is running the current
@@ -382,11 +383,7 @@ public class ApprovalCopier {
}
private Result computeForPatchSet(
- LabelTypes labelTypes,
- ChangeNotes notes,
- PatchSet targetPatchSet,
- RevWalk rw,
- Config repoConfig) {
+ LabelTypes labelTypes, ChangeNotes notes, PatchSet targetPatchSet, RepoView repoView) {
Project.NameKey projectName = notes.getProjectName();
PatchSet.Id targetPsId = targetPatchSet.id();
@@ -420,11 +417,11 @@ public class ApprovalCopier {
ChangeKind changeKind =
changeKindCache.getChangeKind(
projectName,
- rw,
- repoConfig,
+ repoView.getRevWalk(),
+ repoView.getConfig(),
priorPatchSet.getValue().commitId(),
targetPatchSet.commitId());
- boolean isMerge = isMerge(projectName, rw, targetPatchSet);
+ boolean isMerge = isMerge(projectName, repoView.getRevWalk(), targetPatchSet);
logger.atFine().log(
"change kind for patch set %d of change %d against prior patch set %s is %s",
targetPatchSet.id().get(),
@@ -464,8 +461,7 @@ public class ApprovalCopier {
priorPsa.value(),
changeKind,
isMerge,
- rw,
- repoConfig);
+ repoView);
if (approvalCopyResult.canCopy()) {
if (!currentApprovalsByUser.contains(priorPsa.label(), priorPsa.accountId())) {
PatchSetApproval copiedApproval = priorPsa.copyWithPatchSet(targetPatchSet.id());
diff --git a/java/com/google/gerrit/server/approval/ApprovalsUtil.java b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
index e64c2735c6..468c7fd691 100644
--- a/java/com/google/gerrit/server/approval/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
@@ -35,10 +35,10 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
-import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AttentionSetUpdate;
@@ -70,6 +70,7 @@ import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
import com.google.gerrit.server.query.approval.UserInPredicate;
+import com.google.gerrit.server.update.RepoView;
import com.google.gerrit.server.util.AccountTemplateUtil;
import com.google.gerrit.server.util.LabelVote;
import com.google.gerrit.server.util.ManualRequestContext;
@@ -89,8 +90,6 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.StringTokenizer;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.revwalk.RevWalk;
/**
* Utility functions to manipulate patchset approvals.
@@ -314,6 +313,7 @@ public class ApprovalsUtil {
* @param user user adding approvals.
* @param approvals approvals to add.
*/
+ @CanIgnoreReturnValue
public Iterable<PatchSetApproval> addApprovalsForNewPatchSet(
ChangeUpdate update,
LabelTypes labelTypes,
@@ -394,20 +394,15 @@ public class ApprovalsUtil {
*
* @param notes the change notes
* @param patchSet the newly created patch set
- * @param revWalk {@link RevWalk} that can see the new patch set revision
- * @param repoConfig the repo config
+ * @param repoView repo view
* @param changeUpdate changeUpdate that is used to persist the copied approvals and update the
* attention set
* @return the result of the approval copying
*/
public ApprovalCopier.Result copyApprovalsToNewPatchSet(
- ChangeNotes notes,
- PatchSet patchSet,
- RevWalk revWalk,
- Config repoConfig,
- ChangeUpdate changeUpdate) {
+ ChangeNotes notes, PatchSet patchSet, RepoView repoView, ChangeUpdate changeUpdate) {
ApprovalCopier.Result approvalCopierResult =
- approvalCopier.forPatchSet(notes, patchSet, revWalk, repoConfig);
+ approvalCopier.forPatchSet(notes, patchSet, repoView);
approvalCopierResult
.copiedApprovals()
.forEach(approvalData -> changeUpdate.putCopiedApproval(approvalData.patchSetApproval()));
@@ -428,7 +423,7 @@ public class ApprovalsUtil {
ChangeUpdate changeUpdate, ImmutableSet<PatchSetApproval> outdatedApprovals) {
Set<AttentionSetUpdate> updates = new HashSet<>();
- Multimap<Account.Id, PatchSetApproval> outdatedApprovalsByUser = ArrayListMultimap.create();
+ ListMultimap<Account.Id, PatchSetApproval> outdatedApprovalsByUser = ArrayListMultimap.create();
outdatedApprovals.forEach(psa -> outdatedApprovalsByUser.put(psa.accountId(), psa));
for (Map.Entry<Account.Id, Collection<PatchSetApproval>> e :
outdatedApprovalsByUser.asMap().entrySet()) {
@@ -862,7 +857,8 @@ public class ApprovalsUtil {
* deleted labels.
*/
public Iterable<PatchSetApproval> byPatchSet(ChangeNotes notes, PatchSet.Id psId) {
- List<PatchSetApproval> approvalsNotNormalized = notes.load().getApprovals().all().get(psId);
+ ImmutableList<PatchSetApproval> approvalsNotNormalized =
+ notes.load().getApprovals().all().get(psId);
return labelNormalizer.normalize(notes, approvalsNotNormalized).getNormalized();
}
diff --git a/java/com/google/gerrit/server/approval/package-info.java b/java/com/google/gerrit/server/approval/package-info.java
new file mode 100644
index 0000000000..29d11bbd14
--- /dev/null
+++ b/java/com/google/gerrit/server/approval/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.approval;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/approval/testing/package-info.java b/java/com/google/gerrit/server/approval/testing/package-info.java
new file mode 100644
index 0000000000..5bec0dd0de
--- /dev/null
+++ b/java/com/google/gerrit/server/approval/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.approval.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/args4j/ListTagSortOptionHandler.java b/java/com/google/gerrit/server/args4j/ListTagSortOptionHandler.java
new file mode 100644
index 0000000000..9359ca1ac9
--- /dev/null
+++ b/java/com/google/gerrit/server/args4j/ListTagSortOptionHandler.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.args4j;
+
+import static com.google.gerrit.util.cli.Localizable.localizable;
+
+import com.google.gerrit.extensions.common.ListTagSortOption;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.Parameters;
+import org.kohsuke.args4j.spi.Setter;
+
+public class ListTagSortOptionHandler extends OptionHandler<ListTagSortOption> {
+ @Inject
+ public ListTagSortOptionHandler(
+ @Assisted CmdLineParser parser,
+ @Assisted OptionDef option,
+ @Assisted Setter<ListTagSortOption> setter) {
+ super(parser, option, setter);
+ }
+
+ @Override
+ public int parseArguments(Parameters params) throws CmdLineException {
+ String param = params.getParameter(0);
+ try {
+ setter.addValue(ListTagSortOption.valueOf(param.toUpperCase()));
+ return 1;
+ } catch (IllegalArgumentException e) {
+ throw new CmdLineException(
+ owner, localizable("\"%s\" is not a valid sort option: %s"), param, e.getMessage());
+ }
+ }
+
+ @Override
+ public String getDefaultMetaVariable() {
+ return "SORT";
+ }
+}
diff --git a/java/com/google/gerrit/server/args4j/package-info.java b/java/com/google/gerrit/server/args4j/package-info.java
new file mode 100644
index 0000000000..56499eefab
--- /dev/null
+++ b/java/com/google/gerrit/server/args4j/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.args4j;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/audit/BUILD b/java/com/google/gerrit/server/audit/BUILD
index 654996a149..a9526d3e9d 100644
--- a/java/com/google/gerrit/server/audit/BUILD
+++ b/java/com/google/gerrit/server/audit/BUILD
@@ -15,6 +15,7 @@ java_library(
"//lib:servlet-api",
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
],
diff --git a/java/com/google/gerrit/server/audit/group/package-info.java b/java/com/google/gerrit/server/audit/group/package-info.java
new file mode 100644
index 0000000000..af318ca69b
--- /dev/null
+++ b/java/com/google/gerrit/server/audit/group/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.audit.group;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/audit/package-info.java b/java/com/google/gerrit/server/audit/package-info.java
new file mode 100644
index 0000000000..6ae2570e50
--- /dev/null
+++ b/java/com/google/gerrit/server/audit/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.audit;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/auth/openid/package-info.java b/java/com/google/gerrit/server/auth/openid/package-info.java
new file mode 100644
index 0000000000..a516fc5cbe
--- /dev/null
+++ b/java/com/google/gerrit/server/auth/openid/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.auth.openid;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/auth/package-info.java b/java/com/google/gerrit/server/auth/package-info.java
new file mode 100644
index 0000000000..28395e1884
--- /dev/null
+++ b/java/com/google/gerrit/server/auth/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.auth;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/avatar/package-info.java b/java/com/google/gerrit/server/avatar/package-info.java
new file mode 100644
index 0000000000..8fed9db46e
--- /dev/null
+++ b/java/com/google/gerrit/server/avatar/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.avatar;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/cache/CacheBinding.java b/java/com/google/gerrit/server/cache/CacheBinding.java
index 99db64e8f1..8249948c2f 100644
--- a/java/com/google/gerrit/server/cache/CacheBinding.java
+++ b/java/com/google/gerrit/server/cache/CacheBinding.java
@@ -16,17 +16,21 @@ package com.google.gerrit.server.cache;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.Weigher;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.time.Duration;
/** Configure a cache declared within a {@link CacheModule} instance. */
public interface CacheBinding<K, V> {
/** Set the total size of the cache. */
+ @CanIgnoreReturnValue
CacheBinding<K, V> maximumWeight(long weight);
/** Set the time an element lives after last write before being expired. */
+ @CanIgnoreReturnValue
CacheBinding<K, V> expireAfterWrite(Duration duration);
/** Set the time an element lives after last access before being expired. */
+ @CanIgnoreReturnValue
CacheBinding<K, V> expireFromMemoryAfterAccess(Duration duration);
/**
@@ -34,12 +38,15 @@ public interface CacheBinding<K, V> {
* {@link #expireAfterWrite(Duration)} will still be returned, but on access a task is queued to
* refresh their value asynchronously.
*/
+ @CanIgnoreReturnValue
CacheBinding<K, V> refreshAfterWrite(Duration duration);
/** Populate the cache with items from the CacheLoader. */
+ @CanIgnoreReturnValue
CacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> clazz);
/** Algorithm to weigh an object with a method other than the unit weight 1. */
+ @CanIgnoreReturnValue
CacheBinding<K, V> weigher(Class<? extends Weigher<K, V>> clazz);
/**
@@ -47,5 +54,6 @@ public interface CacheBinding<K, V> {
*
* @see CacheDef#configKey()
*/
+ @CanIgnoreReturnValue
CacheBinding<K, V> configKey(String configKey);
}
diff --git a/java/com/google/gerrit/server/cache/CacheMetrics.java b/java/com/google/gerrit/server/cache/CacheMetrics.java
index f1fd4a8354..27a75eb0a3 100644
--- a/java/com/google/gerrit/server/cache/CacheMetrics.java
+++ b/java/com/google/gerrit/server/cache/CacheMetrics.java
@@ -29,7 +29,6 @@ import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.logging.Metadata;
import com.google.inject.Inject;
import com.google.inject.Singleton;
-import java.util.Set;
import org.eclipse.jgit.lib.Config;
@Singleton
@@ -73,7 +72,7 @@ public class CacheMetrics {
new Description("Disk hit ratio for persistent cache").setGauge().setUnit("percent"),
F_NAME);
- Set<CallbackMetric<?>> cacheMetrics =
+ ImmutableSet<CallbackMetric<?>> cacheMetrics =
ImmutableSet.of(memEnt, memHit, memEvict, perDiskEnt, perDiskHit);
metrics.newTrigger(
diff --git a/java/com/google/gerrit/server/cache/CacheModule.java b/java/com/google/gerrit/server/cache/CacheModule.java
index d4e4509d9a..85c5cc5628 100644
--- a/java/com/google/gerrit/server/cache/CacheModule.java
+++ b/java/com/google/gerrit/server/cache/CacheModule.java
@@ -18,6 +18,7 @@ import com.google.common.cache.Cache;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.Weigher;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.server.cache.serialize.JavaCacheSerializer;
@@ -44,6 +45,7 @@ public abstract class CacheModule extends FactoryModule {
* @param <V> type of value stored by the cache.
* @return binding to describe the cache.
*/
+ @CanIgnoreReturnValue
protected <K, V> CacheBinding<K, V> cache(String name, Class<K> keyType, Class<V> valType) {
return cache(name, TypeLiteral.get(keyType), TypeLiteral.get(valType));
}
@@ -55,6 +57,7 @@ public abstract class CacheModule extends FactoryModule {
* @param <V> type of value stored by the cache.
* @return binding to describe the cache.
*/
+ @CanIgnoreReturnValue
protected <K, V> CacheBinding<K, V> cache(String name, Class<K> keyType, TypeLiteral<V> valType) {
return cache(name, TypeLiteral.get(keyType), valType);
}
@@ -66,6 +69,7 @@ public abstract class CacheModule extends FactoryModule {
* @param <V> type of value stored by the cache.
* @return binding to describe the cache.
*/
+ @CanIgnoreReturnValue
protected <K, V> CacheBinding<K, V> cache(
String name, TypeLiteral<K> keyType, TypeLiteral<V> valType) {
CacheProvider<K, V> m = new CacheProvider<>(this, name, keyType, valType);
diff --git a/java/com/google/gerrit/server/cache/CacheProvider.java b/java/com/google/gerrit/server/cache/CacheProvider.java
index 94504b66cc..41e4bc7386 100644
--- a/java/com/google/gerrit/server/cache/CacheProvider.java
+++ b/java/com/google/gerrit/server/cache/CacheProvider.java
@@ -21,6 +21,7 @@ import com.google.common.base.Strings;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.Weigher;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.inject.Inject;
@@ -63,6 +64,7 @@ class CacheProvider<K, V> implements Provider<Cache<K, V>>, CacheBinding<K, V>,
}
@Override
+ @CanIgnoreReturnValue
public CacheBinding<K, V> maximumWeight(long weight) {
checkNotFrozen();
maximumWeight = weight;
@@ -70,6 +72,7 @@ class CacheProvider<K, V> implements Provider<Cache<K, V>>, CacheBinding<K, V>,
}
@Override
+ @CanIgnoreReturnValue
public CacheBinding<K, V> expireAfterWrite(Duration duration) {
checkNotFrozen();
expireAfterWrite = duration;
@@ -77,6 +80,7 @@ class CacheProvider<K, V> implements Provider<Cache<K, V>>, CacheBinding<K, V>,
}
@Override
+ @CanIgnoreReturnValue
public CacheBinding<K, V> expireFromMemoryAfterAccess(Duration duration) {
checkNotFrozen();
expireFromMemoryAfterAccess = duration;
@@ -84,6 +88,7 @@ class CacheProvider<K, V> implements Provider<Cache<K, V>>, CacheBinding<K, V>,
}
@Override
+ @CanIgnoreReturnValue
public CacheBinding<K, V> refreshAfterWrite(Duration duration) {
checkNotFrozen();
refreshAfterWrite = duration;
@@ -91,6 +96,7 @@ class CacheProvider<K, V> implements Provider<Cache<K, V>>, CacheBinding<K, V>,
}
@Override
+ @CanIgnoreReturnValue
public CacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> impl) {
checkNotFrozen();
loader = module.bindCacheLoader(this, impl);
@@ -98,6 +104,7 @@ class CacheProvider<K, V> implements Provider<Cache<K, V>>, CacheBinding<K, V>,
}
@Override
+ @CanIgnoreReturnValue
public CacheBinding<K, V> weigher(Class<? extends Weigher<K, V>> impl) {
checkNotFrozen();
weigher = module.bindWeigher(this, impl);
@@ -105,6 +112,7 @@ class CacheProvider<K, V> implements Provider<Cache<K, V>>, CacheBinding<K, V>,
}
@Override
+ @CanIgnoreReturnValue
public CacheBinding<K, V> configKey(String name) {
checkNotFrozen();
configKey = requireNonNull(name);
diff --git a/java/com/google/gerrit/server/cache/PersistentCacheBinding.java b/java/com/google/gerrit/server/cache/PersistentCacheBinding.java
index 5635f44d77..5a2de69849 100644
--- a/java/com/google/gerrit/server/cache/PersistentCacheBinding.java
+++ b/java/com/google/gerrit/server/cache/PersistentCacheBinding.java
@@ -16,26 +16,33 @@ package com.google.gerrit.server.cache;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.Weigher;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.server.cache.serialize.CacheSerializer;
import java.time.Duration;
/** Configure a persistent cache declared within a {@link CacheModule} instance. */
public interface PersistentCacheBinding<K, V> extends CacheBinding<K, V> {
@Override
+ @CanIgnoreReturnValue
PersistentCacheBinding<K, V> maximumWeight(long weight);
@Override
+ @CanIgnoreReturnValue
PersistentCacheBinding<K, V> expireAfterWrite(Duration duration);
@Override
+ @CanIgnoreReturnValue
PersistentCacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> clazz);
@Override
+ @CanIgnoreReturnValue
PersistentCacheBinding<K, V> expireFromMemoryAfterAccess(Duration duration);
@Override
+ @CanIgnoreReturnValue
PersistentCacheBinding<K, V> weigher(Class<? extends Weigher<K, V>> clazz);
+ @CanIgnoreReturnValue
PersistentCacheBinding<K, V> version(int version);
/**
@@ -44,9 +51,12 @@ public interface PersistentCacheBinding<K, V> extends CacheBinding<K, V> {
* <p>If 0 or negative, persistence for the cache is disabled by default, but may still be
* overridden in the config.
*/
+ @CanIgnoreReturnValue
PersistentCacheBinding<K, V> diskLimit(long limit);
+ @CanIgnoreReturnValue
PersistentCacheBinding<K, V> keySerializer(CacheSerializer<K> keySerializer);
+ @CanIgnoreReturnValue
PersistentCacheBinding<K, V> valueSerializer(CacheSerializer<V> valueSerializer);
}
diff --git a/java/com/google/gerrit/server/cache/h2/BUILD b/java/com/google/gerrit/server/cache/h2/BUILD
index 5e64aa765d..722bf12f84 100644
--- a/java/com/google/gerrit/server/cache/h2/BUILD
+++ b/java/com/google/gerrit/server/cache/h2/BUILD
@@ -15,6 +15,7 @@ java_library(
"//lib:guava",
"//lib:h2",
"//lib:jgit",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
],
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index fdd55ac1df..ddfee38c30 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -32,11 +32,13 @@ import com.google.gerrit.server.cache.h2.H2CacheImpl.SqlStore;
import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.ScheduleConfig;
+import com.google.gerrit.server.config.ScheduleConfig.Schedule;
import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.index.options.BuildBloomFilter;
import com.google.gerrit.server.index.options.IsFirstInsertForEntry;
import com.google.gerrit.server.logging.LoggingContextAwareExecutorService;
-import com.google.gerrit.server.logging.LoggingContextAwareScheduledExecutorService;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -67,6 +69,8 @@ class H2CacheFactory extends PersistentCacheBaseFactory implements LifecycleList
private final boolean h2AutoServer;
private final boolean isOfflineReindex;
private final boolean buildBloomFilter;
+ private final boolean pruneOnStartup;
+ private final Schedule schedule;
@Inject
H2CacheFactory(
@@ -74,12 +78,18 @@ class H2CacheFactory extends PersistentCacheBaseFactory implements LifecycleList
@GerritServerConfig Config cfg,
SitePaths site,
DynamicMap<Cache<?, ?>> cacheMap,
+ WorkQueue queue,
@Nullable IsFirstInsertForEntry isFirstInsertForEntry,
@Nullable BuildBloomFilter buildBloomFilter) {
super(memCacheFactory, cfg, site);
h2CacheSize = cfg.getLong("cache", null, "h2CacheSize", -1);
h2AutoServer = cfg.getBoolean("cache", null, "h2AutoServer", false);
+ pruneOnStartup = cfg.getBoolean("cachePruning", null, "pruneOnStartup", true);
caches = new ArrayList<>();
+ schedule =
+ ScheduleConfig.createSchedule(cfg, "cachePruning")
+ .orElseGet(() -> Schedule.createOrFail(Duration.ofDays(1).toMillis(), "01:00"));
+ logger.atInfo().log("Scheduling cache pruning with schedule %s", schedule);
this.cacheMap = cacheMap;
this.isOfflineReindex =
isFirstInsertForEntry != null && isFirstInsertForEntry.equals(IsFirstInsertForEntry.YES);
@@ -92,16 +102,7 @@ class H2CacheFactory extends PersistentCacheBaseFactory implements LifecycleList
Executors.newFixedThreadPool(
1, new ThreadFactoryBuilder().setNameFormat("DiskCache-Store-%d").build()));
- cleanup =
- isOfflineReindex
- ? null
- : new LoggingContextAwareScheduledExecutorService(
- Executors.newScheduledThreadPool(
- 1,
- new ThreadFactoryBuilder()
- .setNameFormat("DiskCache-Prune-%d")
- .setDaemon(true)
- .build()));
+ cleanup = isOfflineReindex ? null : queue.createQueue(1, "DiskCache-Prune", true);
} else {
executor = null;
cleanup = null;
@@ -114,9 +115,19 @@ class H2CacheFactory extends PersistentCacheBaseFactory implements LifecycleList
for (H2CacheImpl<?, ?> cache : caches) {
executor.execute(cache::start);
if (cleanup != null) {
+ if (pruneOnStartup) {
+ @SuppressWarnings("unused")
+ Future<?> possiblyIgnoredError =
+ cleanup.schedule(() -> cache.prune(), 30, TimeUnit.SECONDS);
+ }
+
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError =
- cleanup.schedule(() -> cache.prune(cleanup), 30, TimeUnit.SECONDS);
+ cleanup.scheduleAtFixedRate(
+ () -> cache.prune(),
+ schedule.initialDelay(),
+ schedule.interval(),
+ TimeUnit.MILLISECONDS);
}
}
}
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 27a09ed70d..a86994650f 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -47,7 +47,6 @@ import java.sql.Timestamp;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
-import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -56,9 +55,6 @@ import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
-import java.util.concurrent.Future;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
@@ -92,6 +88,7 @@ public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> implements Per
private final SqlStore<K, V> store;
private final TypeLiteral<K> keyType;
private final Cache<K, ValueHolder<V>> mem;
+ private final String cacheName;
H2CacheImpl(
Executor executor,
@@ -102,6 +99,7 @@ public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> implements Per
this.store = store;
this.keyType = keyType;
this.mem = mem;
+ this.cacheName = store.url.substring(store.url.lastIndexOf('/') + 1);
}
@Nullable
@@ -230,20 +228,10 @@ public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> implements Per
store.close();
}
- void prune(ScheduledExecutorService service) {
+ void prune() {
+ logger.atFine().log("Pruning cache %s...", cacheName);
store.prune(mem);
-
- Calendar cal = Calendar.getInstance();
- cal.set(Calendar.HOUR_OF_DAY, 01);
- cal.set(Calendar.MINUTE, 0);
- cal.set(Calendar.SECOND, 0);
- cal.set(Calendar.MILLISECOND, 0);
- cal.add(Calendar.DAY_OF_MONTH, 1);
-
- long delay = cal.getTimeInMillis() - TimeUtil.nowMs();
- @SuppressWarnings("unused")
- Future<?> possiblyIgnoredError =
- service.schedule(() -> prune(service), delay, TimeUnit.MILLISECONDS);
+ logger.atFine().log("Finished pruning cache %s...", cacheName);
}
static class ValueHolder<V> {
@@ -431,7 +419,7 @@ public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> implements Per
@Nullable
private BloomFilter<K> buildBloomFilter() {
SqlHandle c = null;
- try {
+ try (TraceTimer ignored = TraceContext.newTimer("Build bloom filter", Metadata.empty())) {
c = acquire();
if (estimatedSize <= 0) {
try (PreparedStatement ps =
@@ -773,6 +761,8 @@ public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> implements Per
"ALTER TABLE data ADD COLUMN IF NOT EXISTS "
+ "space BIGINT AS OCTET_LENGTH(k) + OCTET_LENGTH(v)");
stmt.addBatch("ALTER TABLE data ADD COLUMN IF NOT EXISTS version INT DEFAULT 0 NOT NULL");
+ stmt.addBatch("CREATE INDEX IF NOT EXISTS version_key ON data(version, k)");
+ stmt.addBatch("CREATE INDEX IF NOT EXISTS accessed ON data(accessed)");
stmt.executeBatch();
}
}
diff --git a/java/com/google/gerrit/server/cache/h2/package-info.java b/java/com/google/gerrit/server/cache/h2/package-info.java
new file mode 100644
index 0000000000..ea541d3d7a
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/h2/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.cache.h2;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/cache/mem/BUILD b/java/com/google/gerrit/server/cache/mem/BUILD
index a666df7bc8..a0cc079953 100644
--- a/java/com/google/gerrit/server/cache/mem/BUILD
+++ b/java/com/google/gerrit/server/cache/mem/BUILD
@@ -12,6 +12,7 @@ java_library(
"//lib:caffeine-guava",
"//lib:guava",
"//lib:jgit",
+ "//lib/errorprone:annotations",
"//lib/guice",
],
)
diff --git a/java/com/google/gerrit/server/cache/mem/package-info.java b/java/com/google/gerrit/server/cache/mem/package-info.java
new file mode 100644
index 0000000000..b8abd10204
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/mem/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.cache.mem;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/cache/package-info.java b/java/com/google/gerrit/server/cache/package-info.java
new file mode 100644
index 0000000000..4779c73c7c
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.cache;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/cache/serialize/BUILD b/java/com/google/gerrit/server/cache/serialize/BUILD
index aa9106bfb8..c77db8968f 100644
--- a/java/com/google/gerrit/server/cache/serialize/BUILD
+++ b/java/com/google/gerrit/server/cache/serialize/BUILD
@@ -11,5 +11,6 @@ java_library(
"//lib:guava",
"//lib:jgit",
"//lib:protobuf",
+ "//lib/errorprone:annotations",
],
)
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/BUILD b/java/com/google/gerrit/server/cache/serialize/entities/BUILD
index 55080e87e7..ead40c537e 100644
--- a/java/com/google/gerrit/server/cache/serialize/entities/BUILD
+++ b/java/com/google/gerrit/server/cache/serialize/entities/BUILD
@@ -11,6 +11,7 @@ java_library(
"//lib:guava",
"//lib:jgit",
"//lib:protobuf",
+ "//lib/errorprone:annotations",
"//proto:cache_java_proto",
],
)
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/package-info.java b/java/com/google/gerrit/server/cache/serialize/entities/package-info.java
new file mode 100644
index 0000000000..0737d1b205
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/entities/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.cache.serialize.entities;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/cache/serialize/package-info.java b/java/com/google/gerrit/server/cache/serialize/package-info.java
new file mode 100644
index 0000000000..05df98183f
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.cache.serialize;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/cache/testing/BUILD b/java/com/google/gerrit/server/cache/testing/BUILD
index 09f698c125..b823187329 100644
--- a/java/com/google/gerrit/server/cache/testing/BUILD
+++ b/java/com/google/gerrit/server/cache/testing/BUILD
@@ -8,5 +8,6 @@ java_library(
visibility = ["//visibility:public"],
deps = [
"//lib:protobuf",
+ "//lib/errorprone:annotations",
],
)
diff --git a/java/com/google/gerrit/server/cache/testing/package-info.java b/java/com/google/gerrit/server/cache/testing/package-info.java
new file mode 100644
index 0000000000..ca051504f3
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.cache.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/cancellation/BUILD b/java/com/google/gerrit/server/cancellation/BUILD
index 40557b12a2..c48456c3a2 100644
--- a/java/com/google/gerrit/server/cancellation/BUILD
+++ b/java/com/google/gerrit/server/cancellation/BUILD
@@ -10,5 +10,6 @@ java_library(
"//java/com/google/gerrit/common:annotations",
"//lib:guava",
"//lib/commons:text",
+ "//lib/errorprone:annotations",
],
)
diff --git a/java/com/google/gerrit/server/cancellation/RequestStateContext.java b/java/com/google/gerrit/server/cancellation/RequestStateContext.java
index 390c76f222..39cd441007 100644
--- a/java/com/google/gerrit/server/cancellation/RequestStateContext.java
+++ b/java/com/google/gerrit/server/cancellation/RequestStateContext.java
@@ -16,6 +16,7 @@ package com.google.gerrit.server.cancellation;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.HashSet;
import java.util.Set;
@@ -136,6 +137,7 @@ public class RequestStateContext implements AutoCloseable {
* @param requestStateProvider the {@link RequestStateProvider} that should be registered
* @return the {@code RequestStateContext} instance for chaining calls
*/
+ @CanIgnoreReturnValue
public RequestStateContext addRequestStateProvider(RequestStateProvider requestStateProvider) {
if (threadLocalRequestStateProviders.get() == null) {
threadLocalRequestStateProviders.set(new HashSet<>());
diff --git a/java/com/google/gerrit/server/cancellation/package-info.java b/java/com/google/gerrit/server/cancellation/package-info.java
new file mode 100644
index 0000000000..ac09229c3e
--- /dev/null
+++ b/java/com/google/gerrit/server/cancellation/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.cancellation;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/change/AbandonUtil.java b/java/com/google/gerrit/server/change/AbandonUtil.java
index 67344348f8..07280ba5da 100644
--- a/java/com/google/gerrit/server/change/AbandonUtil.java
+++ b/java/com/google/gerrit/server/change/AbandonUtil.java
@@ -14,8 +14,10 @@
package com.google.gerrit.server.change;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Project;
import com.google.gerrit.exceptions.StorageException;
@@ -40,7 +42,7 @@ public class AbandonUtil {
private final ChangeCleanupConfig cfg;
private final Provider<ChangeQueryProcessor> queryProvider;
- private final ChangeQueryBuilder queryBuilder;
+ private final Supplier<ChangeQueryBuilder> queryBuilderSupplier;
private final BatchAbandon batchAbandon;
private final InternalUser internalUser;
@@ -49,11 +51,11 @@ public class AbandonUtil {
ChangeCleanupConfig cfg,
InternalUser.Factory internalUserFactory,
Provider<ChangeQueryProcessor> queryProvider,
- ChangeQueryBuilder queryBuilder,
+ Provider<ChangeQueryBuilder> queryBuilderProvider,
BatchAbandon batchAbandon) {
this.cfg = cfg;
this.queryProvider = queryProvider;
- this.queryBuilder = queryBuilder;
+ this.queryBuilderSupplier = Suppliers.memoize(queryBuilderProvider::get);
this.batchAbandon = batchAbandon;
internalUser = internalUserFactory.create();
}
@@ -70,8 +72,12 @@ public class AbandonUtil {
query += " -is:mergeable";
}
- List<ChangeData> changesToAbandon =
- queryProvider.get().enforceVisibility(false).query(queryBuilder.parse(query)).entities();
+ ImmutableList<ChangeData> changesToAbandon =
+ queryProvider
+ .get()
+ .enforceVisibility(false)
+ .query(queryBuilderSupplier.get().parse(query))
+ .entities();
ImmutableListMultimap.Builder<Project.NameKey, ChangeData> builder =
ImmutableListMultimap.builder();
for (ChangeData cd : changesToAbandon) {
@@ -79,10 +85,10 @@ public class AbandonUtil {
}
int count = 0;
- ListMultimap<Project.NameKey, ChangeData> abandons = builder.build();
+ ImmutableListMultimap<Project.NameKey, ChangeData> abandons = builder.build();
String message = cfg.getAbandonMessage();
for (Project.NameKey project : abandons.keySet()) {
- Collection<ChangeData> changes = getValidChanges(abandons.get(project), query);
+ List<ChangeData> changes = getValidChanges(abandons.get(project), query);
try {
batchAbandon.batchAbandon(updateFactory, project, internalUser, changes, message);
count += changes.size();
@@ -102,16 +108,16 @@ public class AbandonUtil {
}
}
- private Collection<ChangeData> getValidChanges(Collection<ChangeData> changes, String query)
+ private List<ChangeData> getValidChanges(Collection<ChangeData> changes, String query)
throws QueryParseException {
List<ChangeData> validChanges = new ArrayList<>();
for (ChangeData cd : changes) {
String newQuery = query + " change:" + cd.getId();
- List<ChangeData> changesToAbandon =
+ ImmutableList<ChangeData> changesToAbandon =
queryProvider
.get()
.enforceVisibility(false)
- .query(queryBuilder.parse(newQuery))
+ .query(queryBuilderSupplier.get().parse(newQuery))
.entities();
if (!changesToAbandon.isEmpty()) {
validChanges.add(cd);
diff --git a/java/com/google/gerrit/server/change/ActionJson.java b/java/com/google/gerrit/server/change/ActionJson.java
index 52230badad..950d39056f 100644
--- a/java/com/google/gerrit/server/change/ActionJson.java
+++ b/java/com/google/gerrit/server/change/ActionJson.java
@@ -31,6 +31,9 @@ import com.google.gerrit.extensions.webui.PrivateInternals_UiActionDescription;
import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.extensions.webui.UiActions;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -179,38 +182,43 @@ public class ActionJson {
private Map<String, ActionInfo> toActionMap(
ChangeData changeData, List<ActionVisitor> visitors, ChangeInfo changeInfo) {
- CurrentUser user = userProvider.get();
- Map<String, ActionInfo> out = new LinkedHashMap<>();
- if (!user.isIdentifiedUser()) {
- return out;
- }
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Get actions",
+ Metadata.builder().changeId(changeData.change().getId().get()).build())) {
+ CurrentUser user = userProvider.get();
+ Map<String, ActionInfo> out = new LinkedHashMap<>();
+ if (!user.isIdentifiedUser()) {
+ return out;
+ }
- Iterable<UiAction.Description> descs =
- uiActions.from(changeViews, changeResourceFactory.create(changeData, user));
-
- // The followup action is a client-side only operation that does not
- // have a server side handler. It must be manually registered into the
- // resulting action map.
- if (!changeData.change().isAbandoned()) {
- UiAction.Description descr = new UiAction.Description();
- PrivateInternals_UiActionDescription.setId(descr, "followup");
- PrivateInternals_UiActionDescription.setMethod(descr, "POST");
- descr.setTitle("Create follow-up change");
- descr.setLabel("Follow-Up");
- descs = Iterables.concat(descs, Collections.singleton(descr));
- }
+ Iterable<UiAction.Description> descs =
+ uiActions.from(changeViews, changeResourceFactory.create(changeData, user));
+
+ // The followup action is a client-side only operation that does not
+ // have a server side handler. It must be manually registered into the
+ // resulting action map.
+ if (!changeData.change().isAbandoned()) {
+ UiAction.Description descr = new UiAction.Description();
+ PrivateInternals_UiActionDescription.setId(descr, "followup");
+ PrivateInternals_UiActionDescription.setMethod(descr, "POST");
+ descr.setTitle("Create follow-up change");
+ descr.setLabel("Follow-Up");
+ descs = Iterables.concat(descs, Collections.singleton(descr));
+ }
- ACTION:
- for (UiAction.Description d : descs) {
- ActionInfo actionInfo = new ActionInfo(d);
- for (ActionVisitor visitor : visitors) {
- if (!visitor.visit(d.getId(), actionInfo, changeInfo)) {
- continue ACTION;
+ ACTION:
+ for (UiAction.Description d : descs) {
+ ActionInfo actionInfo = new ActionInfo(d);
+ for (ActionVisitor visitor : visitors) {
+ if (!visitor.visit(d.getId(), actionInfo, changeInfo)) {
+ continue ACTION;
+ }
}
+ out.put(d.getId(), actionInfo);
}
- out.put(d.getId(), actionInfo);
+ return out;
}
- return out;
}
private ImmutableMap<String, ActionInfo> toActionMap(
diff --git a/java/com/google/gerrit/server/change/AddReviewersOp.java b/java/com/google/gerrit/server/change/AddReviewersOp.java
index cbbd01a8a1..66909114d2 100644
--- a/java/com/google/gerrit/server/change/AddReviewersOp.java
+++ b/java/com/google/gerrit/server/change/AddReviewersOp.java
@@ -86,9 +86,9 @@ public class AddReviewersOp extends ReviewerOp {
// Unlike addedCCs, addedReviewers is a PatchSetApproval because the ReviewerResult returned
// via the REST API is supposed to include vote information.
private List<PatchSetApproval> addedReviewers = ImmutableList.of();
- private Collection<Address> addedReviewersByEmail = ImmutableList.of();
+ private ImmutableList<Address> addedReviewersByEmail = ImmutableList.of();
private Collection<Account.Id> addedCCs = ImmutableList.of();
- private Collection<Address> addedCCsByEmail = ImmutableList.of();
+ private ImmutableList<Address> addedCCsByEmail = ImmutableList.of();
private Change change;
diff --git a/java/com/google/gerrit/server/change/AddToAttentionSetOp.java b/java/com/google/gerrit/server/change/AddToAttentionSetOp.java
index f0a70bbe64..8403ae3fc7 100644
--- a/java/com/google/gerrit/server/change/AddToAttentionSetOp.java
+++ b/java/com/google/gerrit/server/change/AddToAttentionSetOp.java
@@ -17,6 +17,8 @@ package com.google.gerrit.server.change;
import static com.google.gerrit.server.mail.send.AttentionSetChangeEmailDecorator.AttentionSetChange.USER_ADDED;
import static java.util.Objects.requireNonNull;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.Change;
@@ -44,6 +46,7 @@ public class AddToAttentionSetOp implements BatchUpdateOp {
private Change change;
private boolean notify;
+ @Nullable private AttentionSetUpdateCondition condition;
/**
* Add a specified user to the attention set.
@@ -66,13 +69,25 @@ public class AddToAttentionSetOp implements BatchUpdateOp {
this.notify = notify;
}
+ /** Sets a condition for performing this attention set update. */
+ @CanIgnoreReturnValue
+ public AddToAttentionSetOp setCondition(AttentionSetUpdateCondition condition) {
+ this.condition = condition;
+ return this;
+ }
+
@Override
public boolean updateChange(ChangeContext ctx) throws RestApiException {
+ if (condition != null && !condition.check()) {
+ return false;
+ }
+
ChangeData changeData = changeDataFactory.create(ctx.getNotes());
if (changeData.attentionSet().stream()
.anyMatch(
u ->
u.account().equals(attentionUserId)
+ && u.reason().equals(reason)
&& u.operation() == AttentionSetUpdate.Operation.ADD)) {
// We still need to perform this update to ensure that we don't remove the user in a follow-up
// operation, but no need to send an email about it.
diff --git a/java/com/google/gerrit/server/change/ArchiveFormatInternal.java b/java/com/google/gerrit/server/change/ArchiveFormatInternal.java
index 0ed1f118bf..b5ac87f5c8 100644
--- a/java/com/google/gerrit/server/change/ArchiveFormatInternal.java
+++ b/java/com/google/gerrit/server/change/ArchiveFormatInternal.java
@@ -63,8 +63,8 @@ public enum ArchiveFormatInternal {
return format.suffixes();
}
- public ArchiveOutputStream createArchiveOutputStream(OutputStream o) throws IOException {
- return (ArchiveOutputStream) this.format.createArchiveOutputStream(o);
+ public ArchiveOutputStream<?> createArchiveOutputStream(OutputStream o) throws IOException {
+ return (ArchiveOutputStream<?>) this.format.createArchiveOutputStream(o);
}
public <T extends Closeable> void putEntry(T out, String path, byte[] data) throws IOException {
diff --git a/java/com/google/gerrit/server/change/AttentionSetUpdateCondition.java b/java/com/google/gerrit/server/change/AttentionSetUpdateCondition.java
new file mode 100644
index 0000000000..b275bd39b7
--- /dev/null
+++ b/java/com/google/gerrit/server/change/AttentionSetUpdateCondition.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.change;
+
+/**
+ * Condition to be checked by {@link AddToAttentionSetOp} and {@link RemoveFromAttentionSetOp}
+ * before performing an attention set update.
+ */
+@FunctionalInterface
+public interface AttentionSetUpdateCondition {
+ /**
+ * Checks whether the condition is fulfilled and the attention set update should be performed.
+ *
+ * @return {@code true} if the attention set should be updated, {@code false} if the attention set
+ * should not be updated
+ */
+ boolean check();
+}
diff --git a/java/com/google/gerrit/server/change/ChangeCleanupRunner.java b/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
index a080d15bc5..2f2cff98a4 100644
--- a/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
+++ b/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
@@ -79,14 +79,16 @@ public class ChangeCleanupRunner implements Runnable {
// abandonInactiveOpenChanges skips failures instead of throwing, so retrying will never
// actually happen. For the purposes of this class that is fine: they'll get tried again the
// next time the scheduled task is run.
- retryHelper
- .changeUpdate(
- "abandonInactiveOpenChanges",
- updateFactory -> {
- abandonUtil.abandonInactiveOpenChanges(updateFactory);
- return null;
- })
- .call();
+ @SuppressWarnings("unused")
+ var unused =
+ retryHelper
+ .changeUpdate(
+ "abandonInactiveOpenChanges",
+ updateFactory -> {
+ abandonUtil.abandonInactiveOpenChanges(updateFactory);
+ return null;
+ })
+ .call();
} catch (RestApiException | UpdateException e) {
logger.atSevere().withCause(e).log("Failed to cleanup changes.");
}
diff --git a/java/com/google/gerrit/server/change/ChangeFinder.java b/java/com/google/gerrit/server/change/ChangeFinder.java
index 9f253de2ab..5668c27902 100644
--- a/java/com/google/gerrit/server/change/ChangeFinder.java
+++ b/java/com/google/gerrit/server/change/ChangeFinder.java
@@ -208,6 +208,12 @@ public class ChangeFinder {
return Optional.of(notes.get(0));
}
+ /**
+ * @deprecated this method is not reliable in Gerrit instances with imported changes, since
+ * multiple changes can have the same change number and make the `changeIdProjectCache` cache
+ * pointless.
+ */
+ @Deprecated(since = "3.10", forRemoval = true)
public List<ChangeNotes> find(Change.Id id) {
String project = changeIdProjectCache.getIfPresent(id);
if (project != null) {
@@ -245,7 +251,7 @@ public class ChangeFinder {
// this case.)
Set<Change.Id> seen = Sets.newHashSetWithExpectedSize(cds.size());
for (ChangeData cd : cds) {
- if (seen.add(cd.getId())) {
+ if (seen.add(cd.virtualId())) {
try {
notes.add(cd.notes());
} catch (NoSuchChangeException e) {
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index f32b2eb58f..ef36bd4656 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -72,6 +72,7 @@ import com.google.gerrit.server.mail.send.OutgoingEmail;
import com.google.gerrit.server.mail.send.StartReviewChangeEmailDecorator;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.patch.AutoMerger;
+import com.google.gerrit.server.patch.DiffOperationsForCommitValidation;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -125,6 +126,7 @@ public class ChangeInserter implements InsertChangeOp {
private final MessageIdGenerator messageIdGenerator;
private final AutoMerger autoMerger;
private final ChangeUtil changeUtil;
+ private final DiffOperationsForCommitValidation.Factory diffOperationsForCommitValidationFactory;
private final Change.Id changeId;
private final PatchSet.Id psId;
@@ -179,6 +181,7 @@ public class ChangeInserter implements InsertChangeOp {
MessageIdGenerator messageIdGenerator,
AutoMerger autoMerger,
ChangeUtil changeUtil,
+ DiffOperationsForCommitValidation.Factory diffOperationsForCommitValidationFactory,
@Assisted Change.Id changeId,
@Assisted ObjectId commitId,
@Assisted String refName) {
@@ -198,6 +201,7 @@ public class ChangeInserter implements InsertChangeOp {
this.messageIdGenerator = messageIdGenerator;
this.autoMerger = autoMerger;
this.changeUtil = changeUtil;
+ this.diffOperationsForCommitValidationFactory = diffOperationsForCommitValidationFactory;
this.changeId = changeId;
this.psId = PatchSet.id(changeId, INITIAL_PATCH_SET_ID);
@@ -451,10 +455,7 @@ public class ChangeInserter implements InsertChangeOp {
ctx.addRefUpdate(cmd);
Optional<ReceiveCommand> autoMerge =
autoMerger.createAutoMergeCommitIfNecessary(
- ctx.getRepoView(),
- ctx.getRevWalk(),
- ctx.getInserter(),
- ctx.getRevWalk().parseCommit(commitId));
+ ctx.getRepoView(), ctx.getInserter(), ctx.getRevWalk().parseCommit(commitId));
if (autoMerge.isPresent()) {
ctx.addRefUpdate(autoMerge.get());
}
@@ -652,7 +653,9 @@ public class ChangeInserter implements InsertChangeOp {
ctx.getRepoView().getConfig(),
ctx.getRevWalk().getObjectReader(),
commitId,
- ctx.getIdentifiedUser())) {
+ ctx.getIdentifiedUser(),
+ diffOperationsForCommitValidationFactory.create(
+ ctx.getRepoView(), ctx.getInserter()))) {
commitValidatorsFactory
.forGerritCommits(
permissionBackend.user(ctx.getUser()).project(ctx.getProject()),
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 2fce4755b5..0abe9cf27e 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -53,6 +53,7 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Address;
@@ -62,7 +63,6 @@ import com.google.gerrit.entities.LegacySubmitRequirement;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.entities.SubmitRecord.Status;
import com.google.gerrit.entities.SubmitRequirementResult;
@@ -85,7 +85,6 @@ import com.google.gerrit.extensions.common.SubmitRecordInfo;
import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
import com.google.gerrit.extensions.common.TrackingIdInfo;
import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.index.RefState;
import com.google.gerrit.index.query.QueryResult;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Description.Units;
@@ -106,6 +105,9 @@ import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
@@ -296,6 +298,7 @@ public class ChangeJson {
logger.atFine().log("options = %s", options);
}
+ @CanIgnoreReturnValue
public ChangeJson fix(FixInput fix) {
this.fix = fix;
return this;
@@ -374,33 +377,47 @@ public class ChangeJson {
}
private static List<LegacySubmitRequirementInfo> requirementsFor(ChangeData cd) {
- List<LegacySubmitRequirementInfo> reqInfos = new ArrayList<>();
- for (SubmitRecord submitRecord : cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)) {
- if (submitRecord.requirements == null) {
- continue;
- }
- for (LegacySubmitRequirement requirement : submitRecord.requirements) {
- reqInfos.add(requirementToInfo(requirement, submitRecord.status));
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Get requirements", Metadata.builder().changeId(cd.change().getId().get()).build())) {
+ List<LegacySubmitRequirementInfo> reqInfos = new ArrayList<>();
+ for (SubmitRecord submitRecord : cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)) {
+ if (submitRecord.requirements == null) {
+ continue;
+ }
+ for (LegacySubmitRequirement requirement : submitRecord.requirements) {
+ reqInfos.add(requirementToInfo(requirement, submitRecord.status));
+ }
}
+ return reqInfos;
}
- return reqInfos;
}
private List<SubmitRecordInfo> submitRecordsFor(ChangeData cd) {
- List<SubmitRecordInfo> submitRecordInfos = new ArrayList<>();
- for (SubmitRecord record : cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)) {
- submitRecordInfos.add(submitRecordToInfo(record));
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Get submit records", Metadata.builder().changeId(cd.change().getId().get()).build())) {
+ List<SubmitRecordInfo> submitRecordInfos = new ArrayList<>();
+ for (SubmitRecord record : cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)) {
+ submitRecordInfos.add(submitRecordToInfo(record));
+ }
+ return submitRecordInfos;
}
- return submitRecordInfos;
}
private List<SubmitRequirementResultInfo> submitRequirementsFor(ChangeData cd) {
- List<SubmitRequirementResultInfo> reqInfos = new ArrayList<>();
- cd.submitRequirementsIncludingLegacy().entrySet().stream()
- .filter(entry -> !entry.getValue().isHidden())
- .forEach(
- entry -> reqInfos.add(SubmitRequirementsJson.toInfo(entry.getKey(), entry.getValue())));
- return reqInfos;
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Get submit requirements",
+ Metadata.builder().changeId(cd.change().getId().get()).build())) {
+ List<SubmitRequirementResultInfo> reqInfos = new ArrayList<>();
+ cd.submitRequirementsIncludingLegacy().entrySet().stream()
+ .filter(entry -> !entry.getValue().isHidden())
+ .forEach(
+ entry ->
+ reqInfos.add(SubmitRequirementsJson.toInfo(entry.getKey(), entry.getValue())));
+ return reqInfos;
+ }
}
private static LegacySubmitRequirementInfo requirementToInfo(
@@ -475,25 +492,30 @@ public class ChangeJson {
}
}
- private void ensureLoaded(Iterable<ChangeData> all) {
+ private void ensureLoaded(Collection<ChangeData> all) {
if (lazyLoad) {
- for (ChangeData cd : all) {
- // Mark all ChangeDatas as coming from the index, but allow backfilling data from NoteDb
- cd.setStorageConstraint(ChangeData.StorageConstraint.INDEX_PRIMARY_NOTEDB_SECONDARY);
- }
- ChangeData.ensureChangeLoaded(all);
- if (has(ALL_REVISIONS)) {
- ChangeData.ensureAllPatchSetsLoaded(all);
- } else if (has(CURRENT_REVISION) || has(MESSAGES)) {
- ChangeData.ensureCurrentPatchSetLoaded(all);
- }
- if (has(REVIEWED) && userProvider.get().isIdentifiedUser()) {
- ChangeData.ensureReviewedByLoadedForOpenChanges(all);
- }
- if (has(STAR) && userProvider.get().isIdentifiedUser()) {
- ChangeData.ensureChangeServerId(all);
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Load change data for lazyLoad options",
+ Metadata.builder().resourceCount(all.size()).build())) {
+ for (ChangeData cd : all) {
+ // Mark all ChangeDatas as coming from the index, but allow backfilling data from NoteDb
+ cd.setStorageConstraint(ChangeData.StorageConstraint.INDEX_PRIMARY_NOTEDB_SECONDARY);
+ }
+ ChangeData.ensureChangeLoaded(all);
+ if (has(ALL_REVISIONS)) {
+ ChangeData.ensureAllPatchSetsLoaded(all);
+ } else if (has(CURRENT_REVISION) || has(MESSAGES)) {
+ ChangeData.ensureCurrentPatchSetLoaded(all);
+ }
+ if (has(REVIEWED) && userProvider.get().isIdentifiedUser()) {
+ ChangeData.ensureReviewedByLoadedForOpenChanges(all);
+ }
+ if (has(STAR) && userProvider.get().isIdentifiedUser()) {
+ ChangeData.ensureChangeServerId(all);
+ }
+ ChangeData.ensureCurrentApprovalsLoaded(all);
}
- ChangeData.ensureCurrentApprovalsLoaded(all);
} else {
for (ChangeData cd : all) {
// Mark all ChangeDatas as coming from the index. Disallow using NoteDb
@@ -528,7 +550,8 @@ public class ChangeJson {
}
continue;
}
- ChangeInfo info = cache.get(cd.getId());
+ Change.Id cdUniqueId = cd.virtualId();
+ ChangeInfo info = cache.get(cdUniqueId);
if (info != null && isCacheable) {
changeInfos.add(info);
continue;
@@ -540,7 +563,7 @@ public class ChangeJson {
info = format(cd, Optional.empty(), false, pluginInfosByChange.get(cd.getId()));
changeInfos.add(info);
if (isCacheable) {
- cache.put(Change.id(info._number), info);
+ cache.put(cdUniqueId, info);
}
} catch (RuntimeException e) {
Optional<RequestCancelledException> requestCancelledException =
@@ -619,10 +642,12 @@ public class ChangeJson {
if (has(CHECK)) {
out.problems = checkerProvider.get().check(cd.notes(), fix).problems();
// If any problems were fixed, the ChangeData needs to be reloaded.
- for (ProblemInfo p : out.problems) {
- if (p.status == ProblemInfo.Status.FIXED) {
+ if (out.problems.stream().anyMatch(p -> p.status == ProblemInfo.Status.FIXED)) {
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Reload change data after fixing a problem",
+ Metadata.builder().changeId(cd.change().getChangeId()).build())) {
cd = changeDataFactory.create(cd.project(), cd.getId());
- break;
}
}
}
@@ -630,6 +655,7 @@ public class ChangeJson {
Change in = cd.change();
out.project = in.getProject().get();
out.branch = in.getDest().shortName();
+ out.currentRevisionNumber = in.currentPatchSetId().get();
out.topic = in.getTopic();
if (!cd.attentionSet().isEmpty()) {
out.removedFromAttentionSet =
@@ -679,28 +705,18 @@ public class ChangeJson {
out.setCreated(in.getCreatedOn());
out.setUpdated(in.getLastUpdatedOn());
out._number = in.getId().get();
- out.totalCommentCount = cd.totalCommentCount();
- out.unresolvedCommentCount = cd.unresolvedCommentCount();
-
- if (cd.getRefStates() != null) {
- String metaName = RefNames.changeMetaRef(cd.getId());
- Optional<RefState> metaState =
- cd.getRefStates().values().stream().filter(r -> r.ref().equals(metaName)).findAny();
- // metaState should always be there, but it doesn't hurt to be extra careful.
- metaState.ifPresent(rs -> out.metaRevId = rs.id().getName());
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Count comments", Metadata.builder().changeId(cd.change().getId().get()).build())) {
+ out.totalCommentCount = cd.totalCommentCount();
+ out.unresolvedCommentCount = cd.unresolvedCommentCount();
}
- if (user.isIdentifiedUser()) {
- if (cd.isStarred(user.getAccountId())) {
- out.starred = true;
- }
- }
-
- if (in.isNew() && has(REVIEWED) && user.isIdentifiedUser()) {
- out.reviewed = cd.isReviewedBy(user.getAccountId()) ? true : null;
- }
+ getMetaState(cd).ifPresent(id -> out.metaRevId = id.getName());
+ out.reviewed = isReviewedByCurrentUser(cd, user);
+ out.starred = isStarredByCurrentUser(cd, user);
out.labels = labelsJson.labelsFor(accountLoader, cd, has(LABELS), has(DETAILED_LABELS));
out.requirements = requirementsFor(cd);
out.submitRecords = submitRecordsFor(cd);
@@ -747,7 +763,7 @@ public class ChangeJson {
boolean needMessages = has(MESSAGES);
boolean needRevisions = has(ALL_REVISIONS) || has(CURRENT_REVISION) || limitToPsId.isPresent();
- Map<PatchSet.Id, PatchSet> src;
+ ImmutableMap<PatchSet.Id, PatchSet> src;
if (needMessages || needRevisions) {
src = loadPatchSets(cd, limitToPsId);
} else {
@@ -777,11 +793,15 @@ public class ChangeJson {
}
if (has(TRACKING_IDS)) {
- ListMultimap<String, String> set = trackingFooters.extract(cd.commitFooters());
- out.trackingIds =
- set.entries().stream()
- .map(e -> new TrackingIdInfo(e.getKey(), e.getValue()))
- .collect(toList());
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Get tracking IDs", Metadata.builder().changeId(cd.change().getId().get()).build())) {
+ ListMultimap<String, String> set = trackingFooters.extract(cd.commitFooters());
+ out.trackingIds =
+ set.entries().stream()
+ .map(e -> new TrackingIdInfo(e.getKey(), e.getValue()))
+ .collect(toList());
+ }
}
out._virtualIdNumber = cd.virtualId().get();
@@ -791,142 +811,206 @@ public class ChangeJson {
private Map<ReviewerState, Collection<AccountInfo>> reviewerMap(
ReviewerSet reviewers, ReviewerByEmailSet reviewersByEmail, boolean includeRemoved) {
- Map<ReviewerState, Collection<AccountInfo>> reviewerMap = new HashMap<>();
- for (ReviewerStateInternal state : ReviewerStateInternal.values()) {
- if (!includeRemoved && state == ReviewerStateInternal.REMOVED) {
- continue;
- }
- Collection<AccountInfo> reviewersByState = toAccountInfo(reviewers.byState(state));
- reviewersByState.addAll(toAccountInfoByEmail(reviewersByEmail.byState(state)));
- if (!reviewersByState.isEmpty()) {
- reviewerMap.put(state.asReviewerState(), reviewersByState);
+ try (TraceTimer timer = TraceContext.newTimer("Get reviewer map", Metadata.empty())) {
+ Map<ReviewerState, Collection<AccountInfo>> reviewerMap = new HashMap<>();
+ for (ReviewerStateInternal state : ReviewerStateInternal.values()) {
+ if (!includeRemoved && state == ReviewerStateInternal.REMOVED) {
+ continue;
+ }
+ List<AccountInfo> reviewersByState = toAccountInfo(reviewers.byState(state));
+ reviewersByState.addAll(toAccountInfoByEmail(reviewersByEmail.byState(state)));
+ if (!reviewersByState.isEmpty()) {
+ reviewerMap.put(state.asReviewerState(), reviewersByState);
+ }
}
+ return reviewerMap;
}
- return reviewerMap;
}
private List<ReviewerUpdateInfo> reviewerUpdates(ChangeData cd) {
- List<ReviewerStatusUpdate> reviewerUpdates = cd.reviewerUpdates();
- List<ReviewerUpdateInfo> result = new ArrayList<>(reviewerUpdates.size());
- for (ReviewerStatusUpdate c : reviewerUpdates) {
- ReviewerUpdateInfo change =
- new ReviewerUpdateInfo(
- c.date(),
- accountLoader.get(c.updatedBy()),
- accountLoader.get(c.reviewer()),
- c.state().asReviewerState());
- result.add(change);
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Get reviewer updates",
+ Metadata.builder().changeId(cd.change().getId().get()).build())) {
+ List<ReviewerStatusUpdate> reviewerUpdates = cd.reviewerUpdates();
+ List<ReviewerUpdateInfo> result = new ArrayList<>(reviewerUpdates.size());
+ for (ReviewerStatusUpdate c : reviewerUpdates) {
+ if (c.reviewer().isPresent()) {
+ result.add(
+ new ReviewerUpdateInfo(
+ c.date(),
+ accountLoader.get(c.updatedBy()),
+ accountLoader.get(c.reviewer().get()),
+ c.state().asReviewerState()));
+ }
+
+ if (c.reviewerByEmail().isPresent()) {
+ result.add(
+ new ReviewerUpdateInfo(
+ c.date(),
+ accountLoader.get(c.updatedBy()),
+ toAccountInfoByEmail(c.reviewerByEmail().get()),
+ c.state().asReviewerState()));
+ }
+ }
+ return result;
}
- return result;
}
private boolean submittable(ChangeData cd) {
- return cd.submitRequirementsIncludingLegacy().values().stream()
- .allMatch(SubmitRequirementResult::fulfilled);
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Compute submittability",
+ Metadata.builder().changeId(cd.change().getId().get()).build())) {
+ return cd.submitRequirementsIncludingLegacy().values().stream()
+ .allMatch(SubmitRequirementResult::fulfilled);
+ }
+ }
+
+ private Optional<ObjectId> getMetaState(ChangeData cd) {
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Get change meta ref",
+ Metadata.builder().changeId(cd.change().getId().get()).build())) {
+ return cd.metaRevision();
+ }
+ }
+
+ private Boolean isReviewedByCurrentUser(ChangeData cd, CurrentUser user) {
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Get reviewed by", Metadata.builder().changeId(cd.change().getId().get()).build())) {
+ return toBoolean(
+ cd.change().isNew()
+ && has(REVIEWED)
+ && user.isIdentifiedUser()
+ && cd.isReviewedBy(user.getAccountId()));
+ }
+ }
+
+ private Boolean isStarredByCurrentUser(ChangeData cd, CurrentUser user) {
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Get starred by", Metadata.builder().changeId(cd.change().getId().get()).build())) {
+ return toBoolean(user.isIdentifiedUser() && cd.isStarred(user.getAccountId()));
+ }
}
private void setSubmitter(ChangeData cd, ChangeInfo out) {
- Optional<PatchSetApproval> s = cd.getSubmitApproval();
- if (!s.isPresent()) {
- return;
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Set submitter", Metadata.builder().changeId(cd.change().getId().get()).build())) {
+ Optional<PatchSetApproval> s = cd.getSubmitApproval();
+ if (!s.isPresent()) {
+ return;
+ }
+ out.setSubmitted(s.get().granted(), accountLoader.get(s.get().accountId()));
}
- out.setSubmitted(s.get().granted(), accountLoader.get(s.get().accountId()));
}
private ImmutableList<ChangeMessageInfo> messages(ChangeData cd) {
- List<ChangeMessage> messages = cmUtil.byChange(cd.notes());
- if (messages.isEmpty()) {
- return ImmutableList.of();
- }
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Get messages", Metadata.builder().changeId(cd.change().getId().get()).build())) {
+ List<ChangeMessage> messages = cmUtil.byChange(cd.notes());
+ if (messages.isEmpty()) {
+ return ImmutableList.of();
+ }
- List<ChangeMessageInfo> result = Lists.newArrayListWithCapacity(messages.size());
- for (ChangeMessage message : messages) {
- result.add(createChangeMessageInfo(message, accountLoader));
+ List<ChangeMessageInfo> result = Lists.newArrayListWithCapacity(messages.size());
+ for (ChangeMessage message : messages) {
+ result.add(createChangeMessageInfo(message, accountLoader));
+ }
+ return ImmutableList.copyOf(result);
}
- return ImmutableList.copyOf(result);
}
private List<AccountInfo> removableReviewers(ChangeData cd, ChangeInfo out)
throws PermissionBackendException {
- // Although this is called removableReviewers, this method also determines
- // which CCs are removable.
- //
- // For reviewers, we need to look at each approval, because the reviewer
- // should only be considered removable if *all* of their approvals can be
- // removed. First, add all reviewers with *any* removable approval to the
- // "removable" set. Along the way, if we encounter a non-removable approval,
- // add the reviewer to the "fixed" set. Before we return, remove all members
- // of "fixed" from "removable", because not all of their approvals can be
- // removed.
- Collection<LabelInfo> labels = out.labels.values();
- Set<Account.Id> fixed = Sets.newHashSetWithExpectedSize(labels.size());
- Set<Account.Id> removable = new HashSet<>();
-
- // Add all reviewers, which will later be removed if they are in the "fixed" set.
- removable.addAll(
- out.reviewers.getOrDefault(ReviewerState.REVIEWER, Collections.emptySet()).stream()
- .filter(a -> a._accountId != null)
- .map(a -> Account.id(a._accountId))
- .collect(Collectors.toSet()));
-
- // Check if the user has the permission to remove a reviewer. This means we can bypass the
- // testRemoveReviewer check for a specific reviewer in the loop saving potentially many
- // permission checks.
- boolean canRemoveAnyReviewer =
- permissionBackend
- .user(userProvider.get())
- .change(cd)
- .test(ChangePermission.REMOVE_REVIEWER);
- for (LabelInfo label : labels) {
- if (label.all == null) {
- continue;
- }
- for (ApprovalInfo ai : label.all) {
- Account.Id id = Account.id(ai._accountId);
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Get removable reviewers",
+ Metadata.builder().changeId(cd.change().getId().get()).build())) {
+ // Although this is called removableReviewers, this method also determines
+ // which CCs are removable.
+ //
+ // For reviewers, we need to look at each approval, because the reviewer
+ // should only be considered removable if *all* of their approvals can be
+ // removed. First, add all reviewers with *any* removable approval to the
+ // "removable" set. Along the way, if we encounter a non-removable approval,
+ // add the reviewer to the "fixed" set. Before we return, remove all members
+ // of "fixed" from "removable", because not all of their approvals can be
+ // removed.
+ Collection<LabelInfo> labels = out.labels.values();
+ Set<Account.Id> fixed = Sets.newHashSetWithExpectedSize(labels.size());
+ Set<Account.Id> removable = new HashSet<>();
+
+ // Add all reviewers, which will later be removed if they are in the "fixed" set.
+ removable.addAll(
+ out.reviewers.getOrDefault(ReviewerState.REVIEWER, Collections.emptySet()).stream()
+ .filter(a -> a._accountId != null)
+ .map(a -> Account.id(a._accountId))
+ .collect(Collectors.toSet()));
+
+ // Check if the user has the permission to remove a reviewer. This means we can bypass the
+ // testRemoveReviewer check for a specific reviewer in the loop saving potentially many
+ // permission checks.
+ boolean canRemoveAnyReviewer =
+ permissionBackend
+ .user(userProvider.get())
+ .change(cd)
+ .test(ChangePermission.REMOVE_REVIEWER);
+ for (LabelInfo label : labels) {
+ if (label.all == null) {
+ continue;
+ }
+ for (ApprovalInfo ai : label.all) {
+ Account.Id id = Account.id(ai._accountId);
- if (!canRemoveAnyReviewer
- && !removeReviewerControl.testRemoveReviewer(
- cd, userProvider.get(), id, MoreObjects.firstNonNull(ai.value, 0))) {
- fixed.add(id);
+ if (!canRemoveAnyReviewer
+ && !removeReviewerControl.testRemoveReviewer(
+ cd, userProvider.get(), id, MoreObjects.firstNonNull(ai.value, 0))) {
+ fixed.add(id);
+ }
}
}
- }
- // CCs are simpler than reviewers. They are removable if the ChangeControl
- // would permit a non-negative approval by that account to be removed, in
- // which case add them to removable. We don't need to add unremovable CCs to
- // "fixed" because we only visit each CC once here.
- Collection<AccountInfo> ccs = out.reviewers.get(ReviewerState.CC);
- if (ccs != null) {
- for (AccountInfo ai : ccs) {
- if (ai._accountId != null) {
- Account.Id id = Account.id(ai._accountId);
- if (canRemoveAnyReviewer
- || removeReviewerControl.testRemoveReviewer(cd, userProvider.get(), id, 0)) {
- removable.add(id);
+ // CCs are simpler than reviewers. They are removable if the ChangeControl
+ // would permit a non-negative approval by that account to be removed, in
+ // which case add them to removable. We don't need to add unremovable CCs to
+ // "fixed" because we only visit each CC once here.
+ Collection<AccountInfo> ccs = out.reviewers.get(ReviewerState.CC);
+ if (ccs != null) {
+ for (AccountInfo ai : ccs) {
+ if (ai._accountId != null) {
+ Account.Id id = Account.id(ai._accountId);
+ if (canRemoveAnyReviewer
+ || removeReviewerControl.testRemoveReviewer(cd, userProvider.get(), id, 0)) {
+ removable.add(id);
+ }
}
}
}
- }
- // Subtract any reviewers with non-removable approvals from the "removable"
- // set. This also subtracts any CCs that for some reason also hold
- // unremovable approvals.
- removable.removeAll(fixed);
+ // Subtract any reviewers with non-removable approvals from the "removable"
+ // set. This also subtracts any CCs that for some reason also hold
+ // unremovable approvals.
+ removable.removeAll(fixed);
- List<AccountInfo> result = Lists.newArrayListWithCapacity(removable.size());
- for (Account.Id id : removable) {
- result.add(accountLoader.get(id));
- }
- // Reviewers added by email are always removable
- for (Collection<AccountInfo> infos : out.reviewers.values()) {
- for (AccountInfo info : infos) {
- if (info._accountId == null) {
- result.add(info);
+ List<AccountInfo> result = Lists.newArrayListWithCapacity(removable.size());
+ for (Account.Id id : removable) {
+ result.add(accountLoader.get(id));
+ }
+ // Reviewers added by email are always removable
+ for (Collection<AccountInfo> infos : out.reviewers.values()) {
+ for (AccountInfo info : infos) {
+ if (info._accountId == null) {
+ result.add(info);
+ }
}
}
+ return result;
}
- return result;
}
private List<AccountInfo> toAccountInfo(Collection<Account.Id> accounts) {
@@ -936,39 +1020,47 @@ public class ChangeJson {
.collect(toList());
}
+ private AccountInfo toAccountInfoByEmail(Address address) {
+ return new AccountInfo(address.name(), address.email());
+ }
+
private List<AccountInfo> toAccountInfoByEmail(Collection<Address> addresses) {
return addresses.stream()
- .map(a -> new AccountInfo(a.name(), a.email()))
+ .map(this::toAccountInfoByEmail)
.sorted(AccountInfoComparator.ORDER_NULLS_FIRST)
.collect(toList());
}
- private Map<PatchSet.Id, PatchSet> loadPatchSets(
+ private ImmutableMap<PatchSet.Id, PatchSet> loadPatchSets(
ChangeData cd, Optional<PatchSet.Id> limitToPsId) {
- Collection<PatchSet> src;
- if (has(ALL_REVISIONS) || has(MESSAGES)) {
- src = cd.patchSets();
- } else {
- PatchSet ps;
- if (limitToPsId.isPresent()) {
- ps = cd.patchSet(limitToPsId.get());
- if (ps == null) {
- throw new StorageException("missing patch set " + limitToPsId.get());
- }
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Load patch sets", Metadata.builder().changeId(cd.change().getId().get()).build())) {
+ Collection<PatchSet> src;
+ if (has(ALL_REVISIONS) || has(MESSAGES)) {
+ src = cd.patchSets();
} else {
- ps = cd.currentPatchSet();
- if (ps == null) {
- throw new StorageException("missing current patch set for change " + cd.getId());
+ PatchSet ps;
+ if (limitToPsId.isPresent()) {
+ ps = cd.patchSet(limitToPsId.get());
+ if (ps == null) {
+ throw new StorageException("missing patch set " + limitToPsId.get());
+ }
+ } else {
+ ps = cd.currentPatchSet();
+ if (ps == null) {
+ throw new StorageException("missing current patch set for change " + cd.getId());
+ }
}
+ src = Collections.singletonList(ps);
}
- src = Collections.singletonList(ps);
- }
- // Sort by patch set ID in increasing order to have a stable output.
- ImmutableSortedMap.Builder<PatchSet.Id, PatchSet> map = ImmutableSortedMap.naturalOrder();
- for (PatchSet patchSet : src) {
- map.put(patchSet.id(), patchSet);
+ // Sort by patch set ID in increasing order to have a stable output.
+ ImmutableSortedMap.Builder<PatchSet.Id, PatchSet> map = ImmutableSortedMap.naturalOrder();
+ for (PatchSet patchSet : src) {
+ map.put(patchSet.id(), patchSet);
+ }
+ return map.build();
}
- return map.build();
}
/** Populate the 'starred' field. */
@@ -991,14 +1083,18 @@ public class ChangeJson {
}
}
- private List<PluginDefinedInfo> getPluginInfos(ChangeData cd) {
+ private ImmutableList<PluginDefinedInfo> getPluginInfos(ChangeData cd) {
return getPluginInfos(Collections.singleton(cd)).get(cd.getId());
}
private ImmutableListMultimap<Change.Id, PluginDefinedInfo> getPluginInfos(
Collection<ChangeData> cds) {
if (pluginDefinedInfosFactory.isPresent()) {
- return pluginDefinedInfosFactory.get().createPluginDefinedInfos(cds);
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Get plugin infos", Metadata.builder().resourceCount(cds.size()).build())) {
+ return pluginDefinedInfosFactory.get().createPluginDefinedInfos(cds);
+ }
}
return ImmutableListMultimap.of();
}
@@ -1023,4 +1119,9 @@ public class ChangeJson {
info.owner = new AccountInfo(c.getOwner().get());
return Optional.of(info);
}
+
+ @Nullable
+ private static Boolean toBoolean(boolean value) {
+ return value ? true : null;
+ }
}
diff --git a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index 1e149548db..74aa373662 100644
--- a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -21,6 +21,7 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.Cache;
import com.google.common.cache.Weigher;
import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Change;
@@ -46,7 +47,6 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
-import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import org.eclipse.jgit.errors.LargeObjectException;
@@ -272,12 +272,12 @@ public class ChangeKindCacheImpl implements ChangeKindCache {
}
private static boolean sameRestOfParents(RevCommit prior, RevCommit next) {
- Set<RevCommit> priorRestParents = allExceptFirstParent(prior.getParents());
- Set<RevCommit> nextRestParents = allExceptFirstParent(next.getParents());
+ ImmutableSet<RevCommit> priorRestParents = allExceptFirstParent(prior.getParents());
+ ImmutableSet<RevCommit> nextRestParents = allExceptFirstParent(next.getParents());
return priorRestParents.equals(nextRestParents);
}
- private static Set<RevCommit> allExceptFirstParent(RevCommit[] parents) {
+ private static ImmutableSet<RevCommit> allExceptFirstParent(RevCommit[] parents) {
return FluentIterable.from(Arrays.asList(parents)).skip(1).toSet();
}
diff --git a/java/com/google/gerrit/server/change/ChangeResource.java b/java/com/google/gerrit/server/change/ChangeResource.java
index fe7fd8e07d..8300541984 100644
--- a/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/java/com/google/gerrit/server/change/ChangeResource.java
@@ -255,7 +255,15 @@ public class ChangeResource implements RestResource, HasETag {
private void hashAccount(Hasher h, AccountState accountState, byte[] buf) {
h.putInt(accountState.account().id().get());
- h.putString(MoreObjects.firstNonNull(accountState.account().metaId(), ZERO_ID_STRING), UTF_8);
+ // Based on the code, it seems the metaId should never be null in this place and so the
+ // uniqueTag.
+ // However, the null-check for metaId has been existed here for some time already - for safety
+ // the same check is applied to uniqueTag.
+ h.putString(
+ MoreObjects.firstNonNull(
+ accountState.account().uniqueTag(),
+ MoreObjects.firstNonNull(accountState.account().metaId(), ZERO_ID_STRING)),
+ UTF_8);
accountState.externalIds().stream().forEach(e -> hashObjectId(h, e.blobId(), buf));
}
}
diff --git a/java/com/google/gerrit/server/change/CommentThread.java b/java/com/google/gerrit/server/change/CommentThread.java
index 0265f60301..51422b1b51 100644
--- a/java/com/google/gerrit/server/change/CommentThread.java
+++ b/java/com/google/gerrit/server/change/CommentThread.java
@@ -18,6 +18,7 @@ import com.google.auto.value.AutoValue;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Streams;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.HumanComment;
import java.util.List;
@@ -58,6 +59,7 @@ public abstract class CommentThread<T extends Comment> {
public abstract Builder<T> comments(List<T> value);
+ @CanIgnoreReturnValue
public Builder<T> addComment(T comment) {
commentsBuilder().add(comment);
return this;
diff --git a/java/com/google/gerrit/server/change/CommentsValidator.java b/java/com/google/gerrit/server/change/CommentsValidator.java
new file mode 100644
index 0000000000..0cdd9b6c39
--- /dev/null
+++ b/java/com/google/gerrit/server/change/CommentsValidator.java
@@ -0,0 +1,251 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.change;
+
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.toList;
+
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.client.Comment.Range;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
+import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.FixReplacementInfo;
+import com.google.gerrit.extensions.common.FixSuggestionInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.patch.DiffSummary;
+import com.google.gerrit.server.patch.DiffSummaryKey;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.jgit.lib.ObjectId;
+
+@Singleton
+public class CommentsValidator {
+
+ private final CommentsUtil commentsUtil;
+ private final PatchListCache patchListCache;
+
+ @Inject
+ CommentsValidator(CommentsUtil commentsUtil, PatchListCache patchListCache) {
+ this.commentsUtil = commentsUtil;
+ this.patchListCache = patchListCache;
+ }
+
+ public static void ensureFixSuggestionsAreAddable(
+ List<FixSuggestionInfo> fixSuggestionInfos, String commentPath) throws BadRequestException {
+ if (fixSuggestionInfos == null) {
+ return;
+ }
+
+ for (FixSuggestionInfo fixSuggestionInfo : fixSuggestionInfos) {
+ ensureDescriptionIsSet(commentPath, fixSuggestionInfo.description);
+ ensureFixReplacementsAreAddable(commentPath, fixSuggestionInfo.replacements);
+ }
+ }
+
+ public <T extends com.google.gerrit.extensions.client.Comment> void checkComments(
+ RevisionResource revision, Map<String, List<T>> commentsPerPath)
+ throws BadRequestException, PatchListNotAvailableException {
+ Set<String> revisionFilePaths = getAffectedFilePaths(revision);
+ for (Map.Entry<String, List<T>> entry : commentsPerPath.entrySet()) {
+ String path = entry.getKey();
+ PatchSet.Id patchSetId = revision.getPatchSet().id();
+ ensurePathRefersToAvailableOrMagicFile(path, revisionFilePaths, patchSetId);
+
+ List<T> comments = entry.getValue();
+ for (T comment : comments) {
+ ensureLineIsNonNegative(comment.line, path);
+ ensureCommentNotOnMagicFilesOfAutoMerge(path, comment);
+ ensureRangeIsValid(path, comment.range);
+ ensureValidPatchsetLevelComment(path, comment);
+ ensureValidInReplyTo(revision.getNotes(), comment.inReplyTo);
+ ensureFixSuggestionsAreAddable(comment.fixSuggestions, path);
+ }
+ }
+ }
+
+ private void ensureValidInReplyTo(ChangeNotes changeNotes, String inReplyTo)
+ throws BadRequestException {
+ if (inReplyTo != null
+ && !commentsUtil.getPublishedHumanComment(changeNotes, inReplyTo).isPresent()
+ && !commentsUtil.getRobotComment(changeNotes, inReplyTo).isPresent()) {
+ throw new BadRequestException(
+ String.format("Invalid inReplyTo, comment %s not found", inReplyTo));
+ }
+ }
+
+ private Set<String> getAffectedFilePaths(RevisionResource revision)
+ throws PatchListNotAvailableException {
+ ObjectId newId = revision.getPatchSet().commitId();
+ DiffSummaryKey key =
+ DiffSummaryKey.fromPatchListKey(
+ PatchListKey.againstDefaultBase(newId, Whitespace.IGNORE_NONE));
+ DiffSummary ds = patchListCache.getDiffSummary(key, revision.getProject());
+ return new HashSet<>(ds.getPaths());
+ }
+
+ private static void ensurePathRefersToAvailableOrMagicFile(
+ String path, Set<String> availableFilePaths, PatchSet.Id patchSetId)
+ throws BadRequestException {
+ if (!availableFilePaths.contains(path) && !Patch.isMagic(path)) {
+ throw new BadRequestException(
+ String.format("file %s not found in revision %s", path, patchSetId));
+ }
+ }
+
+ private static void ensureLineIsNonNegative(Integer line, String path)
+ throws BadRequestException {
+ if (line != null && line < 0) {
+ throw new BadRequestException(
+ String.format("negative line number %d not allowed on %s", line, path));
+ }
+ }
+
+ private static <T extends com.google.gerrit.extensions.client.Comment>
+ void ensureCommentNotOnMagicFilesOfAutoMerge(String path, T comment)
+ throws BadRequestException {
+ if (Patch.isMagic(path) && comment.side == Side.PARENT && comment.parent == null) {
+ throw new BadRequestException(String.format("cannot comment on %s on auto-merge", path));
+ }
+ }
+
+ private static <T extends com.google.gerrit.extensions.client.Comment>
+ void ensureValidPatchsetLevelComment(String path, T comment) throws BadRequestException {
+ if (path.equals(PATCHSET_LEVEL)
+ && (comment.side != null || comment.range != null || comment.line != null)) {
+ throw new BadRequestException("Patchset-level comments can't have side, range, or line");
+ }
+ }
+
+ private static void ensureFixReplacementsAreAddable(
+ String commentPath, List<FixReplacementInfo> fixReplacementInfos) throws BadRequestException {
+ ensureReplacementsArePresent(commentPath, fixReplacementInfos);
+
+ for (FixReplacementInfo fixReplacementInfo : fixReplacementInfos) {
+ ensureReplacementPathIsSetAndNotPatchsetLevel(commentPath, fixReplacementInfo.path);
+ ensureRangeIsSet(commentPath, fixReplacementInfo.range);
+ ensureRangeIsValid(commentPath, fixReplacementInfo.range);
+ ensureReplacementStringIsSet(commentPath, fixReplacementInfo.replacement);
+ }
+
+ Map<String, List<FixReplacementInfo>> replacementsPerFilePath =
+ fixReplacementInfos.stream().collect(groupingBy(fixReplacement -> fixReplacement.path));
+ for (List<FixReplacementInfo> sameFileReplacements : replacementsPerFilePath.values()) {
+ ensureRangesDoNotOverlap(commentPath, sameFileReplacements);
+ }
+ }
+
+ private static void ensureReplacementsArePresent(
+ String commentPath, List<FixReplacementInfo> fixReplacementInfos) throws BadRequestException {
+ if (fixReplacementInfos == null || fixReplacementInfos.isEmpty()) {
+ throw new BadRequestException(
+ String.format(
+ "At least one replacement is "
+ + "required for the suggested fix of the comment on %s",
+ commentPath));
+ }
+ }
+
+ private static void ensureReplacementPathIsSetAndNotPatchsetLevel(
+ String commentPath, String replacementPath) throws BadRequestException {
+ if (replacementPath == null) {
+ throw new BadRequestException(
+ String.format(
+ "A file path must be given for the replacement of the comment on %s", commentPath));
+ }
+ if (replacementPath.equals(PATCHSET_LEVEL)) {
+ throw new BadRequestException(
+ String.format(
+ "A file path must not be %s for the replacement of the comment on %s",
+ PATCHSET_LEVEL, commentPath));
+ }
+ }
+
+ private static void ensureRangeIsSet(String commentPath, Range range) throws BadRequestException {
+ if (range == null) {
+ throw new BadRequestException(
+ String.format(
+ "A range must be given for the replacement of the comment on %s", commentPath));
+ }
+ }
+
+ private static void ensureRangeIsValid(String commentPath, Range range)
+ throws BadRequestException {
+ if (range == null) {
+ return;
+ }
+ if (!range.isValid()) {
+ throw new BadRequestException(
+ String.format(
+ "Range (%s:%s - %s:%s) is not valid for the comment on %s",
+ range.startLine,
+ range.startCharacter,
+ range.endLine,
+ range.endCharacter,
+ commentPath));
+ }
+ }
+
+ private static void ensureReplacementStringIsSet(String commentPath, String replacement)
+ throws BadRequestException {
+ if (replacement == null) {
+ throw new BadRequestException(
+ String.format(
+ "A content for replacement "
+ + "must be indicated for the replacement of the comment on %s",
+ commentPath));
+ }
+ }
+
+ private static void ensureRangesDoNotOverlap(
+ String commentPath, List<FixReplacementInfo> fixReplacementInfos) throws BadRequestException {
+ List<Range> sortedRanges =
+ fixReplacementInfos.stream()
+ .map(fixReplacementInfo -> fixReplacementInfo.range)
+ .sorted()
+ .collect(toList());
+
+ int previousEndLine = 0;
+ int previousOffset = -1;
+ for (Range range : sortedRanges) {
+ if (range.startLine < previousEndLine
+ || (range.startLine == previousEndLine && range.startCharacter < previousOffset)) {
+ throw new BadRequestException(
+ String.format("Replacements overlap for the comment on %s", commentPath));
+ }
+ previousEndLine = range.endLine;
+ previousOffset = range.endCharacter;
+ }
+ }
+
+ private static void ensureDescriptionIsSet(String commentPath, String description)
+ throws BadRequestException {
+ if (description == null) {
+ throw new BadRequestException(
+ String.format(
+ "A description is required for the suggested fix of the comment on %s", commentPath));
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/change/ConsistencyChecker.java b/java/com/google/gerrit/server/change/ConsistencyChecker.java
index b216db3248..9f7a7fc994 100644
--- a/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -29,6 +29,7 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.SetMultimap;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
@@ -778,6 +779,7 @@ public class ConsistencyChecker {
return null;
}
+ @CanIgnoreReturnValue
private ProblemInfo problem(String msg) {
ProblemInfo p = new ProblemInfo();
p.message = requireNonNull(msg);
diff --git a/java/com/google/gerrit/server/change/DeleteChangeOp.java b/java/com/google/gerrit/server/change/DeleteChangeOp.java
index 435f2f19f8..bcfa48a5f7 100644
--- a/java/com/google/gerrit/server/change/DeleteChangeOp.java
+++ b/java/com/google/gerrit/server/change/DeleteChangeOp.java
@@ -16,6 +16,7 @@ package com.google.gerrit.server.change;
import static com.google.common.flogger.LazyArgs.lazy;
+import com.google.common.collect.ImmutableCollection;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
@@ -75,7 +76,7 @@ public class DeleteChangeOp implements BatchUpdateOp {
// fail gracefully if the second delete fails, but fortunately that's not what happens.
@Override
public boolean updateChange(ChangeContext ctx) throws RestApiException, IOException {
- Collection<PatchSet> patchSets = psUtil.byChange(ctx.getNotes());
+ ImmutableCollection<PatchSet> patchSets = psUtil.byChange(ctx.getNotes());
ensureDeletable(ctx, id, patchSets);
// Cleaning up is only possible as long as the change and its elements are
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
index 90cb9a9523..4f001fb0a4 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.change;
import static com.google.gerrit.server.mail.EmailFactories.REVIEWER_DELETED;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
@@ -229,8 +230,7 @@ public class DeleteReviewerOp extends ReviewerOp {
}
private Iterable<PatchSetApproval> approvals(ChangeContext ctx, Account.Id accountId) {
- Iterable<PatchSetApproval> approvals;
- approvals = ctx.getNotes().getApprovals().all().values();
+ ImmutableCollection<PatchSetApproval> approvals = ctx.getNotes().getApprovals().all().values();
return Iterables.filter(approvals, psa -> accountId.equals(psa.accountId()));
}
diff --git a/java/com/google/gerrit/server/change/DeleteReviewersUtil.java b/java/com/google/gerrit/server/change/DeleteReviewersUtil.java
index 79ed043a63..662ee6ba18 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewersUtil.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewersUtil.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.change;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Address;
import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
@@ -26,7 +27,6 @@ import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.inject.Inject;
import java.io.IOException;
-import java.util.Collection;
import org.eclipse.jgit.errors.ConfigInvalidException;
public class DeleteReviewersUtil {
@@ -78,7 +78,7 @@ public class DeleteReviewersUtil {
throw new ResourceNotFoundException(reviewerInput.reviewer);
}
- private Collection<Account.Id> fetchAccountIds(ChangeNotes changeNotes) {
+ private ImmutableSet<Account.Id> fetchAccountIds(ChangeNotes changeNotes) {
return approvalsUtil.getReviewers(changeNotes).all();
}
}
diff --git a/java/com/google/gerrit/server/change/EmailNewPatchSet.java b/java/com/google/gerrit/server/change/EmailNewPatchSet.java
index b295469f18..30d82a43da 100644
--- a/java/com/google/gerrit/server/change/EmailNewPatchSet.java
+++ b/java/com/google/gerrit/server/change/EmailNewPatchSet.java
@@ -145,7 +145,8 @@ public class EmailNewPatchSet {
try {
asyncSender.run();
} finally {
- threadLocalRequestContext.setContext(old);
+ @SuppressWarnings("unused")
+ var unused = threadLocalRequestContext.setContext(old);
}
});
}
diff --git a/java/com/google/gerrit/server/change/EmailReviewComments.java b/java/com/google/gerrit/server/change/EmailReviewComments.java
index 27e68a8cc8..bb93cd321d 100644
--- a/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -228,7 +228,8 @@ public class EmailReviewComments {
} catch (Exception e) {
logger.atSevere().withCause(e).log("Cannot email comments for %s", patchSet.id());
} finally {
- requestContext.setContext(old);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(old);
}
}
diff --git a/java/com/google/gerrit/server/change/FilterIncludedIn.java b/java/com/google/gerrit/server/change/FilterIncludedIn.java
new file mode 100644
index 0000000000..dbc88f1081
--- /dev/null
+++ b/java/com/google/gerrit/server/change/FilterIncludedIn.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import java.util.function.Predicate;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+/**
+ * Filter branches and tags from the result of Included In.
+ *
+ * <p>Plugins can implement this interface to filter branches and tags from the result of Included
+ * In. The filter is applied after the default filtering and sort.
+ */
+@ExtensionPoint
+public interface FilterIncludedIn {
+
+ /**
+ * Returns a predicate for filtering branches.
+ *
+ * @param project the name of the project
+ * @param commit the commit for which it should do the filtering
+ * @return A predicate that returns true if the branch should be included in the result
+ */
+ public default Predicate<String> getBranchFilter(Project.NameKey project, RevCommit commit) {
+ return branch -> true;
+ }
+
+ /**
+ * Returns a predicate for filtering tags.
+ *
+ * @param project the name of the project
+ * @param commit the commit for which it should do the filtering
+ * @return A predicate that returns true if the tag should be included in the result
+ */
+ public default Predicate<String> getTagFilter(Project.NameKey project, RevCommit commit) {
+ return tag -> true;
+ }
+}
diff --git a/java/com/google/gerrit/server/change/GetRelatedChangesUtil.java b/java/com/google/gerrit/server/change/GetRelatedChangesUtil.java
index 834a623f86..ad7deec774 100644
--- a/java/com/google/gerrit/server/change/GetRelatedChangesUtil.java
+++ b/java/com/google/gerrit/server/change/GetRelatedChangesUtil.java
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import static java.util.stream.Collectors.toSet;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.index.IndexConfig;
@@ -98,7 +99,7 @@ public class GetRelatedChangesUtil {
return Collections.emptyList();
}
- List<ChangeData> cds =
+ ImmutableList<ChangeData> cds =
InternalChangeQuery.byProjectGroups(
queryProvider, indexConfig, changeData.project(), groups);
if (cds.isEmpty()) {
diff --git a/java/com/google/gerrit/server/change/IncludedIn.java b/java/com/google/gerrit/server/change/IncludedIn.java
index bc6579eacd..f104a577a3 100644
--- a/java/com/google/gerrit/server/change/IncludedIn.java
+++ b/java/com/google/gerrit/server/change/IncludedIn.java
@@ -34,6 +34,7 @@ import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.plugincontext.PluginSetContext;
+import com.google.gerrit.server.plugincontext.PluginSetEntryContext;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
@@ -41,6 +42,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.Constants;
@@ -56,15 +58,18 @@ public class IncludedIn {
private final GitRepositoryManager repoManager;
private final PermissionBackend permissionBackend;
private final PluginSetContext<ExternalIncludedIn> externalIncludedIn;
+ private final PluginSetContext<FilterIncludedIn> filterIncludedIn;
@Inject
IncludedIn(
GitRepositoryManager repoManager,
PermissionBackend permissionBackend,
- PluginSetContext<ExternalIncludedIn> externalIncludedIn) {
+ PluginSetContext<ExternalIncludedIn> externalIncludedIn,
+ PluginSetContext<FilterIncludedIn> filterIncludedIn) {
this.repoManager = repoManager;
this.permissionBackend = permissionBackend;
this.externalIncludedIn = externalIncludedIn;
+ this.filterIncludedIn = filterIncludedIn;
}
public IncludedInInfo apply(Project.NameKey project, String revisionId)
@@ -94,13 +99,23 @@ public class IncludedIn {
.collect(Collectors.toSet());
// Filter branches and tags according to their visbility by the user
- ImmutableSortedSet<String> filteredBranches =
+ Stream<String> filteredBranchesStream =
sortedShortNames(
filterReadableRefs(
project, getMatchingRefNames(allMatchingTagsAndBranches, branches)));
- ImmutableSortedSet<String> filteredTags =
+ Stream<String> filteredTagsStream =
sortedShortNames(
filterReadableRefs(project, getMatchingRefNames(allMatchingTagsAndBranches, tags)));
+ for (PluginSetEntryContext<FilterIncludedIn> pluginFilter : filterIncludedIn) {
+ filteredBranchesStream =
+ filteredBranchesStream.filter(pluginFilter.get().getBranchFilter(project, rev));
+ filteredTagsStream =
+ filteredTagsStream.filter(pluginFilter.get().getTagFilter(project, rev));
+ }
+ ImmutableSortedSet<String> filteredBranches =
+ filteredBranchesStream.collect(toImmutableSortedSet(naturalOrder()));
+ ImmutableSortedSet<String> filteredTags =
+ filteredTagsStream.collect(toImmutableSortedSet(naturalOrder()));
ListMultimap<String, String> external = MultimapBuilder.hashKeys().arrayListValues().build();
externalIncludedIn.runEach(
@@ -146,9 +161,7 @@ public class IncludedIn {
.collect(toImmutableList());
}
- private ImmutableSortedSet<String> sortedShortNames(Collection<String> refs) {
- return refs.stream()
- .map(Repository::shortenRefName)
- .collect(toImmutableSortedSet(naturalOrder()));
+ private Stream<String> sortedShortNames(Collection<String> refs) {
+ return refs.stream().map(Repository::shortenRefName).sorted(naturalOrder());
}
}
diff --git a/java/com/google/gerrit/server/change/LabelsJson.java b/java/com/google/gerrit/server/change/LabelsJson.java
index e4118d5c03..ac22453edd 100644
--- a/java/com/google/gerrit/server/change/LabelsJson.java
+++ b/java/com/google/gerrit/server/change/LabelsJson.java
@@ -43,6 +43,9 @@ import com.google.gerrit.extensions.common.VotingRangeInfo;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.permissions.LabelPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
@@ -99,12 +102,16 @@ public class LabelsJson {
return null;
}
- LabelTypes labelTypes = cd.getLabelTypes();
- Map<String, LabelWithStatus> withStatus =
- cd.change().isMerged()
- ? labelsForSubmittedChange(accountLoader, cd, labelTypes, standard, detailed)
- : labelsForUnsubmittedChange(accountLoader, cd, labelTypes, standard, detailed);
- return ImmutableMap.copyOf(Maps.transformValues(withStatus, LabelWithStatus::label));
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Get labels", Metadata.builder().changeId(cd.change().getId().get()).build())) {
+ LabelTypes labelTypes = cd.getLabelTypes();
+ Map<String, LabelWithStatus> withStatus =
+ cd.change().isMerged()
+ ? labelsForSubmittedChange(accountLoader, cd, labelTypes, standard, detailed)
+ : labelsForUnsubmittedChange(accountLoader, cd, labelTypes, standard, detailed);
+ return ImmutableMap.copyOf(Maps.transformValues(withStatus, LabelWithStatus::label));
+ }
}
/**
@@ -118,30 +125,36 @@ public class LabelsJson {
*/
Map<String, Collection<String>> permittedLabels(Account.Id filterApprovalsBy, ChangeData cd)
throws PermissionBackendException {
- SetMultimap<String, String> permitted = LinkedHashMultimap.create();
- boolean isMerged = cd.change().isMerged();
- Map<String, Short> currentUserVotes = currentLabels(filterApprovalsBy, cd);
- for (LabelType labelType : cd.getLabelTypes().getLabelTypes()) {
- if (isMerged && !labelType.isAllowPostSubmit()) {
- continue;
- }
- Set<LabelPermission.WithValue> can =
- permissionBackend.absentUser(filterApprovalsBy).change(cd).test(labelType);
- for (LabelValue v : labelType.getValues()) {
- boolean ok = can.contains(new LabelPermission.WithValue(labelType, v));
- if (isMerged) {
- // Votes cannot be decreased if the change is merged. Only accept the label value if it's
- // greater or equal than the user's latest vote.
- short prev = currentUserVotes.getOrDefault(labelType.getName(), (short) 0);
- ok &= v.getValue() >= prev;
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Get permitted labels",
+ Metadata.builder().changeId(cd.change().getId().get()).build())) {
+ SetMultimap<String, String> permitted = LinkedHashMultimap.create();
+ boolean isMerged = cd.change().isMerged();
+ Map<String, Short> currentUserVotes = currentLabels(filterApprovalsBy, cd);
+ for (LabelType labelType : cd.getLabelTypes().getLabelTypes()) {
+ if (isMerged && !labelType.isAllowPostSubmit()) {
+ continue;
}
- if (ok) {
- permitted.put(labelType.getName(), v.formatValue());
+ Set<LabelPermission.WithValue> can =
+ permissionBackend.absentUser(filterApprovalsBy).change(cd).test(labelType);
+ for (LabelValue v : labelType.getValues()) {
+ boolean ok = can.contains(new LabelPermission.WithValue(labelType, v));
+ if (isMerged) {
+ // Votes cannot be decreased if the change is merged. Only accept the label value if
+ // it's
+ // greater or equal than the user's latest vote.
+ short prev = currentUserVotes.getOrDefault(labelType.getName(), (short) 0);
+ ok &= v.getValue() >= prev;
+ }
+ if (ok) {
+ permitted.put(labelType.getName(), v.formatValue());
+ }
}
}
+ clearOnlyZerosEntries(permitted);
+ return permitted.asMap();
}
- clearOnlyZerosEntries(permitted);
- return permitted.asMap();
}
/**
@@ -156,32 +169,37 @@ public class LabelsJson {
Map<String, Map<String, List<AccountInfo>>> removableLabels(
AccountLoader accountLoader, CurrentUser user, ChangeData cd)
throws PermissionBackendException {
- if (cd.change().isMerged()) {
- return new HashMap<>();
- }
-
- Map<String, Map<String, List<AccountInfo>>> res = new HashMap<>();
- LabelTypes labelTypes = cd.getLabelTypes();
- for (PatchSetApproval approval : cd.currentApprovals()) {
- Optional<LabelType> labelType = labelTypes.byLabel(approval.labelId());
- if (!labelType.isPresent()) {
- continue;
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Get removable labels",
+ Metadata.builder().changeId(cd.change().getId().get()).build())) {
+ if (cd.change().isMerged()) {
+ return new HashMap<>();
}
- if (!(deleteVoteControl.testDeleteVotePermissions(user, cd, approval, labelType.get())
- || removeReviewerControl.testRemoveReviewer(
- cd, user, approval.accountId(), approval.value()))) {
- continue;
- }
- if (!res.containsKey(approval.label())) {
- res.put(approval.label(), new HashMap<>());
- }
- String labelValue = LabelValue.formatValue(approval.value());
- if (!res.get(approval.label()).containsKey(labelValue)) {
- res.get(approval.label()).put(labelValue, new ArrayList<>());
+
+ Map<String, Map<String, List<AccountInfo>>> res = new HashMap<>();
+ LabelTypes labelTypes = cd.getLabelTypes();
+ for (PatchSetApproval approval : cd.currentApprovals()) {
+ Optional<LabelType> labelType = labelTypes.byLabel(approval.labelId());
+ if (!labelType.isPresent()) {
+ continue;
+ }
+ if (!(deleteVoteControl.testDeleteVotePermissions(user, cd, approval, labelType.get())
+ || removeReviewerControl.testRemoveReviewer(
+ cd, user, approval.accountId(), approval.value()))) {
+ continue;
+ }
+ if (!res.containsKey(approval.label())) {
+ res.put(approval.label(), new HashMap<>());
+ }
+ String labelValue = LabelValue.formatValue(approval.value());
+ if (!res.get(approval.label()).containsKey(labelValue)) {
+ res.get(approval.label()).put(labelValue, new ArrayList<>());
+ }
+ res.get(approval.label()).get(labelValue).add(accountLoader.get(approval.accountId()));
}
- res.get(approval.label()).get(labelValue).add(accountLoader.get(approval.accountId()));
+ return res;
}
- return res;
}
private static void clearOnlyZerosEntries(SetMultimap<String, String> permitted) {
diff --git a/java/com/google/gerrit/server/change/ParentDataProvider.java b/java/com/google/gerrit/server/change/ParentDataProvider.java
index 48ab59d2b6..c0a1ffe6c5 100644
--- a/java/com/google/gerrit/server/change/ParentDataProvider.java
+++ b/java/com/google/gerrit/server/change/ParentDataProvider.java
@@ -98,12 +98,14 @@ public class ParentDataProvider {
private Optional<ParentCommitData> getFromGerritChange(
Project.NameKey project, ObjectId parentCommitId, String targetBranch) {
List<ChangeData> changeData = queryProvider.get().byCommit(parentCommitId.name());
- if (changeData.size() != 1) {
+ if (changeData.size() > 1) {
logger.atWarning().log(
- "Did not find a single change associated with parent revision %s (project: %s). Found changes %s.",
+ "Found more than one change associated with parent revision %s (project: %s). Found changes %s.",
parentCommitId.name(),
project.get(),
changeData.stream().map(ChangeData::getId).collect(ImmutableList.toImmutableList()));
+ }
+ if (changeData.size() != 1) {
return Optional.empty();
}
ChangeData singleData = changeData.get(0);
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index 854fd4ec3a..3b0f6fbc53 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -23,6 +23,7 @@ import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
@@ -47,6 +48,7 @@ import com.google.gerrit.server.git.validators.TopicValidator;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.patch.AutoMerger;
+import com.google.gerrit.server.patch.DiffOperationsForCommitValidation;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
@@ -86,6 +88,7 @@ public class PatchSetInserter implements BatchUpdateOp {
private final WorkInProgressStateChanged wipStateChanged;
private final AutoMerger autoMerger;
private final TopicValidator topicValidator;
+ private final DiffOperationsForCommitValidation.Factory diffOperationsForCommitValidationFactory;
// Assisted-injected fields.
private final PatchSet.Id psId;
@@ -135,6 +138,7 @@ public class PatchSetInserter implements BatchUpdateOp {
WorkInProgressStateChanged wipStateChanged,
AutoMerger autoMerger,
TopicValidator topicValidator,
+ DiffOperationsForCommitValidation.Factory diffOperationsForCommitValidationFactory,
@Assisted ChangeNotes notes,
@Assisted PatchSet.Id psId,
@Assisted ObjectId commitId) {
@@ -151,6 +155,7 @@ public class PatchSetInserter implements BatchUpdateOp {
this.wipStateChanged = wipStateChanged;
this.autoMerger = autoMerger;
this.topicValidator = topicValidator;
+ this.diffOperationsForCommitValidationFactory = diffOperationsForCommitValidationFactory;
this.origNotes = notes;
this.psId = psId;
@@ -161,37 +166,44 @@ public class PatchSetInserter implements BatchUpdateOp {
return psId;
}
+ @CanIgnoreReturnValue
public PatchSetInserter setMessage(String message) {
this.message = message;
return this;
}
+ @CanIgnoreReturnValue
public PatchSetInserter setDescription(String description) {
this.description = description;
return this;
}
+ @CanIgnoreReturnValue
public PatchSetInserter setWorkInProgress(boolean workInProgress) {
this.workInProgress = workInProgress;
return this;
}
+ @CanIgnoreReturnValue
public PatchSetInserter setValidate(boolean validate) {
this.validate = validate;
return this;
}
+ @CanIgnoreReturnValue
public PatchSetInserter setCheckAddPatchSetPermission(boolean checkAddPatchSetPermission) {
this.checkAddPatchSetPermission = checkAddPatchSetPermission;
return this;
}
+ @CanIgnoreReturnValue
public PatchSetInserter setGroups(List<String> groups) {
requireNonNull(groups, "groups may not be null");
this.groups = groups;
return this;
}
+ @CanIgnoreReturnValue
public PatchSetInserter setValidationOptions(
ImmutableListMultimap<String, String> validationOptions) {
requireNonNull(validationOptions, "validationOptions may not be null");
@@ -199,21 +211,25 @@ public class PatchSetInserter implements BatchUpdateOp {
return this;
}
+ @CanIgnoreReturnValue
public PatchSetInserter setFireRevisionCreated(boolean fireRevisionCreated) {
this.fireRevisionCreated = fireRevisionCreated;
return this;
}
+ @CanIgnoreReturnValue
public PatchSetInserter setAllowClosed(boolean allowClosed) {
this.allowClosed = allowClosed;
return this;
}
+ @CanIgnoreReturnValue
public PatchSetInserter setSendEmail(boolean sendEmail) {
this.sendEmail = sendEmail;
return this;
}
+ @CanIgnoreReturnValue
public PatchSetInserter setTopic(String topic) {
this.topic = topic;
return this;
@@ -225,6 +241,7 @@ public class PatchSetInserter implements BatchUpdateOp {
* cases, we already store the votes of the new patch-sets in SubmitStrategyOp#saveApprovals. We
* should not also store the copied votes.
*/
+ @CanIgnoreReturnValue
public PatchSetInserter setStoreCopiedVotes(boolean storeCopiedVotes) {
this.storeCopiedVotes = storeCopiedVotes;
return this;
@@ -256,10 +273,7 @@ public class PatchSetInserter implements BatchUpdateOp {
Optional<ReceiveCommand> autoMerge =
autoMerger.createAutoMergeCommitIfNecessary(
- ctx.getRepoView(),
- ctx.getRevWalk(),
- ctx.getInserter(),
- ctx.getRevWalk().parseCommit(commitId));
+ ctx.getRepoView(), ctx.getInserter(), ctx.getRevWalk().parseCommit(commitId));
if (autoMerge.isPresent()) {
ctx.addRefUpdate(autoMerge.get());
}
@@ -320,7 +334,7 @@ public class PatchSetInserter implements BatchUpdateOp {
if (storeCopiedVotes) {
approvalCopierResult =
approvalsUtil.copyApprovalsToNewPatchSet(
- ctx.getNotes(), patchSet, ctx.getRevWalk(), ctx.getRepoView().getConfig(), update);
+ ctx.getNotes(), patchSet, ctx.getRepoView(), update);
}
mailMessage = insertChangeMessage(update, ctx);
@@ -424,7 +438,9 @@ public class PatchSetInserter implements BatchUpdateOp {
ctx.getRepoView().getConfig(),
ctx.getRevWalk().getObjectReader(),
commitId,
- ctx.getIdentifiedUser())) {
+ ctx.getIdentifiedUser(),
+ diffOperationsForCommitValidationFactory.create(
+ ctx.getRepoView(), ctx.getInserter()))) {
commitValidatorsFactory
.forGerritCommits(
permissionBackend.user(ctx.getUser()).project(ctx.getProject()),
diff --git a/java/com/google/gerrit/server/change/ReaddOwnerUtil.java b/java/com/google/gerrit/server/change/ReaddOwnerUtil.java
index afbe30b3bb..f42a8433f8 100644
--- a/java/com/google/gerrit/server/change/ReaddOwnerUtil.java
+++ b/java/com/google/gerrit/server/change/ReaddOwnerUtil.java
@@ -16,8 +16,10 @@ package com.google.gerrit.server.change;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Project;
@@ -38,7 +40,6 @@ import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
-import java.util.List;
import java.util.concurrent.TimeUnit;
@Singleton
@@ -47,7 +48,7 @@ public class ReaddOwnerUtil {
private final AttentionSetConfig cfg;
private final Provider<ChangeQueryProcessor> queryProvider;
- private final ChangeQueryBuilder queryBuilder;
+ private final Supplier<ChangeQueryBuilder> queryBuilderSupplier;
private final AddToAttentionSetOp.Factory opFactory;
private final ServiceUserClassifier serviceUserClassifier;
private final InternalUser internalUser;
@@ -56,13 +57,13 @@ public class ReaddOwnerUtil {
ReaddOwnerUtil(
AttentionSetConfig cfg,
Provider<ChangeQueryProcessor> queryProvider,
- ChangeQueryBuilder queryBuilder,
+ Provider<ChangeQueryBuilder> queryBuilderProvider,
AddToAttentionSetOp.Factory opFactory,
ServiceUserClassifier serviceUserClassifier,
InternalUser.Factory internalUserFactory) {
this.cfg = cfg;
this.queryProvider = queryProvider;
- this.queryBuilder = queryBuilder;
+ this.queryBuilderSupplier = Suppliers.memoize(queryBuilderProvider::get);
this.opFactory = opFactory;
this.serviceUserClassifier = serviceUserClassifier;
internalUser = internalUserFactory.create();
@@ -80,8 +81,12 @@ public class ReaddOwnerUtil {
+ TimeUnit.MILLISECONDS.toMinutes(cfg.getReaddAfter())
+ "m";
- List<ChangeData> changesToAddOwner =
- queryProvider.get().enforceVisibility(false).query(queryBuilder.parse(query)).entities();
+ ImmutableList<ChangeData> changesToAddOwner =
+ queryProvider
+ .get()
+ .enforceVisibility(false)
+ .query(queryBuilderSupplier.get().parse(query))
+ .entities();
ImmutableListMultimap.Builder<Project.NameKey, ChangeData> builder =
ImmutableListMultimap.builder();
@@ -89,7 +94,7 @@ public class ReaddOwnerUtil {
builder.put(cd.project(), cd);
}
- ListMultimap<Project.NameKey, ChangeData> ownerAdds = builder.build();
+ ImmutableListMultimap<Project.NameKey, ChangeData> ownerAdds = builder.build();
int ownersAdded = 0;
for (Project.NameKey project : ownerAdds.keySet()) {
try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
diff --git a/java/com/google/gerrit/server/change/RebaseChangeOp.java b/java/com/google/gerrit/server/change/RebaseChangeOp.java
index de3b7d5660..5daac75abd 100644
--- a/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -23,7 +23,10 @@ import static java.util.Objects.requireNonNull;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
@@ -42,7 +45,9 @@ import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.git.GroupCollector;
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.git.MergeUtilFactory;
+import com.google.gerrit.server.logging.CallerFinder;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
@@ -56,6 +61,7 @@ import com.google.gerrit.server.util.AccountTemplateUtil;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -80,6 +86,8 @@ import org.eclipse.jgit.revwalk.RevWalk;
* RevWalk, org.eclipse.jgit.lib.ObjectInserter)}).
*/
public class RebaseChangeOp implements BatchUpdateOp {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
public interface Factory {
RebaseChangeOp create(ChangeNotes notes, PatchSet originalPatchSet, ObjectId baseCommitId);
@@ -91,6 +99,7 @@ public class RebaseChangeOp implements BatchUpdateOp {
private final RebaseUtil rebaseUtil;
private final ChangeResource.Factory changeResourceFactory;
private final ChangeNotes.Factory notesFactory;
+ private final CallerFinder callerFinder;
private final ChangeNotes notes;
private final PatchSet originalPatchSet;
@@ -113,6 +122,7 @@ public class RebaseChangeOp implements BatchUpdateOp {
private boolean matchAuthorToCommitterDate = false;
private ImmutableListMultimap<String, String> validationOptions = ImmutableListMultimap.of();
private String mergeStrategy;
+ private boolean verifyNeedsRebase = true;
private CodeReviewCommit rebasedCommit;
private PatchSet.Id rebasedPatchSetId;
@@ -191,28 +201,34 @@ public class RebaseChangeOp implements BatchUpdateOp {
this.notes = notes;
this.projectName = notes.getProjectName();
this.originalPatchSet = originalPatchSet;
+ this.callerFinder = CallerFinder.builder().addTarget(RebaseChangeOp.class).build();
}
+ @CanIgnoreReturnValue
public RebaseChangeOp setCommitterIdent(PersonIdent committerIdent) {
this.committerIdent = committerIdent;
return this;
}
+ @CanIgnoreReturnValue
public RebaseChangeOp setValidate(boolean validate) {
this.validate = validate;
return this;
}
+ @CanIgnoreReturnValue
public RebaseChangeOp setCheckAddPatchSetPermission(boolean checkAddPatchSetPermission) {
this.checkAddPatchSetPermission = checkAddPatchSetPermission;
return this;
}
+ @CanIgnoreReturnValue
public RebaseChangeOp setFireRevisionCreated(boolean fireRevisionCreated) {
this.fireRevisionCreated = fireRevisionCreated;
return this;
}
+ @CanIgnoreReturnValue
public RebaseChangeOp setForceContentMerge(boolean forceContentMerge) {
this.forceContentMerge = forceContentMerge;
return this;
@@ -226,16 +242,19 @@ public class RebaseChangeOp implements BatchUpdateOp {
*
* @see #setForceContentMerge(boolean)
*/
+ @CanIgnoreReturnValue
public RebaseChangeOp setAllowConflicts(boolean allowConflicts) {
this.allowConflicts = allowConflicts;
return this;
}
+ @CanIgnoreReturnValue
public RebaseChangeOp setDetailedCommitMessage(boolean detailedCommitMessage) {
this.detailedCommitMessage = detailedCommitMessage;
return this;
}
+ @CanIgnoreReturnValue
public RebaseChangeOp setPostMessage(boolean postMessage) {
this.postMessage = postMessage;
return this;
@@ -247,21 +266,25 @@ public class RebaseChangeOp implements BatchUpdateOp {
* cases, we already store the votes of the new patch-sets in SubmitStrategyOp#saveApprovals. We
* should not also store the copied votes.
*/
+ @CanIgnoreReturnValue
public RebaseChangeOp setStoreCopiedVotes(boolean storeCopiedVotes) {
this.storeCopiedVotes = storeCopiedVotes;
return this;
}
+ @CanIgnoreReturnValue
public RebaseChangeOp setSendEmail(boolean sendEmail) {
this.sendEmail = sendEmail;
return this;
}
+ @CanIgnoreReturnValue
public RebaseChangeOp setMatchAuthorToCommitterDate(boolean matchAuthorToCommitterDate) {
this.matchAuthorToCommitterDate = matchAuthorToCommitterDate;
return this;
}
+ @CanIgnoreReturnValue
public RebaseChangeOp setValidationOptions(
ImmutableListMultimap<String, String> validationOptions) {
requireNonNull(validationOptions, "validationOptions may not be null");
@@ -269,15 +292,22 @@ public class RebaseChangeOp implements BatchUpdateOp {
return this;
}
+ @CanIgnoreReturnValue
public RebaseChangeOp setMergeStrategy(String strategy) {
this.mergeStrategy = strategy;
return this;
}
+ @CanIgnoreReturnValue
+ public RebaseChangeOp setVerifyNeedsRebase(boolean verifyNeedsRebase) {
+ this.verifyNeedsRebase = verifyNeedsRebase;
+ return this;
+ }
+
@Override
public void updateRepo(RepoContext ctx)
throws InvalidChangeOperationException, RestApiException, IOException, NoSuchChangeException,
- PermissionBackendException {
+ PermissionBackendException, DiffNotAvailableException {
// Ok that originalPatchSet was not read in a transaction, since we just
// need its revision.
RevWalk rw = ctx.getRevWalk();
@@ -351,6 +381,8 @@ public class RebaseChangeOp implements BatchUpdateOp {
}
}
+ logger.atFine().log(
+ "flushing inserter %s", ctx.getRevWalk().getObjectReader().getCreatedFromInserter());
ctx.getRevWalk().getObjectReader().getCreatedFromInserter().flush();
patchSetInserter.updateRepo(ctx);
}
@@ -440,7 +472,7 @@ public class RebaseChangeOp implements BatchUpdateOp {
throws ResourceConflictException, IOException {
RevCommit parentCommit = original.getParent(0);
- if (base.equals(parentCommit)) {
+ if (verifyNeedsRebase && base.equals(parentCommit)) {
throw new ResourceConflictException("Change is already up to date.");
}
@@ -467,10 +499,37 @@ public class RebaseChangeOp implements BatchUpdateOp {
if (success) {
filesWithGitConflicts = null;
tree = merger.getResultTreeId();
+ logger.atFine().log(
+ "tree of rebased commit: %s (no conflicts, inserter: %s, caller: %s)",
+ tree.name(), merger.getObjectInserter(), callerFinder.findCallerLazy());
} else {
List<String> conflicts = ImmutableList.of();
+ Map<String, ResolveMerger.MergeFailureReason> failed = ImmutableMap.of();
if (merger instanceof ResolveMerger) {
conflicts = ((ResolveMerger) merger).getUnmergedPaths();
+ failed = ((ResolveMerger) merger).getFailingPaths();
+ }
+
+ if (merger.getResultTreeId() != null) {
+ // Merging with conflicts below uses the same DirCache instance that has been used by the
+ // Merger to attempt the merge without conflicts.
+ //
+ // The Merger uses the DirCache to do the updates, and in particular to write the result
+ // tree. DirCache caches a single DirCacheTree instance that is used to write the result
+ // tree, but it writes the result tree only if there were no conflicts.
+ //
+ // Merging with conflicts uses the same DirCache instance to write the tree with conflicts
+ // that has been used by the Merger. This means if the Merger unexpectedly wrote a result
+ // tree although there had been conflicts, then merging with conflicts uses the same
+ // DirCacheTree instance to write the tree with conflicts. However DirCacheTree#writeTree
+ // writes a tree only once and then that tree is cached. Further invocations of
+ // DirCacheTree#writeTree have no effect and return the previously created tree. This means
+ // merging with conflicts can only successfully create the tree with conflicts if the Merger
+ // didn't write a result tree yet. Hence this is checked here and we log a warning if the
+ // result tree was already written.
+ logger.atWarning().log(
+ "result tree has already been written: %s (merger: %s, conflicts: %s, failed: %s)",
+ merger, merger.getResultTreeId().name(), conflicts, failed);
}
if (!allowConflicts || !(merger instanceof ResolveMerger)) {
@@ -489,6 +548,7 @@ public class RebaseChangeOp implements BatchUpdateOp {
.map(Map.Entry::getKey)
.collect(toImmutableSet());
+ logger.atFine().log("rebasing with conflicts");
tree =
MergeUtil.mergeWithConflicts(
ctx.getRevWalk(),
@@ -499,11 +559,23 @@ public class RebaseChangeOp implements BatchUpdateOp {
"BASE",
ctx.getRevWalk().parseCommit(base),
mergeResults);
+ logger.atFine().log(
+ "tree of rebased commit: %s (with conflicts, inserter: %s, caller: %s)",
+ tree.name(), ctx.getInserter(), callerFinder.findCallerLazy());
+ }
+
+ List<ObjectId> parents = new ArrayList<>();
+ parents.add(base);
+ if (original.getParentCount() > 1) {
+ // If a merge commit is rebased add all other parents (parent 2 to N).
+ for (int parent = 1; parent < original.getParentCount(); parent++) {
+ parents.add(original.getParent(parent));
+ }
}
CommitBuilder cb = new CommitBuilder();
cb.setTreeId(tree);
- cb.setParentId(base);
+ cb.setParentIds(parents);
cb.setAuthor(original.getAuthorIdent());
cb.setMessage(commitMessage);
if (committerIdent != null) {
@@ -523,6 +595,7 @@ public class RebaseChangeOp implements BatchUpdateOp {
ObjectId objectId = ctx.getInserter().insert(cb);
CodeReviewCommit commit = ((CodeReviewRevWalk) ctx.getRevWalk()).parseCommit(objectId);
commit.setFilesWithGitConflicts(filesWithGitConflicts);
+ logger.atFine().log("rebased commit=%s", commit.name());
return commit;
}
}
diff --git a/java/com/google/gerrit/server/change/RebaseUtil.java b/java/com/google/gerrit/server/change/RebaseUtil.java
index 47a1e115b6..93fcbc6629 100644
--- a/java/com/google/gerrit/server/change/RebaseUtil.java
+++ b/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -51,6 +51,7 @@ import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
+import org.eclipse.jgit.errors.InvalidObjectIdException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
@@ -252,18 +253,16 @@ public class RebaseUtil {
String.format("Change %s is %s", change.getId(), ChangeUtil.status(change)));
}
- if (!hasOneParent(rw, patchSet)) {
+ if (!hasAtLeastOneParent(rw, patchSet)) {
throw new ResourceConflictException(
String.format(
- "Error rebasing %s. Cannot rebase %s",
- change.getId(),
- countParents(rw, patchSet) > 1 ? "merge commits" : "commit with no ancestor"));
+ "Error rebasing %s. Cannot rebase commit with no ancestor", change.getId()));
}
}
- public static boolean hasOneParent(RevWalk rw, PatchSet ps) throws IOException {
- // Prevent rebase of exotic changes (merge commit, no ancestor).
- return countParents(rw, ps) == 1;
+ public static boolean hasAtLeastOneParent(RevWalk rw, PatchSet ps) throws IOException {
+ // Prevent rebase of changes with no ancestor.
+ return countParents(rw, ps) >= 1;
}
private static int countParents(RevWalk rw, PatchSet ps) throws IOException {
@@ -346,7 +345,7 @@ public class RebaseUtil {
}
}
- // Try parsing as SHA-1.
+ // Try parsing as SHA-1 based on the change-index.
Base ret = null;
for (ChangeData cd : queryProvider.get().byProjectCommit(rsrc.getProject(), base)) {
for (PatchSet ps : cd.patchSets()) {
@@ -371,8 +370,8 @@ public class RebaseUtil {
/**
* Parse or find the commit onto which a patch set should be rebased.
*
- * <p>If a {@code rebaseInput.base} is provided, parse it. Otherwise, finds the latest patch set
- * of the change corresponding to this commit's parent, or the destination branch tip in the case
+ * <p>If a {@code rebaseInput.base} is provided, parse it. Otherwise, find the latest patch set of
+ * the change corresponding to this commit's parent, or the destination branch tip in the case
* where the parent's change is merged.
*
* @param git the repository.
@@ -413,11 +412,16 @@ public class RebaseUtil {
throw new UnprocessableEntityException(
String.format("Base change not found: %s", inputBase), e);
}
- if (base == null) {
- throw new ResourceConflictException(
- "base revision is missing from the destination branch: " + inputBase);
+ if (base != null) {
+ return getLatestRevisionForBaseChange(rw, permissionBackend, rsrc, base);
+ }
+ if (isBaseRevisionInDestBranch(rw, inputBase, git, change.getDest())) {
+ // The requested base is a valid commit in the dest branch, which is not associated with any
+ // Gerrit change.
+ return ObjectId.fromString(inputBase);
}
- return getLatestRevisionForBaseChange(rw, permissionBackend, rsrc, base);
+ throw new ResourceConflictException(
+ "base revision is missing from the destination branch: " + inputBase);
}
private ObjectId getDestRefTip(Repository git, BranchNameKey destRefKey)
@@ -487,9 +491,7 @@ public class RebaseUtil {
ObjectId baseId = null;
RevCommit commit = rw.parseCommit(patchSet.commitId());
- if (commit.getParentCount() > 1) {
- throw new UnprocessableEntityException("Cannot rebase a change with multiple parents.");
- } else if (commit.getParentCount() == 0) {
+ if (commit.getParentCount() == 0) {
throw new UnprocessableEntityException(
"Cannot rebase a change without any parents (is this the initial commit?).");
}
@@ -535,6 +537,18 @@ public class RebaseUtil {
return baseId;
}
+ private boolean isBaseRevisionInDestBranch(
+ RevWalk rw, String expectedBaseSha1, Repository git, BranchNameKey destRefKey)
+ throws IOException, ResourceConflictException {
+ RevCommit potentialBaseCommit;
+ try {
+ potentialBaseCommit = rw.parseCommit(ObjectId.fromString(expectedBaseSha1));
+ } catch (InvalidObjectIdException | IOException e) {
+ return false;
+ }
+ return rw.isMergedInto(potentialBaseCommit, rw.parseCommit(getDestRefTip(git, destRefKey)));
+ }
+
public RebaseChangeOp getRebaseOp(
RevWalk rw,
RevisionResource revRsrc,
diff --git a/java/com/google/gerrit/server/change/RelatedChangesSorter.java b/java/com/google/gerrit/server/change/RelatedChangesSorter.java
index 2d93d4ad91..b95d592630 100644
--- a/java/com/google/gerrit/server/change/RelatedChangesSorter.java
+++ b/java/com/google/gerrit/server/change/RelatedChangesSorter.java
@@ -102,7 +102,7 @@ public class RelatedChangesSorter {
}
}
- Collection<PatchSetData> ancestors = walkAncestors(parents, start);
+ Set<PatchSetData> ancestors = walkAncestors(parents, start);
List<PatchSetData> descendants =
walkDescendants(children, start, otherPatchSetsOfStart, ancestors);
List<PatchSetData> result = new ArrayList<>(ancestors.size() + descendants.size() - 1);
@@ -135,7 +135,7 @@ public class RelatedChangesSorter {
}
}
- Collection<PatchSetData> ancestors = walkAncestors(parents, start);
+ Set<PatchSetData> ancestors = walkAncestors(parents, start);
return List.copyOf(ancestors);
}
diff --git a/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java b/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java
index 5930f7a42a..5ccbc04f82 100644
--- a/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java
+++ b/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java
@@ -17,6 +17,8 @@ package com.google.gerrit.server.change;
import static com.google.gerrit.server.mail.send.AttentionSetChangeEmailDecorator.AttentionSetChange.USER_REMOVED;
import static java.util.Objects.requireNonNull;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.AttentionSetUpdate.Operation;
@@ -46,6 +48,7 @@ public class RemoveFromAttentionSetOp implements BatchUpdateOp {
private Change change;
private boolean notify;
+ @Nullable private AttentionSetUpdateCondition condition;
/**
* Remove a specified user from the attention set.
@@ -68,8 +71,19 @@ public class RemoveFromAttentionSetOp implements BatchUpdateOp {
this.notify = notify;
}
+ /** Sets a condition for performing this attention set update. */
+ @CanIgnoreReturnValue
+ public RemoveFromAttentionSetOp setCondition(AttentionSetUpdateCondition condition) {
+ this.condition = condition;
+ return this;
+ }
+
@Override
public boolean updateChange(ChangeContext ctx) throws RestApiException {
+ if (condition != null && !condition.check()) {
+ return false;
+ }
+
ChangeData changeData = changeDataFactory.create(ctx.getNotes());
Optional<AttentionSetUpdate> existingEntry =
changeData.attentionSet().stream()
diff --git a/java/com/google/gerrit/server/change/ReviewerModifier.java b/java/com/google/gerrit/server/change/ReviewerModifier.java
index daa41bf187..61fab27285 100644
--- a/java/com/google/gerrit/server/change/ReviewerModifier.java
+++ b/java/com/google/gerrit/server/change/ReviewerModifier.java
@@ -24,6 +24,7 @@ import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static java.util.Comparator.comparing;
import static java.util.Objects.requireNonNull;
+import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
@@ -222,7 +223,10 @@ public class ReviewerModifier {
throws IOException, PermissionBackendException, ConfigInvalidException {
try (TraceContext.TraceTimer ignored =
TraceContext.newTimer(getClass().getSimpleName() + "#prepare", Metadata.empty())) {
- requireNonNull(input.reviewer);
+ if (Strings.nullToEmpty(input.reviewer).trim().isEmpty()) {
+ return fail(input, FailureType.NOT_FOUND, "reviewer user identifier is required");
+ }
+
boolean confirmed = input.confirmed();
boolean allowByEmail =
projectCache
@@ -667,7 +671,9 @@ public class ReviewerModifier {
throws RestApiException, IOException, PermissionBackendException {
for (ReviewerModification addition : modifications()) {
addition.op.setPatchSet(patchSet);
- addition.op.updateChange(ctx);
+
+ @SuppressWarnings("unused")
+ var unused = addition.op.updateChange(ctx);
}
}
diff --git a/java/com/google/gerrit/server/change/RevisionJson.java b/java/com/google/gerrit/server/change/RevisionJson.java
index 2ab7e15a52..5b63fac740 100644
--- a/java/com/google/gerrit/server/change/RevisionJson.java
+++ b/java/com/google/gerrit/server/change/RevisionJson.java
@@ -65,6 +65,9 @@ import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.account.GpgApiAdapter;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeUtilFactory;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
@@ -258,26 +261,30 @@ public class RevisionJson {
Optional<PatchSet.Id> limitToPsId,
ChangeInfo changeInfo)
throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
- Map<String, RevisionInfo> res = new LinkedHashMap<>();
- try (Repository repo = openRepoIfNecessary(cd.project());
- RevWalk rw = newRevWalk(repo)) {
- for (PatchSet in : map.values()) {
- PatchSet.Id id = in.id();
- boolean want;
- if (has(ALL_REVISIONS)) {
- want = true;
- } else if (limitToPsId.isPresent()) {
- want = id.equals(limitToPsId.get());
- } else {
- want = id.equals(cd.change().currentPatchSetId());
- }
- if (want) {
- res.put(
- in.commitId().name(),
- toRevisionInfo(accountLoader, cd, in, repo, rw, false, changeInfo));
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Get revisions", Metadata.builder().changeId(cd.change().getId().get()).build())) {
+ Map<String, RevisionInfo> res = new LinkedHashMap<>();
+ try (Repository repo = openRepoIfNecessary(cd.project());
+ RevWalk rw = newRevWalk(repo)) {
+ for (PatchSet in : map.values()) {
+ PatchSet.Id id = in.id();
+ boolean want;
+ if (has(ALL_REVISIONS)) {
+ want = true;
+ } else if (limitToPsId.isPresent()) {
+ want = id.equals(limitToPsId.get());
+ } else {
+ want = id.equals(cd.change().currentPatchSetId());
+ }
+ if (want) {
+ res.put(
+ in.commitId().name(),
+ toRevisionInfo(accountLoader, cd, in, repo, rw, false, changeInfo));
+ }
}
+ return res;
}
- return res;
}
}
diff --git a/java/com/google/gerrit/server/change/RevisionResource.java b/java/com/google/gerrit/server/change/RevisionResource.java
index e5a57b2d7a..4a101581b8 100644
--- a/java/com/google/gerrit/server/change/RevisionResource.java
+++ b/java/com/google/gerrit/server/change/RevisionResource.java
@@ -41,22 +41,22 @@ public class RevisionResource implements RestResource, HasETag {
return new RevisionResource(change, ps, Optional.empty(), false);
}
- private final ChangeResource change;
+ private final ChangeResource changeResource;
private final PatchSet ps;
private final Optional<ChangeEdit> edit;
private final boolean cacheable;
- public RevisionResource(ChangeResource change, PatchSet ps) {
- this(change, ps, Optional.empty());
+ public RevisionResource(ChangeResource changeResource, PatchSet ps) {
+ this(changeResource, ps, Optional.empty());
}
- public RevisionResource(ChangeResource change, PatchSet ps, Optional<ChangeEdit> edit) {
- this(change, ps, edit, true);
+ public RevisionResource(ChangeResource changeResource, PatchSet ps, Optional<ChangeEdit> edit) {
+ this(changeResource, ps, edit, true);
}
private RevisionResource(
- ChangeResource change, PatchSet ps, Optional<ChangeEdit> edit, boolean cacheable) {
- this.change = change;
+ ChangeResource changeResource, PatchSet ps, Optional<ChangeEdit> edit, boolean cacheable) {
+ this.changeResource = changeResource;
this.ps = ps;
this.edit = edit;
this.cacheable = cacheable;
@@ -67,15 +67,15 @@ public class RevisionResource implements RestResource, HasETag {
}
public PermissionBackend.ForChange permissions() {
- return change.permissions();
+ return changeResource.permissions();
}
public ChangeResource getChangeResource() {
- return change;
+ return changeResource;
}
public Change getChange() {
- return getChangeResource().getChange();
+ return changeResource.getChange();
}
public Project.NameKey getProject() {
@@ -83,7 +83,7 @@ public class RevisionResource implements RestResource, HasETag {
}
public ChangeNotes getNotes() {
- return getChangeResource().getNotes();
+ return changeResource.getNotes();
}
public PatchSet getPatchSet() {
@@ -96,9 +96,9 @@ public class RevisionResource implements RestResource, HasETag {
TraceContext.newTimer(
"Compute revision ETag",
Metadata.builder()
- .changeId(change.getId().get())
+ .changeId(changeResource.getId().get())
.patchSetId(ps.number())
- .projectName(change.getProject().get())
+ .projectName(changeResource.getProject().get())
.build())) {
Hasher h = Hashing.murmur3_128().newHasher();
prepareETag(h, getUser());
@@ -109,7 +109,7 @@ public class RevisionResource implements RestResource, HasETag {
public void prepareETag(Hasher h, CurrentUser user) {
// Conservative estimate: refresh the revision if its parent change has changed, so we don't
// have to check whether a given modification affected this revision specifically.
- change.prepareETag(h, user);
+ changeResource.prepareETag(h, user);
}
public Account.Id getAccountId() {
@@ -117,7 +117,7 @@ public class RevisionResource implements RestResource, HasETag {
}
public CurrentUser getUser() {
- return getChangeResource().getUser();
+ return changeResource.getUser();
}
public Optional<ChangeEdit> getEdit() {
diff --git a/java/com/google/gerrit/server/change/SetHashtagsOp.java b/java/com/google/gerrit/server/change/SetHashtagsOp.java
index bfc483469b..43045a427e 100644
--- a/java/com/google/gerrit/server/change/SetHashtagsOp.java
+++ b/java/com/google/gerrit/server/change/SetHashtagsOp.java
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.server.change.HashtagsUtil.extractTags;
import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Ordering;
import com.google.gerrit.common.Nullable;
@@ -91,7 +92,7 @@ public class SetHashtagsOp implements BatchUpdateOp {
ChangeNotes notes = update.getNotes().load();
try {
- Set<String> existingHashtags = notes.getHashtags();
+ ImmutableSet<String> existingHashtags = notes.getHashtags();
Set<String> updated = new HashSet<>();
toAdd = new HashSet<>(extractTags(input.add));
toRemove = new HashSet<>(extractTags(input.remove));
diff --git a/java/com/google/gerrit/server/change/WalkSorter.java b/java/com/google/gerrit/server/change/WalkSorter.java
index d2f7edeaa2..cc87ea84ad 100644
--- a/java/com/google/gerrit/server/change/WalkSorter.java
+++ b/java/com/google/gerrit/server/change/WalkSorter.java
@@ -23,7 +23,9 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Ordering;
+import com.google.common.collect.SetMultimap;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.exceptions.StorageException;
@@ -87,11 +89,13 @@ public class WalkSorter {
includePatchSets = new HashSet<>();
}
+ @CanIgnoreReturnValue
public WalkSorter includePatchSets(Iterable<PatchSet.Id> patchSets) {
Iterables.addAll(includePatchSets, patchSets);
return this;
}
+ @CanIgnoreReturnValue
public WalkSorter setRetainBody(boolean retainBody) {
this.retainBody = retainBody;
return this;
@@ -143,8 +147,8 @@ public class WalkSorter {
Set<RevCommit> commits = byCommit.keySet();
ListMultimap<RevCommit, RevCommit> children = collectChildren(commits);
- ListMultimap<RevCommit, RevCommit> pending =
- MultimapBuilder.hashKeys().arrayListValues().build();
+ SetMultimap<RevCommit, RevCommit> pending =
+ MultimapBuilder.hashKeys().hashSetValues().build();
Deque<RevCommit> todo = new ArrayDeque<>();
RevFlag done = rw.newFlag("done");
@@ -153,6 +157,7 @@ public class WalkSorter {
int found = 0;
RevCommit c;
List<PatchSetData> result = new ArrayList<>(expected);
+ int maxPopSize = commits.size() * commits.size();
while (found < expected && (c = rw.next()) != null) {
if (!commits.contains(c)) {
continue;
@@ -161,9 +166,15 @@ public class WalkSorter {
todo.add(c);
int i = 0;
while (!todo.isEmpty()) {
- // Sanity check: we can't pop more than N pending commits, otherwise
- // we have an infinite loop due to programmer error or something.
- checkState(++i <= commits.size(), "Too many pending steps while sorting %s", commits);
+ // Sanity check: in the worst case scenario, each commit can add all previous commits in
+ // the todo queue. This can lead to (n-1) + (n-2) + ... +1 iterations of the algorithm.
+ // So, in the worst case we can't pop more than N^2 pending commits, otherwise
+ // we have an infinite loop due to programmer error or something. (actually, it is
+ // (N-1) + (N-2) + (N-3) + (1) + 1 = N/2*(N-1)+1, but N^2 is enough for us.)
+ checkState(
+ ++i <= maxPopSize,
+ "Too many pending steps while sorting %s - can be a problem in the algorithm.",
+ commits);
RevCommit t = todo.removeFirst();
if (t.has(done)) {
continue;
diff --git a/java/com/google/gerrit/server/change/package-info.java b/java/com/google/gerrit/server/change/package-info.java
new file mode 100644
index 0000000000..0f70412d49
--- /dev/null
+++ b/java/com/google/gerrit/server/change/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.change;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/change/testing/package-info.java b/java/com/google/gerrit/server/change/testing/package-info.java
new file mode 100644
index 0000000000..3cd4da31bb
--- /dev/null
+++ b/java/com/google/gerrit/server/change/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.change.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/comment/package-info.java b/java/com/google/gerrit/server/comment/package-info.java
new file mode 100644
index 0000000000..e06653bb5b
--- /dev/null
+++ b/java/com/google/gerrit/server/comment/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.comment;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/config/AdministrateServerGroupsProvider.java b/java/com/google/gerrit/server/config/AdministrateServerGroupsProvider.java
index 9f6ecfb53e..1551df7cbf 100644
--- a/java/com/google/gerrit/server/config/AdministrateServerGroupsProvider.java
+++ b/java/com/google/gerrit/server/config/AdministrateServerGroupsProvider.java
@@ -54,7 +54,8 @@ public class AdministrateServerGroupsProvider implements Provider<ImmutableSet<G
}
groups = builder.build();
} finally {
- threadContext.setContext(ctx);
+ @SuppressWarnings("unused")
+ var unused = threadContext.setContext(ctx);
}
}
diff --git a/java/com/google/gerrit/server/config/CachedPreferences.java b/java/com/google/gerrit/server/config/CachedPreferences.java
index 169d9ecb45..6e957e6c7f 100644
--- a/java/com/google/gerrit/server/config/CachedPreferences.java
+++ b/java/com/google/gerrit/server/config/CachedPreferences.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.config;
import com.google.auto.value.AutoValue;
-import com.google.common.base.Function;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
@@ -23,6 +22,10 @@ import com.google.gerrit.extensions.client.EditPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.proto.Entities.UserPreferences;
import com.google.gerrit.server.cache.proto.Cache.CachedPreferencesProto;
+import com.google.gerrit.server.config.PreferencesParserUtil.DiffPreferencesParser;
+import com.google.gerrit.server.config.PreferencesParserUtil.EditPreferencesParser;
+import com.google.gerrit.server.config.PreferencesParserUtil.GeneralPreferencesParser;
+import com.google.gerrit.server.config.PreferencesParserUtil.PreferencesParser;
import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
@@ -67,38 +70,17 @@ public abstract class CachedPreferences {
public static GeneralPreferencesInfo general(
Optional<CachedPreferences> defaultPreferences, CachedPreferences userPreferences) {
- return getPreferences(
- defaultPreferences,
- userPreferences,
- PreferencesParserUtil::parseGeneralPreferences,
- p ->
- UserPreferencesConverter.GeneralPreferencesInfoConverter.fromProto(
- p.getGeneralPreferencesInfo()),
- GeneralPreferencesInfo.defaults());
+ return getPreferences(defaultPreferences, userPreferences, GeneralPreferencesParser.Instance);
}
public static DiffPreferencesInfo diff(
Optional<CachedPreferences> defaultPreferences, CachedPreferences userPreferences) {
- return getPreferences(
- defaultPreferences,
- userPreferences,
- PreferencesParserUtil::parseDiffPreferences,
- p ->
- UserPreferencesConverter.DiffPreferencesInfoConverter.fromProto(
- p.getDiffPreferencesInfo()),
- DiffPreferencesInfo.defaults());
+ return getPreferences(defaultPreferences, userPreferences, DiffPreferencesParser.Instance);
}
public static EditPreferencesInfo edit(
Optional<CachedPreferences> defaultPreferences, CachedPreferences userPreferences) {
- return getPreferences(
- defaultPreferences,
- userPreferences,
- PreferencesParserUtil::parseEditPreferences,
- p ->
- UserPreferencesConverter.EditPreferencesInfoConverter.fromProto(
- p.getEditPreferencesInfo()),
- EditPreferencesInfo.defaults());
+ return getPreferences(defaultPreferences, userPreferences, EditPreferencesParser.Instance);
}
public Config asConfig() {
@@ -135,34 +117,26 @@ public abstract class CachedPreferences {
return cachedPreferences.map(CachedPreferences::asConfig).orElse(null);
}
- @FunctionalInterface
- private interface ComputePreferencesFn<PreferencesT> {
- PreferencesT apply(Config cfg, @Nullable Config defaultCfg, @Nullable PreferencesT input)
- throws ConfigInvalidException;
- }
-
private static <PreferencesT> PreferencesT getPreferences(
Optional<CachedPreferences> defaultPreferences,
CachedPreferences userPreferences,
- ComputePreferencesFn<PreferencesT> computePreferencesFn,
- Function<UserPreferences, PreferencesT> fromUserPreferencesFn,
- PreferencesT javaDefaults) {
+ PreferencesParser<PreferencesT> preferencesParser) {
try {
CachedPreferencesProto userPreferencesProto = userPreferences.config();
switch (userPreferencesProto.getPreferencesCase()) {
case USER_PREFERENCES:
PreferencesT pref =
- fromUserPreferencesFn.apply(userPreferencesProto.getUserPreferences());
- return computePreferencesFn.apply(new Config(), configOrNull(defaultPreferences), pref);
+ preferencesParser.fromUserPreferences(userPreferencesProto.getUserPreferences());
+ return preferencesParser.parse(pref, configOrNull(defaultPreferences));
case LEGACY_GIT_CONFIG:
- return computePreferencesFn.apply(
+ return preferencesParser.parse(
userPreferences.asConfig(), configOrNull(defaultPreferences), null);
case PREFERENCES_NOT_SET:
throw new ConfigInvalidException("Invalid config " + userPreferences);
}
} catch (ConfigInvalidException e) {
- return javaDefaults;
+ return preferencesParser.getJavaDefaults();
}
- return javaDefaults;
+ return preferencesParser.getJavaDefaults();
}
}
diff --git a/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java b/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java
index 5e6a520762..c3516dd1aa 100644
--- a/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java
+++ b/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java
@@ -15,6 +15,7 @@ package com.google.gerrit.server.config;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import java.util.Collections;
import java.util.LinkedHashSet;
@@ -64,21 +65,21 @@ public class ConfigUpdatedEvent {
return accept(Collections.singleton(entry));
}
- public Multimap<UpdateResult, ConfigUpdateEntry> accept(Set<ConfigKey> entries) {
+ public ListMultimap<UpdateResult, ConfigUpdateEntry> accept(Set<ConfigKey> entries) {
return createUpdate(entries, UpdateResult.APPLIED);
}
- public Multimap<UpdateResult, ConfigUpdateEntry> accept(String section) {
+ public ListMultimap<UpdateResult, ConfigUpdateEntry> accept(String section) {
Set<ConfigKey> entries = getEntriesFromSection(oldConfig, section);
entries.addAll(getEntriesFromSection(newConfig, section));
return createUpdate(entries, UpdateResult.APPLIED);
}
- public Multimap<UpdateResult, ConfigUpdateEntry> reject(ConfigKey entry) {
+ public ListMultimap<UpdateResult, ConfigUpdateEntry> reject(ConfigKey entry) {
return reject(Collections.singleton(entry));
}
- public Multimap<UpdateResult, ConfigUpdateEntry> reject(Set<ConfigKey> entries) {
+ public ListMultimap<UpdateResult, ConfigUpdateEntry> reject(Set<ConfigKey> entries) {
return createUpdate(entries, UpdateResult.REJECTED);
}
@@ -95,9 +96,9 @@ public class ConfigUpdatedEvent {
return res;
}
- private Multimap<UpdateResult, ConfigUpdateEntry> createUpdate(
+ private ListMultimap<UpdateResult, ConfigUpdateEntry> createUpdate(
Set<ConfigKey> entries, UpdateResult updateResult) {
- Multimap<UpdateResult, ConfigUpdateEntry> updates = ArrayListMultimap.create();
+ ListMultimap<UpdateResult, ConfigUpdateEntry> updates = ArrayListMultimap.create();
entries.stream()
.filter(this::isValueUpdated)
.map(e -> new ConfigUpdateEntry(e, getString(e, oldConfig), getString(e, newConfig)))
diff --git a/java/com/google/gerrit/server/config/ConfigUtil.java b/java/com/google/gerrit/server/config/ConfigUtil.java
index 22c3d9946e..e76207ce78 100644
--- a/java/com/google/gerrit/server/config/ConfigUtil.java
+++ b/java/com/google/gerrit/server/config/ConfigUtil.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.config;
import static java.util.Objects.requireNonNull;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.common.UsedAt.Project;
import java.io.IOException;
@@ -344,6 +345,7 @@ public class ConfigUtil {
* when their values are false
* @return loaded instance
*/
+ @CanIgnoreReturnValue
public static <T> T loadSection(Config cfg, String section, String sub, T s, T defaults, T i)
throws ConfigInvalidException {
try {
@@ -395,6 +397,50 @@ public class ConfigUtil {
}
/**
+ * Merges config by inspecting Java class attributes, similar to {@link #loadSection}.
+ *
+ * <p>Config values are stored optimized: no default values are stored. The loading is performed
+ * eagerly: all values are set, except default boolean values.
+ *
+ * <p>Fields marked with final or transient modifiers are skipped.
+ *
+ * @param cfg config from which the values are loaded
+ * @param s instance of class in which the values are set
+ * @param defaults instance of class with default values
+ * @return loaded instance
+ */
+ @CanIgnoreReturnValue
+ public static <T> T mergeWithDefaults(T cfg, T s, T defaults) throws ConfigInvalidException {
+ try {
+ for (Field f : s.getClass().getDeclaredFields()) {
+ if (skipField(f)) {
+ continue;
+ }
+ Class<?> t = f.getType();
+ String n = f.getName();
+ f.setAccessible(true);
+
+ Object val = f.get(cfg);
+ if (val == null) {
+ val = f.get(defaults);
+ if (!isString(t) && !isCollectionOrMap(t)) {
+ requireNonNull(val, "Default cannot be null for: " + n);
+ }
+ }
+ if (!isBoolean(t) || (boolean) val) {
+ // To reproduce the same behavior as in the loadSection method above, values are
+ // explicitly set for all types, except the boolean type. For the boolean type, the value
+ // is set only if it is 'true' (so, the false value is omitted in the result object).
+ f.set(s, val);
+ }
+ }
+ } catch (SecurityException | IllegalArgumentException | IllegalAccessException e) {
+ throw new ConfigInvalidException("cannot load values", e);
+ }
+ return s;
+ }
+
+ /**
* Update user config by applying the specified delta
*
* <p>As opposed to {@link com.google.gerrit.server.config.ConfigUtil#storeSection}, this method
diff --git a/java/com/google/gerrit/server/config/ExperimentResource.java b/java/com/google/gerrit/server/config/ExperimentResource.java
new file mode 100644
index 0000000000..e22ffc7092
--- /dev/null
+++ b/java/com/google/gerrit/server/config/ExperimentResource.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+public class ExperimentResource extends ConfigResource {
+ public static final TypeLiteral<RestView<ExperimentResource>> EXPERIMENT_KIND =
+ new TypeLiteral<>() {};
+
+ private final String name;
+
+ public ExperimentResource(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index fe82a8852f..a38a3fcdc0 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -81,7 +81,6 @@ import com.google.gerrit.extensions.webui.TagWebLink;
import com.google.gerrit.extensions.webui.TopMenu;
import com.google.gerrit.extensions.webui.WebUiPlugin;
import com.google.gerrit.server.AnonymousUser;
-import com.google.gerrit.server.ChangeDraftUpdate;
import com.google.gerrit.server.CmdLineParserModule;
import com.google.gerrit.server.CreateGroupPermissionSyncer;
import com.google.gerrit.server.DeadlineChecker;
@@ -122,6 +121,7 @@ import com.google.gerrit.server.change.ChangeKindCacheImpl;
import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
import com.google.gerrit.server.change.EmailNewPatchSet;
import com.google.gerrit.server.change.FileInfoJsonModule;
+import com.google.gerrit.server.change.FilterIncludedIn;
import com.google.gerrit.server.change.MergeabilityCacheImpl;
import com.google.gerrit.server.change.ReviewerSuggestion;
import com.google.gerrit.server.change.RevisionJson;
@@ -160,7 +160,7 @@ import com.google.gerrit.server.git.validators.RefOperationValidators;
import com.google.gerrit.server.git.validators.UploadValidationListener;
import com.google.gerrit.server.git.validators.UploadValidators;
import com.google.gerrit.server.group.db.GroupDbModule;
-import com.google.gerrit.server.index.change.ReindexAfterRefUpdate;
+import com.google.gerrit.server.index.change.ReindexChangesAfterRefUpdate;
import com.google.gerrit.server.logging.PerformanceLogger;
import com.google.gerrit.server.mail.AutoReplyMailFilter;
import com.google.gerrit.server.mail.ListMailFilter;
@@ -171,11 +171,11 @@ import com.google.gerrit.server.mail.send.MailSoySauceModule;
import com.google.gerrit.server.mail.send.MailSoyTemplateProvider;
import com.google.gerrit.server.mime.FileTypeRegistry;
import com.google.gerrit.server.mime.MimeUtilFileTypeRegistry;
-import com.google.gerrit.server.notedb.ChangeDraftNotesUpdate;
import com.google.gerrit.server.notedb.DeleteZombieCommentsRefs;
import com.google.gerrit.server.notedb.NoteDbModule;
import com.google.gerrit.server.notedb.StoreSubmitRequirementsOp;
import com.google.gerrit.server.patch.DiffFileSizeValidator;
+import com.google.gerrit.server.patch.DiffOperationsForCommitValidation;
import com.google.gerrit.server.patch.DiffOperationsImpl;
import com.google.gerrit.server.patch.DiffValidator;
import com.google.gerrit.server.patch.PatchListCacheImpl;
@@ -318,6 +318,7 @@ public class GerritGlobalModule extends FactoryModule {
factory(ProjectOwnerGroupsProvider.Factory.class);
factory(SubmitRuleEvaluator.Factory.class);
factory(DeleteZombieCommentsRefs.Factory.class);
+ factory(DiffOperationsForCommitValidation.Factory.class);
bind(AuthBackend.class).to(UniversalAuthBackend.class).in(SINGLETON);
DynamicSet.setOf(binder(), AuthBackend.class);
@@ -396,7 +397,8 @@ public class GerritGlobalModule extends FactoryModule {
DynamicSet.setOf(binder(), GarbageCollectorListener.class);
DynamicSet.setOf(binder(), HeadUpdatedListener.class);
DynamicSet.setOf(binder(), UsageDataPublishedListener.class);
- DynamicSet.bind(binder(), GitBatchRefUpdateListener.class).to(ReindexAfterRefUpdate.class);
+ DynamicSet.bind(binder(), GitBatchRefUpdateListener.class)
+ .to(ReindexChangesAfterRefUpdate.class);
DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
.to(ProjectConfigEntry.UpdateChecker.class);
DynamicSet.setOf(binder(), EventListener.class);
@@ -429,6 +431,7 @@ public class GerritGlobalModule extends FactoryModule {
DynamicSet.bind(binder(), GerritConfigListener.class)
.toInstance(SuggestReviewers.configListener());
DynamicSet.setOf(binder(), ExternalIncludedIn.class);
+ DynamicSet.setOf(binder(), FilterIncludedIn.class);
DynamicMap.mapOf(binder(), ProjectConfigEntry.class);
DynamicSet.setOf(binder(), PluginPushOption.class);
DynamicSet.setOf(binder(), PatchSetWebLink.class);
@@ -481,7 +484,6 @@ public class GerritGlobalModule extends FactoryModule {
bind(CommentValidator.class)
.annotatedWith(Exports.named(CommentCumulativeSizeValidator.class.getSimpleName()))
.to(CommentCumulativeSizeValidator.class);
- bind(ChangeDraftUpdate.ChangeDraftUpdateFactory.class).to(ChangeDraftNotesUpdate.Factory.class);
DynamicMap.mapOf(binder(), DynamicOptions.DynamicBean.class);
DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeOperatorFactory.class);
diff --git a/java/com/google/gerrit/server/config/GerritInstanceNameProvider.java b/java/com/google/gerrit/server/config/GerritInstanceNameProvider.java
index 740bb01e41..b2e80d7c09 100644
--- a/java/com/google/gerrit/server/config/GerritInstanceNameProvider.java
+++ b/java/com/google/gerrit/server/config/GerritInstanceNameProvider.java
@@ -30,9 +30,8 @@ public class GerritInstanceNameProvider implements Provider<String> {
@Inject
public GerritInstanceNameProvider(
- @GerritServerConfig Config config,
- @CanonicalWebUrl @Nullable Provider<String> canonicalUrlProvider) {
- this.instanceName = getInstanceName(config, canonicalUrlProvider);
+ @GerritServerConfig Config config, @CanonicalWebUrl @Nullable String canonicalUrl) {
+ this.instanceName = getInstanceName(config, canonicalUrl);
}
@Override
@@ -40,14 +39,13 @@ public class GerritInstanceNameProvider implements Provider<String> {
return instanceName;
}
- private static String getInstanceName(
- Config config, @Nullable Provider<String> canonicalUrlProvider) {
+ private static String getInstanceName(Config config, String canonicalUrl) {
String instanceName = config.getString("gerrit", null, "instanceName");
- if (instanceName != null || canonicalUrlProvider == null) {
+ if (instanceName != null) {
return instanceName;
}
- return extractInstanceName(canonicalUrlProvider.get());
+ return extractInstanceName(canonicalUrl);
}
private static String extractInstanceName(String canonicalUrl) {
diff --git a/java/com/google/gerrit/server/config/GerritServerConfigReloader.java b/java/com/google/gerrit/server/config/GerritServerConfigReloader.java
index 5ecf6ed43b..ea7eea723e 100644
--- a/java/com/google/gerrit/server/config/GerritServerConfigReloader.java
+++ b/java/com/google/gerrit/server/config/GerritServerConfigReloader.java
@@ -15,7 +15,7 @@
package com.google.gerrit.server.config;
import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.Multimap;
+import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.server.config.ConfigUpdatedEvent.ConfigUpdateEntry;
import com.google.gerrit.server.config.ConfigUpdatedEvent.UpdateResult;
@@ -43,17 +43,17 @@ public class GerritServerConfigReloader {
* Reloads the Gerrit Server Configuration from disk. Synchronized to ensure that one issued
* reload is fully completed before a new one starts.
*/
- public Multimap<UpdateResult, ConfigUpdateEntry> reloadConfig() {
+ public ListMultimap<UpdateResult, ConfigUpdateEntry> reloadConfig() {
logger.atInfo().log("Starting server configuration reload");
- Multimap<UpdateResult, ConfigUpdateEntry> updates =
+ ListMultimap<UpdateResult, ConfigUpdateEntry> updates =
fireUpdatedConfigEvent(configProvider.updateConfig());
logger.atInfo().log("Server configuration reload completed succesfully");
return updates;
}
- public Multimap<UpdateResult, ConfigUpdateEntry> fireUpdatedConfigEvent(
+ public ListMultimap<UpdateResult, ConfigUpdateEntry> fireUpdatedConfigEvent(
ConfigUpdatedEvent event) {
- Multimap<UpdateResult, ConfigUpdateEntry> updates = ArrayListMultimap.create();
+ ListMultimap<UpdateResult, ConfigUpdateEntry> updates = ArrayListMultimap.create();
configListeners.runEach(l -> updates.putAll(l.configUpdated(event)));
return updates;
}
diff --git a/java/com/google/gerrit/server/config/GitwebCgiConfig.java b/java/com/google/gerrit/server/config/GitwebCgiConfig.java
index 1ed0f16ad9..862b092ad3 100644
--- a/java/com/google/gerrit/server/config/GitwebCgiConfig.java
+++ b/java/com/google/gerrit/server/config/GitwebCgiConfig.java
@@ -21,7 +21,6 @@ import com.google.common.flogger.FluentLogger;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.nio.file.Path;
-import java.nio.file.Paths;
import org.eclipse.jgit.lib.Config;
@Singleton
@@ -48,7 +47,7 @@ public class GitwebCgiConfig {
}
String cfgCgi = cfg.getString("gitweb", null, "cgi");
- Path pkgCgi = Paths.get("/usr/lib/cgi-bin/gitweb.cgi");
+ Path pkgCgi = Path.of("/usr/lib/cgi-bin/gitweb.cgi");
String[] resourcePaths = {
"/usr/share/gitweb/static", "/usr/share/gitweb", "/var/www/static", "/var/www",
};
@@ -96,7 +95,7 @@ public class GitwebCgiConfig {
Path js = null;
Path logo = null;
for (String path : resourcePaths) {
- Path dir = Paths.get(path);
+ Path dir = Path.of(path);
css = dir.resolve("gitweb.css");
js = dir.resolve("gitweb.js");
logo = dir.resolve("git-logo.png");
diff --git a/java/com/google/gerrit/server/config/GroupSetProvider.java b/java/com/google/gerrit/server/config/GroupSetProvider.java
index 025946d893..1a7f14c386 100644
--- a/java/com/google/gerrit/server/config/GroupSetProvider.java
+++ b/java/com/google/gerrit/server/config/GroupSetProvider.java
@@ -51,7 +51,8 @@ public abstract class GroupSetProvider implements Provider<Set<AccountGroup.UUID
}
groupIds = builder.build();
} finally {
- threadContext.setContext(ctx);
+ @SuppressWarnings("unused")
+ var unused = threadContext.setContext(ctx);
}
}
diff --git a/java/com/google/gerrit/server/config/IndexResource.java b/java/com/google/gerrit/server/config/IndexResource.java
new file mode 100644
index 0000000000..30d39c413c
--- /dev/null
+++ b/java/com/google/gerrit/server/config/IndexResource.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.index.Index;
+import com.google.gerrit.index.IndexDefinition;
+import com.google.inject.TypeLiteral;
+
+public class IndexResource extends ConfigResource {
+ public static final TypeLiteral<RestView<IndexResource>> INDEX_KIND = new TypeLiteral<>() {};
+
+ private IndexDefinition<?, ?, ?> def;
+
+ public IndexResource(IndexDefinition<?, ?, ?> def) {
+ this.def = def;
+ }
+
+ public IndexDefinition<?, ?, ? extends Index<?, ?>> getIndexDefinition() {
+ return def;
+ }
+}
diff --git a/java/com/google/gerrit/server/config/IndexVersionResource.java b/java/com/google/gerrit/server/config/IndexVersionResource.java
new file mode 100644
index 0000000000..8ccb0b0e6f
--- /dev/null
+++ b/java/com/google/gerrit/server/config/IndexVersionResource.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.index.Index;
+import com.google.gerrit.index.IndexDefinition;
+import com.google.inject.TypeLiteral;
+
+public class IndexVersionResource implements RestResource {
+ public static final TypeLiteral<RestView<IndexVersionResource>> INDEX_VERSION_KIND =
+ new TypeLiteral<>() {};
+
+ private final IndexDefinition<?, ?, ?> def;
+ private final Index<?, ?> index;
+
+ public IndexVersionResource(IndexDefinition<?, ?, ?> def, Index<?, ?> index) {
+ this.def = def;
+ this.index = index;
+ }
+
+ public IndexDefinition<?, ?, ?> getIndexDefinition() {
+ return def;
+ }
+
+ public Index<?, ?> getIndex() {
+ return index;
+ }
+}
diff --git a/java/com/google/gerrit/server/config/PreferencesParserUtil.java b/java/com/google/gerrit/server/config/PreferencesParserUtil.java
index fbdb324030..93df926df2 100644
--- a/java/com/google/gerrit/server/config/PreferencesParserUtil.java
+++ b/java/com/google/gerrit/server/config/PreferencesParserUtil.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.config;
import static com.google.gerrit.server.config.ConfigUtil.loadSection;
+import static com.google.gerrit.server.config.ConfigUtil.mergeWithDefaults;
import static com.google.gerrit.server.config.ConfigUtil.skipField;
import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE;
import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN;
@@ -30,6 +31,7 @@ import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.EditPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.client.MenuItem;
+import com.google.gerrit.proto.Entities.UserPreferences;
import com.google.gerrit.server.git.UserConfigSections;
import java.lang.reflect.Field;
import java.util.ArrayList;
@@ -66,13 +68,31 @@ public class PreferencesParserUtil {
r.my = input.my;
} else {
r.changeTable = parseChangeTableColumns(cfg, defaultCfg);
- r.my = parseMyMenus(cfg, defaultCfg);
+ r.my = parseMyMenus(my(cfg), defaultCfg);
}
return r;
}
/**
* Returns a {@link GeneralPreferencesInfo} that is the result of parsing {@code defaultCfg} for
+ * the server's default configs and {@code cfg} for the user's config.
+ */
+ public static GeneralPreferencesInfo parseGeneralPreferences(
+ GeneralPreferencesInfo cfg, @Nullable Config defaultCfg) throws ConfigInvalidException {
+ GeneralPreferencesInfo r =
+ mergeWithDefaults(
+ cfg,
+ new GeneralPreferencesInfo(),
+ defaultCfg != null
+ ? parseDefaultGeneralPreferences(defaultCfg, null)
+ : GeneralPreferencesInfo.defaults());
+ r.changeTable = cfg.changeTable != null ? cfg.changeTable : Lists.newArrayList();
+ r.my = parseMyMenus(cfg.my, defaultCfg);
+ return r;
+ }
+
+ /**
+ * Returns a {@link GeneralPreferencesInfo} that is the result of parsing {@code defaultCfg} for
* the server's default configs. These configs are then overlaid to inherit values (default ->
* input (if provided).
*/
@@ -110,6 +130,20 @@ public class PreferencesParserUtil {
/**
* Returns a {@link DiffPreferencesInfo} that is the result of parsing {@code defaultCfg} for the
+ * server's default configs and {@code cfg} for the user's config.
+ */
+ public static DiffPreferencesInfo parseDiffPreferences(
+ DiffPreferencesInfo cfg, @Nullable Config defaultCfg) throws ConfigInvalidException {
+ return mergeWithDefaults(
+ cfg,
+ new DiffPreferencesInfo(),
+ defaultCfg != null
+ ? parseDefaultDiffPreferences(defaultCfg, null)
+ : DiffPreferencesInfo.defaults());
+ }
+
+ /**
+ * Returns a {@link DiffPreferencesInfo} that is the result of parsing {@code defaultCfg} for the
* server's default configs. These configs are then overlaid to inherit values (default -> input
* (if provided).
*/
@@ -147,6 +181,20 @@ public class PreferencesParserUtil {
/**
* Returns a {@link EditPreferencesInfo} that is the result of parsing {@code defaultCfg} for the
+ * server's default configs and {@code cfg} for the user's config.
+ */
+ public static EditPreferencesInfo parseEditPreferences(
+ EditPreferencesInfo cfg, @Nullable Config defaultCfg) throws ConfigInvalidException {
+ return mergeWithDefaults(
+ cfg,
+ new EditPreferencesInfo(),
+ defaultCfg != null
+ ? parseDefaultEditPreferences(defaultCfg, null)
+ : EditPreferencesInfo.defaults());
+ }
+
+ /**
+ * Returns a {@link EditPreferencesInfo} that is the result of parsing {@code defaultCfg} for the
* server's default configs. These configs are then overlaid to inherit values (default -> input
* (if provided).
*/
@@ -171,11 +219,14 @@ public class PreferencesParserUtil {
return changeTable;
}
- private static List<MenuItem> parseMyMenus(Config cfg, @Nullable Config defaultCfg) {
- List<MenuItem> my = my(cfg);
- if (my.isEmpty() && defaultCfg != null) {
+ private static List<MenuItem> parseMyMenus(
+ @Nullable List<MenuItem> my, @Nullable Config defaultCfg) {
+ if (defaultCfg != null && (my == null || my.isEmpty())) {
my = my(defaultCfg);
}
+ if (my == null) {
+ my = new ArrayList<>();
+ }
if (my.isEmpty()) {
my.add(new MenuItem("Dashboard", "#/dashboard/self", null));
my.add(new MenuItem("Draft Comments", "#/q/has:draft", null));
@@ -264,4 +315,110 @@ public class PreferencesParserUtil {
String val = cfg.getString(UserConfigSections.MY, subsection, key);
return !Strings.isNullOrEmpty(val) ? val : defaultValue;
}
+
+ /** Provides methods for parsing user configs */
+ public interface PreferencesParser<T> {
+ T parse(Config cfg, @Nullable Config defaultConfig, @Nullable T input)
+ throws ConfigInvalidException;
+
+ T parse(T cfg, @Nullable Config defaultConfig) throws ConfigInvalidException;
+
+ T fromUserPreferences(UserPreferences userPreferences);
+
+ T getJavaDefaults();
+ }
+
+ /** Provides methods for parsing GeneralPreferencesInfo configs */
+ public static class GeneralPreferencesParser
+ implements PreferencesParser<GeneralPreferencesInfo> {
+ public static GeneralPreferencesParser Instance = new GeneralPreferencesParser();
+
+ private GeneralPreferencesParser() {}
+
+ @Override
+ public GeneralPreferencesInfo parse(
+ Config cfg, @Nullable Config defaultCfg, @Nullable GeneralPreferencesInfo input)
+ throws ConfigInvalidException {
+ return PreferencesParserUtil.parseGeneralPreferences(cfg, defaultCfg, input);
+ }
+
+ @Override
+ public GeneralPreferencesInfo parse(GeneralPreferencesInfo cfg, @Nullable Config defaultCfg)
+ throws ConfigInvalidException {
+ return PreferencesParserUtil.parseGeneralPreferences(cfg, defaultCfg);
+ }
+
+ @Override
+ public GeneralPreferencesInfo fromUserPreferences(UserPreferences p) {
+ return UserPreferencesConverter.GeneralPreferencesInfoConverter.fromProto(
+ p.getGeneralPreferencesInfo());
+ }
+
+ @Override
+ public GeneralPreferencesInfo getJavaDefaults() {
+ return GeneralPreferencesInfo.defaults();
+ }
+ }
+
+ /** Provides methods for parsing EditPreferencesInfo configs */
+ public static class EditPreferencesParser implements PreferencesParser<EditPreferencesInfo> {
+ public static EditPreferencesParser Instance = new EditPreferencesParser();
+
+ private EditPreferencesParser() {}
+
+ @Override
+ public EditPreferencesInfo parse(
+ Config cfg, @Nullable Config defaultCfg, @Nullable EditPreferencesInfo input)
+ throws ConfigInvalidException {
+ return PreferencesParserUtil.parseEditPreferences(cfg, defaultCfg, input);
+ }
+
+ @Override
+ public EditPreferencesInfo parse(EditPreferencesInfo cfg, @Nullable Config defaultCfg)
+ throws ConfigInvalidException {
+ return PreferencesParserUtil.parseEditPreferences(cfg, defaultCfg);
+ }
+
+ @Override
+ public EditPreferencesInfo fromUserPreferences(UserPreferences p) {
+ return UserPreferencesConverter.EditPreferencesInfoConverter.fromProto(
+ p.getEditPreferencesInfo());
+ }
+
+ @Override
+ public EditPreferencesInfo getJavaDefaults() {
+ return EditPreferencesInfo.defaults();
+ }
+ }
+
+ /** Provides methods for parsing DiffPreferencesInfo configs */
+ public static class DiffPreferencesParser implements PreferencesParser<DiffPreferencesInfo> {
+ public static DiffPreferencesParser Instance = new DiffPreferencesParser();
+
+ private DiffPreferencesParser() {}
+
+ @Override
+ public DiffPreferencesInfo parse(
+ Config cfg, @Nullable Config defaultCfg, @Nullable DiffPreferencesInfo input)
+ throws ConfigInvalidException {
+ return PreferencesParserUtil.parseDiffPreferences(cfg, defaultCfg, input);
+ }
+
+ @Override
+ public DiffPreferencesInfo parse(DiffPreferencesInfo cfg, @Nullable Config defaultCfg)
+ throws ConfigInvalidException {
+ return PreferencesParserUtil.parseDiffPreferences(cfg, defaultCfg);
+ }
+
+ @Override
+ public DiffPreferencesInfo fromUserPreferences(UserPreferences p) {
+ return UserPreferencesConverter.DiffPreferencesInfoConverter.fromProto(
+ p.getDiffPreferencesInfo());
+ }
+
+ @Override
+ public DiffPreferencesInfo getJavaDefaults() {
+ return DiffPreferencesInfo.defaults();
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/config/ProjectConfigEntry.java b/java/com/google/gerrit/server/config/ProjectConfigEntry.java
index e11d6aa7c1..1ff0a8b37b 100644
--- a/java/com/google/gerrit/server/config/ProjectConfigEntry.java
+++ b/java/com/google/gerrit/server/config/ProjectConfigEntry.java
@@ -44,7 +44,7 @@ public class ProjectConfigEntry {
private final String displayName;
private final String description;
private final boolean inheritable;
- private final String defaultValue;
+ @Nullable private final String defaultValue;
private final ProjectConfigEntryType type;
private final List<String> permittedValues;
diff --git a/java/com/google/gerrit/server/config/RepositoryConfig.java b/java/com/google/gerrit/server/config/RepositoryConfig.java
index d569c872cb..c008f637d3 100644
--- a/java/com/google/gerrit/server/config/RepositoryConfig.java
+++ b/java/com/google/gerrit/server/config/RepositoryConfig.java
@@ -58,7 +58,7 @@ public class RepositoryConfig {
@Nullable
public Path getBasePath(Project.NameKey project) {
String basePath = cfg.getString(SECTION_NAME, findSubSection(project.get()), BASE_PATH_NAME);
- return basePath != null ? Paths.get(basePath) : null;
+ return basePath != null ? Path.of(basePath) : null;
}
public ImmutableList<Path> getAllBasePaths() {
diff --git a/java/com/google/gerrit/server/config/UserPreferencesConverter.java b/java/com/google/gerrit/server/config/UserPreferencesConverter.java
index 4a052d7580..7eae7d099f 100644
--- a/java/com/google/gerrit/server/config/UserPreferencesConverter.java
+++ b/java/com/google/gerrit/server/config/UserPreferencesConverter.java
@@ -16,6 +16,7 @@ package com.google.gerrit.server.config;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.EditPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
@@ -180,20 +181,24 @@ public final class UserPreferencesConverter {
MenuItem javaItem) {
UserPreferences.GeneralPreferencesInfo.MenuItem.Builder builder =
UserPreferences.GeneralPreferencesInfo.MenuItem.newBuilder();
- builder = setIfNotNull(builder, builder::setName, javaItem.name);
- builder = setIfNotNull(builder, builder::setUrl, javaItem.url);
- builder = setIfNotNull(builder, builder::setTarget, javaItem.target);
- builder = setIfNotNull(builder, builder::setId, javaItem.id);
+ builder = setIfNotNull(builder, builder::setName, trimSafe(javaItem.name));
+ builder = setIfNotNull(builder, builder::setUrl, trimSafe(javaItem.url));
+ builder = setIfNotNull(builder, builder::setTarget, trimSafe(javaItem.target));
+ builder = setIfNotNull(builder, builder::setId, trimSafe(javaItem.id));
return builder.build();
}
+ private static @Nullable String trimSafe(@Nullable String s) {
+ return s == null ? s : s.trim();
+ }
+
private static MenuItem menuItemFromProto(
UserPreferences.GeneralPreferencesInfo.MenuItem proto) {
return new MenuItem(
- proto.hasName() ? proto.getName() : null,
- proto.hasUrl() ? proto.getUrl() : null,
- proto.hasTarget() ? proto.getTarget() : null,
- proto.hasId() ? proto.getId() : null);
+ proto.hasName() ? proto.getName().trim() : null,
+ proto.hasUrl() ? proto.getUrl().trim() : null,
+ proto.hasTarget() ? proto.getTarget().trim() : null,
+ proto.hasId() ? proto.getId().trim() : null);
}
private GeneralPreferencesInfoConverter() {}
diff --git a/java/com/google/gerrit/server/config/VersionedDefaultPreferences.java b/java/com/google/gerrit/server/config/VersionedDefaultPreferences.java
index bea6dd3a52..45a9ddf0d5 100644
--- a/java/com/google/gerrit/server/config/VersionedDefaultPreferences.java
+++ b/java/com/google/gerrit/server/config/VersionedDefaultPreferences.java
@@ -16,13 +16,11 @@ package com.google.gerrit.server.config;
import static com.google.common.base.Preconditions.checkState;
-import com.google.common.base.Strings;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.server.git.meta.VersionedMetaData;
+import com.google.gerrit.server.git.meta.VersionedConfigFile;
import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Repository;
@@ -30,11 +28,9 @@ import org.eclipse.jgit.lib.Repository;
* Low-level storage API to load Gerrit's default config from {@code All-Users}. Should not be used
* directly.
*/
-public class VersionedDefaultPreferences extends VersionedMetaData {
+public class VersionedDefaultPreferences extends VersionedConfigFile {
private static final String PREFERENCES_CONFIG = "preferences.config";
- private Config cfg;
-
public static Config get(Repository allUsersRepo, AllUsersName allUsersName)
throws StorageException, ConfigInvalidException {
VersionedDefaultPreferences versionedDefaultPreferences = new VersionedDefaultPreferences();
@@ -46,27 +42,13 @@ public class VersionedDefaultPreferences extends VersionedMetaData {
return versionedDefaultPreferences.getConfig();
}
- @Override
- protected String getRefName() {
- return RefNames.REFS_USERS_DEFAULT;
+ public VersionedDefaultPreferences() {
+ super(RefNames.REFS_USERS_DEFAULT, PREFERENCES_CONFIG, "Update default preferences\n");
}
+ @Override
public Config getConfig() {
checkState(cfg != null, "Default preferences not loaded yet.");
return cfg;
}
-
- @Override
- protected void onLoad() throws IOException, ConfigInvalidException {
- cfg = readConfig(PREFERENCES_CONFIG);
- }
-
- @Override
- protected boolean onSave(CommitBuilder commit) throws IOException {
- if (Strings.isNullOrEmpty(commit.getMessage())) {
- commit.setMessage("Update default preferences\n");
- }
- saveConfig(PREFERENCES_CONFIG, cfg);
- return true;
- }
}
diff --git a/java/com/google/gerrit/server/config/package-info.java b/java/com/google/gerrit/server/config/package-info.java
new file mode 100644
index 0000000000..349fb04588
--- /dev/null
+++ b/java/com/google/gerrit/server/config/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.config;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/data/BUILD b/java/com/google/gerrit/server/data/BUILD
index 1aaab9634e..4eb7b7f132 100644
--- a/java/com/google/gerrit/server/data/BUILD
+++ b/java/com/google/gerrit/server/data/BUILD
@@ -10,5 +10,6 @@ java_library(
"//java/com/google/gerrit/entities",
"//java/com/google/gerrit/extensions:api",
"//lib:gson",
+ "//lib/errorprone:annotations",
],
)
diff --git a/java/com/google/gerrit/server/data/package-info.java b/java/com/google/gerrit/server/data/package-info.java
new file mode 100644
index 0000000000..0383bb580e
--- /dev/null
+++ b/java/com/google/gerrit/server/data/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.data;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/diff/package-info.java b/java/com/google/gerrit/server/diff/package-info.java
new file mode 100644
index 0000000000..874dd17e49
--- /dev/null
+++ b/java/com/google/gerrit/server/diff/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.diff;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/documentation/package-info.java b/java/com/google/gerrit/server/documentation/package-info.java
new file mode 100644
index 0000000000..64b3ea14e1
--- /dev/null
+++ b/java/com/google/gerrit/server/documentation/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.documentation;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 68569f0be4..565d471740 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -18,12 +18,14 @@ import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
import com.google.common.base.Charsets;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.changes.ChangeEditIdentityType;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MergeConflictException;
@@ -202,7 +204,13 @@ public class ChangeEditModifier {
Instant nowTimestamp = TimeUtil.now();
String commitMessage = currentEditCommit.getFullMessage();
ObjectId newEditCommitId =
- createCommit(repository, basePatchSetCommit, newTreeId, commitMessage, nowTimestamp);
+ createCommit(
+ repository,
+ basePatchSetCommit,
+ newTreeId,
+ commitMessage,
+ currentEditCommit.getAuthorIdent(),
+ new PersonIdent(currentEditCommit.getCommitterIdent(), nowTimestamp));
noteDbEdits.baseEditOnDifferentPatchset(
project,
@@ -236,6 +244,26 @@ public class ChangeEditModifier {
CommitModification.builder().newCommitMessage(newCommitMessage).build());
}
+ public void modifyIdentity(
+ Repository repository,
+ ChangeNotes notes,
+ PersonIdent identity,
+ ChangeEditIdentityType identityType)
+ throws AuthException, IOException, InvalidChangeOperationException,
+ PermissionBackendException, BadRequestException, ResourceConflictException {
+ CommitModification.Builder cmb = CommitModification.builder();
+ switch (identityType) {
+ case AUTHOR:
+ cmb.newAuthor(identity);
+ break;
+ case COMMITTER:
+ default:
+ cmb.newCommitter(identity);
+ break;
+ }
+ modifyCommit(repository, notes, new ModificationIntention.LatestCommit(), cmb.build());
+ }
+
/**
* Modifies the contents of a file of a change edit. If the change edit doesn't exist, a new one
* will be created based on the current patch set.
@@ -367,6 +395,7 @@ public class ChangeEditModifier {
repository, notes, new ModificationIntention.PatchsetCommit(patchSet), commitModification);
}
+ @CanIgnoreReturnValue
private ChangeEdit modifyCommit(
Repository repository,
ChangeNotes notes,
@@ -402,16 +431,22 @@ public class ChangeEditModifier {
createNewCommitMessage(
changeIdRequired, currentChangeId, editBehavior, commitModification, commitToModify);
newCommitMessage = editBehavior.mergeCommitMessageIfNecessary(newCommitMessage, commitToModify);
+ Instant nowTimestamp = TimeUtil.now();
+ PersonIdent author = getAuthor(commitModification, commitToModify, nowTimestamp);
+ PersonIdent committer =
+ getCommitter(commitModification, commitToModify, basePatchsetCommit, nowTimestamp);
Optional<ChangeEdit> unmodifiedEdit =
- editBehavior.getEditIfNoModification(newTreeId, newCommitMessage);
+ editBehavior.getEditIfNoModification(
+ newTreeId, newCommitMessage,
+ author, committer);
if (unmodifiedEdit.isPresent()) {
return unmodifiedEdit.get();
}
- Instant nowTimestamp = TimeUtil.now();
ObjectId newEditCommit =
- createCommit(repository, basePatchsetCommit, newTreeId, newCommitMessage, nowTimestamp);
+ createCommit(
+ repository, basePatchsetCommit, newTreeId, newCommitMessage, author, committer);
try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
return editBehavior.updateEditInStorage(
@@ -419,6 +454,44 @@ public class ChangeEditModifier {
}
}
+ private PersonIdent getAuthor(
+ CommitModification commitModification, RevCommit commitToModify, Instant timestamp) {
+ PersonIdent currentAuthor = commitToModify.getAuthorIdent();
+ if (!commitModification.newAuthor().isPresent()) {
+ return currentAuthor;
+ }
+ PersonIdent newAuthor = commitModification.newAuthor().get();
+ String newName = newAuthor.getName();
+ String newEmail = newAuthor.getEmailAddress();
+ return new PersonIdent(
+ newName.isEmpty() ? currentAuthor.getName() : newName,
+ newEmail.isEmpty() ? currentAuthor.getEmailAddress() : newEmail,
+ timestamp,
+ zoneId);
+ }
+
+ private PersonIdent getCommitter(
+ CommitModification commitModification,
+ RevCommit commitToModify,
+ RevCommit basePatchsetCommit,
+ Instant timestamp) {
+ PersonIdent currentCommitter = commitToModify.getCommitterIdent();
+ if (!commitModification.newCommitter().isPresent()) {
+ if (commitToModify.equals(basePatchsetCommit)) {
+ return getCommitterIdent(basePatchsetCommit, timestamp);
+ }
+ return new PersonIdent(currentCommitter, timestamp);
+ }
+ PersonIdent newCommitter = commitModification.newCommitter().get();
+ String newName = newCommitter.getName();
+ String newEmail = newCommitter.getEmailAddress();
+ return new PersonIdent(
+ newName.isEmpty() ? currentCommitter.getName() : newName,
+ newEmail.isEmpty() ? currentCommitter.getEmailAddress() : newEmail,
+ timestamp,
+ zoneId);
+ }
+
private void assertCanEdit(ChangeNotes notes)
throws AuthException, PermissionBackendException, ResourceConflictException {
if (!currentUser.get().isIdentifiedUser()) {
@@ -493,7 +566,8 @@ public class ChangeEditModifier {
if (!successful) {
throw new MergeConflictException(
- "The existing change edit could not be merged with another tree.");
+ "Rebasing change edit onto another patchset results in merge conflicts. Download the edit"
+ + " patchset and rebase manually to preserve changes.");
}
return threeWayMerger.getResultTreeId();
}
@@ -528,14 +602,15 @@ public class ChangeEditModifier {
RevCommit basePatchsetCommit,
ObjectId tree,
String commitMessage,
- Instant timestamp)
+ PersonIdent author,
+ PersonIdent committer)
throws IOException {
try (ObjectInserter objectInserter = repository.newObjectInserter()) {
CommitBuilder builder = new CommitBuilder();
builder.setTreeId(tree);
builder.setParentIds(basePatchsetCommit.getParents());
- builder.setAuthor(basePatchsetCommit.getAuthorIdent());
- builder.setCommitter(getCommitterIdent(basePatchsetCommit, timestamp));
+ builder.setAuthor(author);
+ builder.setCommitter(committer);
builder.setMessage(commitMessage);
ObjectId newCommitId = objectInserter.insert(builder);
objectInserter.flush();
@@ -572,7 +647,11 @@ public class ChangeEditModifier {
String mergeCommitMessageIfNecessary(String newCommitMessage, RevCommit commitToModify)
throws MergeConflictException;
- Optional<ChangeEdit> getEditIfNoModification(ObjectId newTreeId, String newCommitMessage);
+ Optional<ChangeEdit> getEditIfNoModification(
+ ObjectId newTreeId,
+ String newCommitMessage,
+ PersonIdent newAuthor,
+ PersonIdent newCommitter);
ChangeEdit updateEditInStorage(
Repository repository,
@@ -662,13 +741,29 @@ public class ChangeEditModifier {
@Override
public Optional<ChangeEdit> getEditIfNoModification(
- ObjectId newTreeId, String newCommitMessage) {
- if (!ObjectId.isEqual(newTreeId, changeEdit.getEditCommit().getTree())) {
+ ObjectId newTreeId,
+ String newCommitMessage,
+ PersonIdent newAuthor,
+ PersonIdent newCommitter) {
+ RevCommit editCommit = changeEdit.getEditCommit();
+
+ if (!ObjectId.isEqual(newTreeId, editCommit.getTree())) {
return Optional.empty();
}
- if (!Objects.equals(newCommitMessage, changeEdit.getEditCommit().getFullMessage())) {
+ if (!Objects.equals(newCommitMessage, editCommit.getFullMessage())) {
return Optional.empty();
}
+ if (!newAuthor.getName().equals(editCommit.getAuthorIdent().getName())
+ || !newAuthor.getEmailAddress().equals(editCommit.getAuthorIdent().getEmailAddress())) {
+ return Optional.empty();
+ }
+ if (!newCommitter.getName().equals(editCommit.getCommitterIdent().getName())
+ || !newCommitter
+ .getEmailAddress()
+ .equals(editCommit.getCommitterIdent().getEmailAddress())) {
+ return Optional.empty();
+ }
+
// Modifications are already contained in the change edit.
return Optional.of(changeEdit);
}
@@ -723,7 +818,10 @@ public class ChangeEditModifier {
@Override
public Optional<ChangeEdit> getEditIfNoModification(
- ObjectId newTreeId, String newCommitMessage) {
+ ObjectId newTreeId,
+ String newCommitMessage,
+ PersonIdent newAuthor,
+ PersonIdent newCommitter) {
return Optional.empty();
}
@@ -756,6 +854,7 @@ public class ChangeEditModifier {
this.gitReferenceUpdated = gitReferenceUpdated;
}
+ @CanIgnoreReturnValue
ChangeEdit createEdit(
Repository repository,
ChangeNotes notes,
diff --git a/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index ab41a37933..0189306411 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -239,8 +239,10 @@ public class ChangeEditUtil {
throws IOException, ResourceConflictException {
RevCommit parent = rw.parseCommit(basePatchSet.commitId());
if (parent.getTree().equals(edit.getTree())
- && edit.getFullMessage().equals(parent.getFullMessage())) {
- throw new ResourceConflictException("identical tree and message");
+ && edit.getFullMessage().equals(parent.getFullMessage())
+ && parent.getAuthorIdent().equals(edit.getAuthorIdent())
+ && parent.getCommitterIdent().equals(edit.getCommitterIdent())) {
+ throw new ResourceConflictException("identical tree, message, author and committer");
}
return writeSquashedCommit(rw, inserter, parent, edit);
}
@@ -283,7 +285,7 @@ public class ChangeEditUtil {
for (int i = 0; i < parent.getParentCount(); i++) {
mergeCommit.addParentId(parent.getParent(i));
}
- mergeCommit.setAuthor(parent.getAuthorIdent());
+ mergeCommit.setAuthor(edit.getAuthorIdent());
mergeCommit.setMessage(edit.getFullMessage());
mergeCommit.setCommitter(edit.getCommitterIdent());
mergeCommit.setTreeId(edit.getTree());
diff --git a/java/com/google/gerrit/server/edit/CommitModification.java b/java/com/google/gerrit/server/edit/CommitModification.java
index f9ed58e43e..f412e677be 100644
--- a/java/com/google/gerrit/server/edit/CommitModification.java
+++ b/java/com/google/gerrit/server/edit/CommitModification.java
@@ -16,8 +16,10 @@ package com.google.gerrit.server.edit;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.server.edit.tree.TreeModification;
import java.util.Optional;
+import org.eclipse.jgit.lib.PersonIdent;
@AutoValue
public abstract class CommitModification {
@@ -26,12 +28,17 @@ public abstract class CommitModification {
public abstract Optional<String> newCommitMessage();
+ public abstract Optional<PersonIdent> newAuthor();
+
+ public abstract Optional<PersonIdent> newCommitter();
+
public static Builder builder() {
return new AutoValue_CommitModification.Builder();
}
@AutoValue.Builder
public abstract static class Builder {
+ @CanIgnoreReturnValue
public Builder addTreeModification(TreeModification treeModification) {
treeModificationsBuilder().add(treeModification);
return this;
@@ -43,6 +50,10 @@ public abstract class CommitModification {
public abstract Builder newCommitMessage(String newCommitMessage);
+ public abstract Builder newAuthor(PersonIdent personIdent);
+
+ public abstract Builder newCommitter(PersonIdent personIdent);
+
public abstract CommitModification build();
}
}
diff --git a/java/com/google/gerrit/server/edit/package-info.java b/java/com/google/gerrit/server/edit/package-info.java
new file mode 100644
index 0000000000..dea8a96589
--- /dev/null
+++ b/java/com/google/gerrit/server/edit/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.edit;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java b/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
index 88390561e9..af0496ad22 100644
--- a/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
+++ b/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
@@ -19,10 +19,8 @@ import static com.google.gerrit.entities.Patch.FileMode.REGULAR_FILE;
import static java.util.Objects.requireNonNull;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
-import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
-import com.google.common.flogger.FluentLogger;
import com.google.common.io.ByteStreams;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.restapi.RawInput;
@@ -33,7 +31,6 @@ import java.util.Collections;
import java.util.List;
import org.eclipse.jgit.dircache.DirCacheEditor;
import org.eclipse.jgit.dircache.DirCacheEntry;
-import org.eclipse.jgit.errors.InvalidObjectIdException;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
@@ -41,8 +38,6 @@ import org.eclipse.jgit.lib.Repository;
/** A {@code TreeModification} which changes the content of a file. */
public class ChangeFileContentModification implements TreeModification {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
private final String filePath;
private final RawInput newContent;
private final Integer newGitFileMode;
@@ -73,8 +68,7 @@ public class ChangeFileContentModification implements TreeModification {
return ImmutableSet.of(filePath);
}
- @VisibleForTesting
- RawInput getNewContent() {
+ public RawInput getNewContent() {
return newContent;
}
@@ -121,15 +115,10 @@ public class ChangeFileContentModification implements TreeModification {
ObjectId newBlobObjectId = createNewBlobAndGetItsId();
dirCacheEntry.setObjectId(newBlobObjectId);
}
- // Previously, these two exceptions were swallowed. To improve the
- // situation, we log them now. However, we should think of a better
- // approach.
} catch (IOException e) {
String message =
String.format("Could not change the content of %s", dirCacheEntry.getPathString());
- logger.atSevere().withCause(e).log("%s", message);
- } catch (InvalidObjectIdException e) {
- logger.atSevere().withCause(e).log("Invalid object id in submodule link");
+ throw new IllegalStateException(message, e);
}
}
diff --git a/java/com/google/gerrit/server/edit/tree/TreeCreator.java b/java/com/google/gerrit/server/edit/tree/TreeCreator.java
index dfc1ffb7fe..b3ce564c91 100644
--- a/java/com/google/gerrit/server/edit/tree/TreeCreator.java
+++ b/java/com/google/gerrit/server/edit/tree/TreeCreator.java
@@ -18,9 +18,11 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.UsedAt;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEditor;
@@ -39,26 +41,41 @@ public class TreeCreator {
private final ObjectId baseTreeId;
private final ImmutableList<? extends ObjectId> baseParents;
+ private final Optional<ObjectInserter> objectInserter;
private final List<TreeModification> treeModifications = new ArrayList<>();
public static TreeCreator basedOn(RevCommit baseCommit) {
requireNonNull(baseCommit, "baseCommit is required");
- return new TreeCreator(baseCommit.getTree(), ImmutableList.copyOf(baseCommit.getParents()));
+ return new TreeCreator(
+ baseCommit.getTree(), ImmutableList.copyOf(baseCommit.getParents()), Optional.empty());
+ }
+
+ @UsedAt(UsedAt.Project.GOOGLE)
+ public static TreeCreator basedOn(RevCommit baseCommit, ObjectInserter objectInserter) {
+ requireNonNull(baseCommit, "baseCommit is required");
+ return new TreeCreator(
+ baseCommit.getTree(),
+ ImmutableList.copyOf(baseCommit.getParents()),
+ Optional.of(objectInserter));
}
public static TreeCreator basedOnTree(
ObjectId baseTreeId, ImmutableList<? extends ObjectId> baseParents) {
requireNonNull(baseTreeId, "baseTreeId is required");
- return new TreeCreator(baseTreeId, baseParents);
+ return new TreeCreator(baseTreeId, baseParents, Optional.empty());
}
public static TreeCreator basedOnEmptyTree() {
- return new TreeCreator(ObjectId.zeroId(), ImmutableList.of());
+ return new TreeCreator(ObjectId.zeroId(), ImmutableList.of(), Optional.empty());
}
- private TreeCreator(ObjectId baseTreeId, ImmutableList<? extends ObjectId> baseParents) {
+ private TreeCreator(
+ ObjectId baseTreeId,
+ ImmutableList<? extends ObjectId> baseParents,
+ Optional<ObjectInserter> objectInserter) {
this.baseTreeId = requireNonNull(baseTreeId, "baseTree is required");
this.baseParents = baseParents;
+ this.objectInserter = objectInserter;
}
/**
@@ -141,17 +158,22 @@ public class TreeCreator {
return pathEdits;
}
+ private ObjectId writeAndGetId(Repository repository, DirCache tree) throws IOException {
+ ObjectInserter oi = objectInserter.orElseGet(() -> repository.newObjectInserter());
+ try {
+ ObjectId treeId = tree.writeTree(oi);
+ oi.flush();
+ return treeId;
+ } finally {
+ if (objectInserter.isEmpty()) {
+ oi.close();
+ }
+ }
+ }
+
private static void applyPathEdits(DirCache tree, List<DirCacheEditor.PathEdit> pathEdits) {
DirCacheEditor dirCacheEditor = tree.editor();
pathEdits.forEach(dirCacheEditor::add);
dirCacheEditor.finish();
}
-
- private static ObjectId writeAndGetId(Repository repository, DirCache tree) throws IOException {
- try (ObjectInserter objectInserter = repository.newObjectInserter()) {
- ObjectId treeId = tree.writeTree(objectInserter);
- objectInserter.flush();
- return treeId;
- }
- }
}
diff --git a/java/com/google/gerrit/server/edit/tree/package-info.java b/java/com/google/gerrit/server/edit/tree/package-info.java
new file mode 100644
index 0000000000..4070c146cd
--- /dev/null
+++ b/java/com/google/gerrit/server/edit/tree/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.edit.tree;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/events/CommitReceivedEvent.java b/java/com/google/gerrit/server/events/CommitReceivedEvent.java
index de355ea9a8..e0bc1124ac 100644
--- a/java/com/google/gerrit/server/events/CommitReceivedEvent.java
+++ b/java/com/google/gerrit/server/events/CommitReceivedEvent.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.events;
import com.google.common.collect.ImmutableListMultimap;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.patch.DiffOperationsForCommitValidation;
import java.io.IOException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
@@ -36,6 +37,13 @@ public class CommitReceivedEvent extends RefEvent implements AutoCloseable {
public RevCommit commit;
public IdentifiedUser user;
+ /**
+ * Use this for computing the modified files of the received commits. Using {@link
+ * com.google.gerrit.server.patch.DiffOperations} from commit validators is not safe, see javadoc
+ * on {@link DiffOperationsForCommitValidation}.
+ */
+ public DiffOperationsForCommitValidation diffOperations;
+
public CommitReceivedEvent() {
super(TYPE);
}
@@ -48,7 +56,8 @@ public class CommitReceivedEvent extends RefEvent implements AutoCloseable {
Config repoConfig,
ObjectReader reader,
ObjectId commitId,
- IdentifiedUser user)
+ IdentifiedUser user,
+ DiffOperationsForCommitValidation diffOperations)
throws IOException {
this();
this.command = command;
@@ -59,6 +68,7 @@ public class CommitReceivedEvent extends RefEvent implements AutoCloseable {
this.revWalk = new RevWalk(reader);
this.commit = revWalk.parseCommit(commitId);
this.user = user;
+ this.diffOperations = diffOperations;
revWalk.parseBody(commit);
}
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index 46fe994fd6..2827f5976f 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -76,7 +76,6 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
-import java.util.Set;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -601,7 +600,7 @@ public class EventFactory {
}
private void addHashTags(ChangeAttribute changeAttribute, ChangeNotes notes) {
- Set<String> hashtags = notes.load().getHashtags();
+ ImmutableSet<String> hashtags = notes.load().getHashtags();
if (!hashtags.isEmpty()) {
changeAttribute.hashtags = new ArrayList<>(hashtags.size());
changeAttribute.hashtags.addAll(hashtags);
diff --git a/java/com/google/gerrit/server/events/EventTypes.java b/java/com/google/gerrit/server/events/EventTypes.java
index 4cc719885c..d08a3c2013 100644
--- a/java/com/google/gerrit/server/events/EventTypes.java
+++ b/java/com/google/gerrit/server/events/EventTypes.java
@@ -72,7 +72,7 @@ public class EventTypes {
*
* @return ImmutableMap of event types, Event classes.
*/
- public static Map<String, Class<?>> getRegisteredEvents() {
+ public static ImmutableMap<String, Class<?>> getRegisteredEvents() {
return ImmutableMap.copyOf(typesByString);
}
}
diff --git a/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index 4133c9022a..89aebdeb88 100644
--- a/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -53,6 +53,7 @@ import com.google.gerrit.extensions.events.VoteDeletedListener;
import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.config.GerritInstanceId;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.data.AccountAttribute;
import com.google.gerrit.server.data.ApprovalAttribute;
@@ -72,7 +73,9 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.stream.Collectors;
+import org.apache.commons.lang3.ObjectUtils;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
@@ -101,8 +104,7 @@ public class StreamEventsApiListener
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public static class StreamEventsApiListenerModule extends AbstractModule {
-
- private Config config;
+ private final Config config;
public StreamEventsApiListenerModule(Config config) {
this.config = config;
@@ -148,6 +150,8 @@ public class StreamEventsApiListener
private final ChangeNotes.Factory changeNotesFactory;
private final boolean enableDraftCommentEvents;
+ private final String gerritInstanceId;
+
@Inject
StreamEventsApiListener(
PluginItemContext<EventDispatcher> dispatcher,
@@ -156,7 +160,8 @@ public class StreamEventsApiListener
GitRepositoryManager repoManager,
PatchSetUtil psUtil,
ChangeNotes.Factory changeNotesFactory,
- @GerritServerConfig Config config) {
+ @GerritServerConfig Config config,
+ @Nullable @GerritInstanceId String gerritInstanceId) {
this.dispatcher = dispatcher;
this.eventFactory = eventFactory;
this.projectCache = projectCache;
@@ -165,6 +170,7 @@ public class StreamEventsApiListener
this.changeNotesFactory = changeNotesFactory;
this.enableDraftCommentEvents =
config.getBoolean("event", "stream-events", "enableDraftCommentEvents", false);
+ this.gerritInstanceId = gerritInstanceId;
}
private ChangeNotes getNotes(ChangeInfo info) {
@@ -345,6 +351,12 @@ public class StreamEventsApiListener
@Override
public void onNewProjectCreated(NewProjectCreatedListener.Event ev) {
+ if (!Objects.equals(ev.getInstanceId(), gerritInstanceId)) {
+ logger.atFine().log(
+ "Ignoring project-created event for project %s (instanceId: %s)",
+ ev.getProjectName(), ObjectUtils.firstNonNull(ev.getInstanceId(), "Not defined"));
+ return;
+ }
ProjectCreatedEvent event = new ProjectCreatedEvent();
event.projectName = ev.getProjectName();
event.headName = ev.getHeadName();
diff --git a/java/com/google/gerrit/server/events/package-info.java b/java/com/google/gerrit/server/events/package-info.java
new file mode 100644
index 0000000000..cf589fed78
--- /dev/null
+++ b/java/com/google/gerrit/server/events/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.events;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
index 32ec401a84..cd917459c3 100644
--- a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
+++ b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
@@ -25,4 +25,39 @@ public class ExperimentFeaturesConstants {
/** Features, enabled by default in the current release. */
public static final ImmutableSet<String> DEFAULT_ENABLED_FEATURES = ImmutableSet.of();
+
+ /**
+ * If true, gerrit checks implicit merges on each merge operations.
+ *
+ * <p>If only this option is set (without {@link
+ * #GERRIT_BACKEND_FEATURE_REJECT_IMPLICIT_MERGES_ON_MERGE}) - then the outcome of the check is
+ * only logged and doesn't block merge operation. Any exceptions during the check are logged and
+ * doesn't block merge operation.
+ */
+ public static String GERRIT_BACKEND_FEATURE_CHECK_IMPLICIT_MERGES_ON_MERGE =
+ "GerritBackendFeature__check_implicit_merges_on_merge";
+
+ /**
+ * If true, gerrit rejects implicit merges on merge.
+ *
+ * <p>Should work together with {@link #GERRIT_BACKEND_FEATURE_CHECK_IMPLICIT_MERGES_ON_MERGE}.
+ *
+ * <p>If {@link #GERRIT_BACKEND_FEATURE_ALWAYS_REJECT_IMPLICIT_MERGES_ON_MERGE} is set to true
+ * then implicit merges are rejected even if rejectImplicitMerges in project config is set to
+ * false.
+ *
+ * <p>If {@link #GERRIT_BACKEND_FEATURE_ALWAYS_REJECT_IMPLICIT_MERGES_ON_MERGE} is set to false
+ * then implicit merges are rejected only if rejectImplicitMerges in project config is set to
+ * true.
+ */
+ public static String GERRIT_BACKEND_FEATURE_REJECT_IMPLICIT_MERGES_ON_MERGE =
+ "GerritBackendFeature__reject_implicit_merges_on_merge";
+
+ /** If true, gerrit ignores rejectImplicitMerges setting from the project config on merge. */
+ public static String GERRIT_BACKEND_FEATURE_ALWAYS_REJECT_IMPLICIT_MERGES_ON_MERGE =
+ "GerritBackendFeature__always_reject_implicit_merges_on_merge";
+
+ /** Whether we allow fix suggestions in HumanComments. */
+ public static final String ALLOW_FIX_SUGGESTIONS_IN_COMMENTS =
+ "GerritBackendFeature__allow_fix_suggestions_in_comments";
}
diff --git a/java/com/google/gerrit/server/experiments/package-info.java b/java/com/google/gerrit/server/experiments/package-info.java
new file mode 100644
index 0000000000..eeaeac61bc
--- /dev/null
+++ b/java/com/google/gerrit/server/experiments/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.experiments;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/extensions/events/package-info.java b/java/com/google/gerrit/server/extensions/events/package-info.java
new file mode 100644
index 0000000000..c2f060e726
--- /dev/null
+++ b/java/com/google/gerrit/server/extensions/events/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.extensions.events;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/extensions/webui/package-info.java b/java/com/google/gerrit/server/extensions/webui/package-info.java
new file mode 100644
index 0000000000..e5d7b488f5
--- /dev/null
+++ b/java/com/google/gerrit/server/extensions/webui/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.extensions.webui;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/fixes/package-info.java b/java/com/google/gerrit/server/fixes/package-info.java
new file mode 100644
index 0000000000..f36b397e99
--- /dev/null
+++ b/java/com/google/gerrit/server/fixes/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.fixes;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/fixes/testing/BUILD b/java/com/google/gerrit/server/fixes/testing/BUILD
index 765e8bf9f6..cb7894c801 100644
--- a/java/com/google/gerrit/server/fixes/testing/BUILD
+++ b/java/com/google/gerrit/server/fixes/testing/BUILD
@@ -12,6 +12,7 @@ java_library(
"//java/com/google/gerrit/truth",
"//lib:guava",
"//lib:jgit",
+ "//lib/errorprone:annotations",
"//lib/truth",
],
)
diff --git a/java/com/google/gerrit/server/fixes/testing/package-info.java b/java/com/google/gerrit/server/fixes/testing/package-info.java
new file mode 100644
index 0000000000..b79464294c
--- /dev/null
+++ b/java/com/google/gerrit/server/fixes/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.fixes.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/git/ChangesByProjectCache.java b/java/com/google/gerrit/server/git/ChangesByProjectCache.java
index df91891544..1e714978a0 100644
--- a/java/com/google/gerrit/server/git/ChangesByProjectCache.java
+++ b/java/com/google/gerrit/server/git/ChangesByProjectCache.java
@@ -30,8 +30,8 @@ public interface ChangesByProjectCache {
}
public static class Module extends AbstractModule {
- private UseIndex useIndex;
- private @GerritServerConfig Config config;
+ private final UseIndex useIndex;
+ private final Config config;
public Module(UseIndex useIndex, @GerritServerConfig Config config) {
this.useIndex = useIndex;
diff --git a/java/com/google/gerrit/server/git/ChangesByProjectCacheImpl.java b/java/com/google/gerrit/server/git/ChangesByProjectCacheImpl.java
index 094287b7be..ed16006b9f 100644
--- a/java/com/google/gerrit/server/git/ChangesByProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/git/ChangesByProjectCacheImpl.java
@@ -18,13 +18,13 @@ import com.google.auto.value.AutoValue;
import com.google.common.cache.Cache;
import com.google.common.cache.Weigher;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.git.ChangesByProjectCache.UseIndex;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
@@ -94,7 +94,7 @@ public class ChangesByProjectCacheImpl implements ChangesByProjectCache {
if (projectChanges != null) {
return projectChanges
.getUpdatedChangeDatas(
- project, repo, cdFactory, ChangeNotes.Factory.scanChangeIds(repo), "Updating")
+ project, cdFactory, ChangeNotes.Factory.scanChangeIds(repo), "Updating")
.stream();
}
if (UseIndex.TRUE.equals(useIndex)) {
@@ -114,19 +114,18 @@ public class ChangesByProjectCacheImpl implements ChangesByProjectCache {
}
return projectChanges.getUpdatedChangeDatas(
project,
- repo,
cdFactory,
ChangeNotes.Factory.scanChangeIds(repo),
ours == projectChanges ? "Scanning" : "Updating");
}
- private Collection<ChangeData> queryChangeDatasAndLoad(Project.NameKey project) {
- Collection<ChangeData> cds = queryChangeDatas(project);
+ private List<ChangeData> queryChangeDatasAndLoad(Project.NameKey project) {
+ List<ChangeData> cds = queryChangeDatas(project);
cache.put(project, new CachedProjectChanges(cds));
return cds;
}
- private Collection<ChangeData> queryChangeDatas(Project.NameKey project) {
+ private List<ChangeData> queryChangeDatas(Project.NameKey project) {
try (TraceTimer timer =
TraceContext.newTimer(
"Querying changes of project", Metadata.builder().projectName(project.get()).build())) {
@@ -151,7 +150,6 @@ public class ChangesByProjectCacheImpl implements ChangesByProjectCache {
public Collection<ChangeData> getUpdatedChangeDatas(
Project.NameKey project,
- Repository repo,
ChangeData.Factory cdFactory,
Map<Change.Id, ObjectId> metaObjectIdByChange,
String operation) {
@@ -180,6 +178,7 @@ public class ChangesByProjectCacheImpl implements ChangesByProjectCache {
}
}
+ @CanIgnoreReturnValue
public CachedProjectChanges update(ChangeData old, ChangeData updated) {
if (old != null) {
if (old.isPrivateOrThrow()) {
@@ -195,6 +194,7 @@ public class ChangesByProjectCacheImpl implements ChangesByProjectCache {
return insert(updated);
}
+ @CanIgnoreReturnValue
public CachedProjectChanges insert(ChangeData cd) {
if (cd.isPrivateOrThrow()) {
privateChangeById.put(
diff --git a/java/com/google/gerrit/server/git/GarbageCollection.java b/java/com/google/gerrit/server/git/GarbageCollection.java
index 30330eb688..1029b17a02 100644
--- a/java/com/google/gerrit/server/git/GarbageCollection.java
+++ b/java/com/google/gerrit/server/git/GarbageCollection.java
@@ -16,6 +16,7 @@ package com.google.gerrit.server.git;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GarbageCollectionResult;
import com.google.gerrit.common.data.GarbageCollectionResult.GcError;
@@ -64,15 +65,18 @@ public class GarbageCollection {
this.listeners = listeners;
}
+ @CanIgnoreReturnValue
public GarbageCollectionResult run(List<Project.NameKey> projectNames) {
return run(projectNames, null);
}
+ @CanIgnoreReturnValue
public GarbageCollectionResult run(List<Project.NameKey> projectNames, PrintWriter writer) {
return run(projectNames, gcConfig.isAggressive(), writer);
}
/** Runs GC on the given projects, serially. Progress is written to writer if non-null. */
+ @CanIgnoreReturnValue
public GarbageCollectionResult run(
List<Project.NameKey> projectNames, boolean aggressive, @Nullable PrintWriter writer) {
GarbageCollectionResult result = new GarbageCollectionResult();
diff --git a/java/com/google/gerrit/server/git/GarbageCollectionQueue.java b/java/com/google/gerrit/server/git/GarbageCollectionQueue.java
index 5df9ab5b4c..671faf960a 100644
--- a/java/com/google/gerrit/server/git/GarbageCollectionQueue.java
+++ b/java/com/google/gerrit/server/git/GarbageCollectionQueue.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.git;
import com.google.common.collect.Sets;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Project;
import com.google.inject.Singleton;
import java.util.Collection;
@@ -26,6 +27,7 @@ import java.util.Set;
public class GarbageCollectionQueue {
private final Set<Project.NameKey> projectsScheduledForGc = new HashSet<>();
+ @CanIgnoreReturnValue
public synchronized Set<Project.NameKey> addAll(Collection<Project.NameKey> projects) {
Set<Project.NameKey> added = Sets.newLinkedHashSetWithExpectedSize(projects.size());
for (Project.NameKey p : projects) {
diff --git a/java/com/google/gerrit/server/git/GroupCollector.java b/java/com/google/gerrit/server/git/GroupCollector.java
index 72d8bd9784..b87eeeac57 100644
--- a/java/com/google/gerrit/server/git/GroupCollector.java
+++ b/java/com/google/gerrit/server/git/GroupCollector.java
@@ -75,11 +75,11 @@ import org.eclipse.jgit.revwalk.RevCommit;
public class GroupCollector {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- public static List<String> getDefaultGroups(ObjectId commit) {
+ public static ImmutableList<String> getDefaultGroups(ObjectId commit) {
return ImmutableList.of(commit.name());
}
- public static List<String> getGroups(RevisionResource rsrc) {
+ public static ImmutableList<String> getGroups(RevisionResource rsrc) {
if (rsrc.getEdit().isPresent()) {
// Groups for an edit are just the base revision's groups, since they have
// the same parent.
diff --git a/java/com/google/gerrit/server/git/InMemoryInserter.java b/java/com/google/gerrit/server/git/InMemoryInserter.java
index 8d12f2b7bd..1328b5927e 100644
--- a/java/com/google/gerrit/server/git/InMemoryInserter.java
+++ b/java/com/google/gerrit/server/git/InMemoryInserter.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.git;
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
@@ -60,10 +61,12 @@ public class InMemoryInserter extends ObjectInserter {
}
@Override
+ @CanIgnoreReturnValue
public ObjectId insert(int type, byte[] data, int off, int len) {
return insert(InsertedObject.create(type, data, off, len));
}
+ @CanIgnoreReturnValue
public ObjectId insert(InsertedObject obj) {
inserted.put(obj.id(), obj);
return obj.id();
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index 0d6885e538..1adbb67348 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -27,6 +27,7 @@ import com.google.auto.factory.AutoFactory;
import com.google.auto.factory.Provided;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
@@ -240,6 +241,8 @@ public class MergeUtil {
if (m.merge(mergeTip, originalCommit)) {
filesWithGitConflicts = null;
tree = m.getResultTreeId();
+ logger.atFine().log(
+ "CherryPick treeId=%s (no conflicts, inserter: %s)", tree.name(), m.getObjectInserter());
if (tree.equals(mergeTip.getTree()) && !ignoreIdenticalTree) {
throw new MergeIdenticalTreeException("identical tree");
}
@@ -260,6 +263,32 @@ public class MergeUtil {
// For merging with conflict markers we need a ResolveMerger, double-check that we have one.
checkState(m instanceof ResolveMerger, "allow conflicts is not supported");
+
+ if (m.getResultTreeId() != null) {
+ // Merging with conflicts below uses the same DirCache instance that has been used by the
+ // Merger to attempt the merge without conflicts.
+ //
+ // The Merger uses the DirCache to do the updates, and in particular to write the result
+ // tree. DirCache caches a single DirCacheTree instance that is used to write the result
+ // tree, but it writes the result tree only if there were no conflicts.
+ //
+ // Merging with conflicts uses the same DirCache instance to write the tree with conflicts
+ // that has been used by the Merger. This means if the Merger unexpectedly wrote a result
+ // tree although there had been conflicts, then merging with conflicts uses the same
+ // DirCacheTree instance to write the tree with conflicts. However DirCacheTree#writeTree
+ // writes a tree only once and then that tree is cached. Further invocations of
+ // DirCacheTree#writeTree have no effect and return the previously created tree. This means
+ // merging with conflicts can only successfully create the tree with conflicts if the Merger
+ // didn't write a result tree yet. Hence this is checked here and we log a warning if the
+ // result tree was already written.
+ logger.atWarning().log(
+ "result tree has already been written: %s (merge: %s, conflicts: %s, failed: %s)",
+ m,
+ m.getResultTreeId().name(),
+ ((ResolveMerger) m).getUnmergedPaths(),
+ ((ResolveMerger) m).getFailingPaths());
+ }
+
Map<String, MergeResult<? extends Sequence>> mergeResults =
((ResolveMerger) m).getMergeResults();
@@ -272,6 +301,8 @@ public class MergeUtil {
tree =
mergeWithConflicts(
rw, inserter, dc, "HEAD", mergeTip, "CHANGE", originalCommit, mergeResults);
+ logger.atFine().log(
+ "AutoMerge treeId=%s (with conflicts, inserter: %s)", tree.name(), inserter);
}
CommitBuilder cherryPickCommit = new CommitBuilder();
@@ -283,6 +314,7 @@ public class MergeUtil {
matchAuthorToCommitterDate(project, cherryPickCommit);
CodeReviewCommit commit = rw.parseCommit(inserter.insert(cherryPickCommit));
commit.setFilesWithGitConflicts(filesWithGitConflicts);
+ logger.atFine().log("CherryPick commitId=%s", commit.name());
return commit;
}
@@ -462,8 +494,32 @@ public class MergeUtil {
tree = m.getResultTreeId();
} else {
List<String> conflicts = ImmutableList.of();
+ Map<String, ResolveMerger.MergeFailureReason> failed = ImmutableMap.of();
if (m instanceof ResolveMerger) {
conflicts = ((ResolveMerger) m).getUnmergedPaths();
+ failed = ((ResolveMerger) m).getFailingPaths();
+ }
+
+ if (m.getResultTreeId() != null) {
+ // Merging with conflicts below uses the same DirCache instance that has been used by the
+ // Merger to attempt the merge without conflicts.
+ //
+ // The Merger uses the DirCache to do the updates, and in particular to write the result
+ // tree. DirCache caches a single DirCacheTree instance that is used to write the result
+ // tree, but it writes the result tree only if there were no conflicts.
+ //
+ // Merging with conflicts uses the same DirCache instance to write the tree with conflicts
+ // that has been used by the Merger. This means if the Merger unexpectedly wrote a result
+ // tree although there had been conflicts, then merging with conflicts uses the same
+ // DirCacheTree instance to write the tree with conflicts. However DirCacheTree#writeTree
+ // writes a tree only once and then that tree is cached. Further invocations of
+ // DirCacheTree#writeTree have no effect and return the previously created tree. This means
+ // merging with conflicts can only successfully create the tree with conflicts if the Merger
+ // didn't write a result tree yet. Hence this is checked here and we log a warning if the
+ // result tree was already written.
+ logger.atWarning().log(
+ "result tree has already been written: %s (merge: %s, conflicts: %s, failed: %s)",
+ m, m.getResultTreeId().name(), conflicts, failed);
}
if (!allowConflicts) {
@@ -841,7 +897,14 @@ public class MergeUtil {
}
}
- private static CodeReviewCommit failed(
+ /**
+ * Marks all commits that are reachable from the given commit {@code n} as failed by setting the
+ * provided {@code failure} status code on them.
+ *
+ * <p>If the same commits are retrieved from the same {@link CodeReviewRevWalk} instance later the
+ * status code that we set here can be read there.
+ */
+ private static void failed(
CodeReviewRevWalk rw,
CodeReviewCommit mergeTip,
CodeReviewCommit n,
@@ -854,7 +917,6 @@ public class MergeUtil {
while ((failed = rw.next()) != null) {
failed.setStatusCode(failure);
}
- return failed;
}
public CodeReviewCommit writeMergeCommit(
@@ -993,6 +1055,12 @@ public class MergeUtil {
@Override
public void close() {}
+
+ @Override
+ public String toString() {
+ return String.format(
+ "%s (wrapped inserter: %s)", super.toString(), inserter.toString());
+ }
},
repoConfig);
}
diff --git a/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index ab5c988813..716e48f449 100644
--- a/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -24,6 +24,7 @@ import com.google.common.base.Strings;
import com.google.common.base.Ticker;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.UncheckedExecutionException;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.server.CancellationMetrics;
import com.google.gerrit.server.cancellation.RequestStateProvider;
import com.google.inject.assistedinject.Assisted;
@@ -297,6 +298,7 @@ public class MultiProgressMonitor implements RequestStateProvider {
*
* @see #waitFor(Future, long, TimeUnit, long, TimeUnit)
*/
+ @CanIgnoreReturnValue
public <T> T waitFor(Future<T> workerFuture) {
try {
return waitFor(
@@ -329,6 +331,7 @@ public class MultiProgressMonitor implements RequestStateProvider {
* @throws TimeoutException if this thread or a worker thread was interrupted, the worker was
* cancelled, or timed out waiting for a worker to call {@link #end()}.
*/
+ @CanIgnoreReturnValue
public <T> T waitFor(
Future<T> workerFuture,
long taskTimeoutTime,
@@ -360,6 +363,7 @@ public class MultiProgressMonitor implements RequestStateProvider {
*
* @see #waitForNonFinalTask(Future, long, TimeUnit, long, TimeUnit)
*/
+ @CanIgnoreReturnValue
public <T> T waitForNonFinalTask(Future<T> workerFuture) {
try {
return waitForNonFinalTask(workerFuture, 0, null, 0, null);
diff --git a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
index 83024e3f45..d8afbbb7fa 100644
--- a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
+++ b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
@@ -155,13 +155,14 @@ public class SearchingChangeCacheImpl
.byProject(key);
Map<Change.Id, CachedChange> result = new HashMap<>(cds.size());
for (ChangeData cd : cds) {
- if (result.containsKey(cd.getId())) {
+ final Change.Id cdUniqueId = cd.virtualId();
+ if (result.containsKey(cdUniqueId)) {
logger.atWarning().log(
"Duplicate changes returned from change query by project %s: %s, %s",
- key, cd.change(), result.get(cd.getId()).change());
+ key, cd.change(), result.get(cdUniqueId).change());
}
result.put(
- cd.getId(),
+ cdUniqueId,
new AutoValue_SearchingChangeCacheImpl_CachedChange(cd.change(), cd.reviewers()));
}
return List.copyOf(result.values());
diff --git a/java/com/google/gerrit/server/git/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java
index 86d6c7c7d2..d51ee5eee0 100644
--- a/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/java/com/google/gerrit/server/git/WorkQueue.java
@@ -19,6 +19,7 @@ import static java.util.stream.Collectors.toList;
import com.google.common.base.CaseFormat;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.events.LifecycleListener;
@@ -46,6 +47,7 @@ import java.util.concurrent.Delayed;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
+import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableScheduledFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
@@ -90,8 +92,8 @@ public class WorkQueue {
private final WorkQueue workQueue;
@Inject
- Lifecycle(WorkQueue workQeueue) {
- this.workQueue = workQeueue;
+ Lifecycle(WorkQueue workQueue) {
+ this.workQueue = workQueue;
}
@Override
@@ -480,8 +482,9 @@ public class WorkQueue {
@Override
protected <V> RunnableScheduledFuture<V> decorateTask(
- Callable<V> callable, RunnableScheduledFuture<V> task) {
- throw new UnsupportedOperationException("Callable not implemented");
+ Callable<V> callable, RunnableScheduledFuture<V> r) {
+ FutureTask<V> ft = new FutureTask<>(callable);
+ return decorateTask(ft, r);
}
void remove(Task<?> task) {
@@ -621,6 +624,7 @@ public class WorkQueue {
}
@Override
+ @CanIgnoreReturnValue
public boolean cancel(boolean mayInterruptIfRunning) {
if (task.cancel(mayInterruptIfRunning)) {
// Tiny abuse of runningState: if the task needs to know it
@@ -695,7 +699,7 @@ public class WorkQueue {
try {
executor.onStart(this);
runningState.set(State.RUNNING);
- Thread.currentThread().setName(oldThreadName + "[" + task.toString() + "]");
+ Thread.currentThread().setName(oldThreadName + "[" + this + "]");
task.run();
} finally {
Thread.currentThread().setName(oldThreadName);
diff --git a/java/com/google/gerrit/server/schema/VersionedAccountPreferences.java b/java/com/google/gerrit/server/git/meta/VersionedConfigFile.java
index 468c26b162..de8dabd1f8 100644
--- a/java/com/google/gerrit/server/schema/VersionedAccountPreferences.java
+++ b/java/com/google/gerrit/server/git/meta/VersionedConfigFile.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -12,34 +12,50 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.schema;
+package com.google.gerrit.server.git.meta;
import com.google.common.base.Strings;
-import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.server.git.meta.VersionedMetaData;
import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
-/** Preferences for user accounts during schema migrations. */
-class VersionedAccountPreferences extends VersionedMetaData {
- static final String PREFERENCES = "preferences.config";
+/**
+ * Versioned configuration file living in git
+ *
+ * <p>This class is a low-level API that allows callers to read the config directly from a
+ * repository and make updates to it.
+ */
+public class VersionedConfigFile extends VersionedMetaData {
+ protected final String ref;
+ protected final String fileName;
+ protected final String defaultOnSaveMessage;
+ protected Config cfg;
- static VersionedAccountPreferences forUser(Account.Id id) {
- return new VersionedAccountPreferences(RefNames.refsUsers(id));
+ public VersionedConfigFile(String fileName) {
+ this(RefNames.REFS_CONFIG, fileName);
}
- static VersionedAccountPreferences forDefault() {
- return new VersionedAccountPreferences(RefNames.REFS_USERS_DEFAULT);
+ public VersionedConfigFile(String ref, String fileName) {
+ this(ref, fileName, "Updated configuration\n");
}
- private final String ref;
- private Config cfg;
-
- protected VersionedAccountPreferences(String ref) {
+ public VersionedConfigFile(String ref, String fileName, String defaultOnSaveMessage) {
this.ref = ref;
+ this.fileName = fileName;
+ this.defaultOnSaveMessage = defaultOnSaveMessage;
+ }
+
+ public Config getConfig() {
+ if (cfg == null) {
+ cfg = new Config();
+ }
+ return cfg;
+ }
+
+ protected String getFileName() {
+ return fileName;
}
@Override
@@ -47,21 +63,17 @@ class VersionedAccountPreferences extends VersionedMetaData {
return ref;
}
- Config getConfig() {
- return cfg;
- }
-
@Override
protected void onLoad() throws IOException, ConfigInvalidException {
- cfg = readConfig(PREFERENCES);
+ cfg = readConfig(fileName);
}
@Override
protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
if (Strings.isNullOrEmpty(commit.getMessage())) {
- commit.setMessage("Updated preferences\n");
+ commit.setMessage(defaultOnSaveMessage);
}
- saveConfig(PREFERENCES, cfg);
+ saveConfig(fileName, cfg);
return true;
}
}
diff --git a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
index 4f0bde8f54..5e8f99a787 100644
--- a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
+++ b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
@@ -19,6 +19,7 @@ import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdate
import com.google.common.base.MoreObjects;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
import com.google.gerrit.git.GitUpdateFailureException;
@@ -204,6 +205,7 @@ public abstract class VersionedMetaData {
* @throws IOException if there is a storage problem and the update cannot be executed as
* requested or if it failed because of a concurrent update to the same reference
*/
+ @CanIgnoreReturnValue
public RevCommit commit(MetaDataUpdate update) throws IOException {
try (BatchMetaDataUpdate batch = openUpdate(update)) {
batch.write(update.getCommitBuilder());
@@ -241,6 +243,7 @@ public abstract class VersionedMetaData {
* @throws IOException if there is a storage problem and the update cannot be executed as
* requested or if it failed because of a concurrent update to the same reference
*/
+ @CanIgnoreReturnValue
public RevCommit commitToNewRef(MetaDataUpdate update, String refName) throws IOException {
try (BatchMetaDataUpdate batch = openUpdate(update)) {
batch.write(update.getCommitBuilder());
@@ -253,10 +256,13 @@ public abstract class VersionedMetaData {
void write(VersionedMetaData config, CommitBuilder commit) throws IOException;
+ @CanIgnoreReturnValue
RevCommit createRef(String refName) throws IOException;
+ @CanIgnoreReturnValue
RevCommit commit() throws IOException;
+ @CanIgnoreReturnValue
RevCommit commitAt(ObjectId revision) throws IOException;
@Override
diff --git a/java/com/google/gerrit/server/git/meta/package-info.java b/java/com/google/gerrit/server/git/meta/package-info.java
new file mode 100644
index 0000000000..48fd8a6c87
--- /dev/null
+++ b/java/com/google/gerrit/server/git/meta/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.git.meta;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/git/package-info.java b/java/com/google/gerrit/server/git/package-info.java
new file mode 100644
index 0000000000..7b95aed297
--- /dev/null
+++ b/java/com/google/gerrit/server/git/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.git;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index 088493483d..cb1af07759 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.server.quota.QuotaGroupDefinitions.REPOSITORY_SIZE_GROUP;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.google.gerrit.common.Nullable;
@@ -69,7 +70,6 @@ import com.google.inject.name.Named;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collection;
-import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@@ -422,12 +422,14 @@ public class AsyncReceiveCommits {
int totalChanges = 0;
if (result.magicPush()) {
pushType = PushType.CREATE_REPLACE;
- Set<Change.Id> created = result.changes().get(ReceiveCommitsResult.ChangeStatus.CREATED);
- Set<Change.Id> replaced = result.changes().get(ReceiveCommitsResult.ChangeStatus.REPLACED);
+ ImmutableSet<Change.Id> created =
+ result.changes().get(ReceiveCommitsResult.ChangeStatus.CREATED);
+ ImmutableSet<Change.Id> replaced =
+ result.changes().get(ReceiveCommitsResult.ChangeStatus.REPLACED);
metrics.changes.record(pushType, created.size() + replaced.size());
totalChanges = replaced.size() + created.size();
} else {
- Set<Change.Id> autoclosed =
+ ImmutableSet<Change.Id> autoclosed =
result.changes().get(ReceiveCommitsResult.ChangeStatus.AUTOCLOSED);
if (!autoclosed.isEmpty()) {
pushType = PushType.AUTOCLOSE;
diff --git a/java/com/google/gerrit/server/git/receive/BUILD b/java/com/google/gerrit/server/git/receive/BUILD
index 5c1cf52227..41942756c9 100644
--- a/java/com/google/gerrit/server/git/receive/BUILD
+++ b/java/com/google/gerrit/server/git/receive/BUILD
@@ -28,6 +28,7 @@ java_library(
"//lib:jgit",
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
"//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java b/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
index f680b7bb8e..6a43719e96 100644
--- a/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
+++ b/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
@@ -32,6 +32,7 @@ import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.logging.TraceContext.TraceTimer;
+import com.google.gerrit.server.patch.DiffOperationsForCommitValidation;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.ssh.SshInfo;
@@ -108,6 +109,7 @@ public class BranchCommitValidator {
Result validateCommit(
Repository repository,
ObjectReader objectReader,
+ DiffOperationsForCommitValidation diffOperationsForCommitValidation,
ReceiveCommand cmd,
RevCommit commit,
ImmutableListMultimap<String, String> pushOptions,
@@ -116,7 +118,16 @@ public class BranchCommitValidator {
@Nullable Change change)
throws IOException {
return validateCommit(
- repository, objectReader, cmd, commit, pushOptions, isMerged, rejectCommits, change, false);
+ repository,
+ objectReader,
+ diffOperationsForCommitValidation,
+ cmd,
+ commit,
+ pushOptions,
+ isMerged,
+ rejectCommits,
+ change,
+ false);
}
/**
@@ -134,6 +145,7 @@ public class BranchCommitValidator {
Result validateCommit(
Repository repository,
ObjectReader objectReader,
+ DiffOperationsForCommitValidation diffOperationsForCommitValidation,
ReceiveCommand cmd,
RevCommit commit,
ImmutableListMultimap<String, String> pushOptions,
@@ -153,7 +165,8 @@ public class BranchCommitValidator {
new Config(repository.getConfig()),
objectReader,
commit,
- user)) {
+ user,
+ diffOperationsForCommitValidation)) {
CommitValidators validators;
if (isMerged) {
validators =
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index f60a0c7033..a5e20c6c72 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -161,6 +161,7 @@ import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.gerrit.server.mail.MailUtil.MailRecipients;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.patch.AutoMerger;
+import com.google.gerrit.server.patch.DiffOperationsForCommitValidation;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.GlobalPermission;
@@ -183,10 +184,12 @@ import com.google.gerrit.server.submit.MergeOp;
import com.google.gerrit.server.submit.MergeOpRepoManager;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.BatchUpdates;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.PostUpdateContext;
import com.google.gerrit.server.update.RepoContext;
import com.google.gerrit.server.update.RepoOnlyOp;
+import com.google.gerrit.server.update.RepoView;
import com.google.gerrit.server.update.RetryHelper;
import com.google.gerrit.server.update.SubmissionExecutor;
import com.google.gerrit.server.update.SubmissionListener;
@@ -365,6 +368,7 @@ class ReceiveCommits {
private final AccountResolver accountResolver;
private final AllProjectsName allProjectsName;
private final BatchUpdate.Factory batchUpdateFactory;
+ private final BatchUpdates batchUpdates;
private final CancellationMetrics cancellationMetrics;
private final ChangeEditUtil editUtil;
private final ChangeIndexer indexer;
@@ -380,6 +384,7 @@ class ReceiveCommits {
private final CreateGroupPermissionSyncer createGroupPermissionSyncer;
private final CreateRefControl createRefControl;
private final DeadlineChecker.Factory deadlineCheckerFactory;
+ private final DiffOperationsForCommitValidation.Factory diffOperationsForCommitValidationFactory;
private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
private final DynamicSet<PluginPushOption> pluginPushOptions;
private final PluginSetContext<ReceivePackInitializer> initializers;
@@ -452,6 +457,7 @@ class ReceiveCommits {
AccountResolver accountResolver,
AllProjectsName allProjectsName,
BatchUpdate.Factory batchUpdateFactory,
+ BatchUpdates batchUpdates,
CancellationMetrics cancellationMetrics,
ProjectConfig.Factory projectConfigFactory,
@GerritServerConfig Config config,
@@ -467,6 +473,7 @@ class ReceiveCommits {
CreateGroupPermissionSyncer createGroupPermissionSyncer,
CreateRefControl createRefControl,
DeadlineChecker.Factory deadlineCheckerFactory,
+ DiffOperationsForCommitValidation.Factory diffOperationsForCommitValidationFactory,
DynamicMap<ProjectConfigEntry> pluginConfigEntries,
DynamicSet<PluginPushOption> pluginPushOptions,
PluginSetContext<ReceivePackInitializer> initializers,
@@ -508,6 +515,7 @@ class ReceiveCommits {
this.accountResolver = accountResolver;
this.allProjectsName = allProjectsName;
this.batchUpdateFactory = batchUpdateFactory;
+ this.batchUpdates = batchUpdates;
this.cancellationMetrics = cancellationMetrics;
this.changeFormatter = changeFormatterProvider.get();
this.changeUtil = changeUtil;
@@ -519,6 +527,7 @@ class ReceiveCommits {
this.createRefControl = createRefControl;
this.createGroupPermissionSyncer = createGroupPermissionSyncer;
this.deadlineCheckerFactory = deadlineCheckerFactory;
+ this.diffOperationsForCommitValidationFactory = diffOperationsForCommitValidationFactory;
this.editUtil = editUtil;
this.hashtagsFactory = hashtagsFactory;
this.setTopicFactory = setTopicFactory;
@@ -736,85 +745,94 @@ class ReceiveCommits {
return;
}
- List<ReceiveCommand> magicCommands = new ArrayList<>();
- List<ReceiveCommand> regularCommands = new ArrayList<>();
+ try (ObjectInserter ins = repo.newObjectInserter();
+ ObjectReader reader = ins.newReader();
+ RevWalk globalRevWalk = new RevWalk(reader)) {
+ globalRevWalk.setRetainBody(false);
- for (ReceiveCommand cmd : commands) {
- if (MagicBranch.isMagicBranch(cmd.getRefName())) {
- magicCommands.add(cmd);
- } else {
- regularCommands.add(cmd);
+ List<ReceiveCommand> magicCommands = new ArrayList<>();
+ List<ReceiveCommand> regularCommands = new ArrayList<>();
+
+ for (ReceiveCommand cmd : commands) {
+ if (MagicBranch.isMagicBranch(cmd.getRefName())) {
+ magicCommands.add(cmd);
+ } else {
+ regularCommands.add(cmd);
+ }
}
- }
- if (!magicCommands.isEmpty() && !regularCommands.isEmpty()) {
- rejectRemaining(commands, "cannot combine normal pushes and magic pushes");
- return;
- }
+ if (!magicCommands.isEmpty() && !regularCommands.isEmpty()) {
+ rejectRemaining(commands, "cannot combine normal pushes and magic pushes");
+ return;
+ }
- try {
- if (!magicCommands.isEmpty()) {
- parseMagicBranch(Iterables.getLast(magicCommands));
- // Using the submit option submits the created change(s) immediately without checking labels
- // nor submit rules. Hence we shouldn't record such pushes as "magic" which implies that
- // code review is being done.
- String pushKind = magicBranch != null && magicBranch.submit ? "direct_submit" : "magic";
- metrics.pushCount.increment(pushKind, project.getName(), getUpdateType(magicCommands));
- }
- Optional<String> justification =
- pushOptions.get(DIRECT_PUSH_JUSTIFICATION_OPTION).stream().findFirst();
- try (RefUpdateContext ctx = RefUpdateContext.openDirectPush(justification)) {
- if (!regularCommands.isEmpty()) {
- metrics.pushCount.increment("direct", project.getName(), getUpdateType(regularCommands));
- }
-
- if (!regularCommands.isEmpty()) {
- handleRegularCommands(regularCommands, progress);
- return;
+ try {
+ if (!magicCommands.isEmpty()) {
+ parseMagicBranch(globalRevWalk, Iterables.getLast(magicCommands));
+ // Using the submit option submits the created change(s) immediately without checking
+ // labels
+ // nor submit rules. Hence we shouldn't record such pushes as "magic" which implies that
+ // code review is being done.
+ String pushKind = magicBranch != null && magicBranch.submit ? "direct_submit" : "magic";
+ metrics.pushCount.increment(pushKind, project.getName(), getUpdateType(magicCommands));
+ }
+ Optional<String> justification =
+ pushOptions.get(DIRECT_PUSH_JUSTIFICATION_OPTION).stream().findFirst();
+ try (RefUpdateContext ctx = RefUpdateContext.openDirectPush(justification)) {
+ if (!regularCommands.isEmpty()) {
+ metrics.pushCount.increment(
+ "direct", project.getName(), getUpdateType(regularCommands));
+ }
+
+ if (!regularCommands.isEmpty()) {
+ handleRegularCommands(globalRevWalk, ins, regularCommands, progress);
+ return;
+ }
}
- }
- boolean first = true;
- for (ReceiveCommand cmd : magicCommands) {
- if (first) {
- first = false;
- } else {
- reject(cmd, "duplicate request");
+ boolean first = true;
+ for (ReceiveCommand cmd : magicCommands) {
+ if (first) {
+ first = false;
+ } else {
+ reject(cmd, "duplicate request");
+ }
}
+ } catch (PermissionBackendException | NoSuchProjectException | IOException err) {
+ logger.atSevere().withCause(err).log("Failed to process refs in %s", project.getName());
+ return;
}
- } catch (PermissionBackendException | NoSuchProjectException | IOException err) {
- logger.atSevere().withCause(err).log("Failed to process refs in %s", project.getName());
- return;
- }
- Task newProgress = progress.beginSubTask("new", UNKNOWN);
- Task replaceProgress = progress.beginSubTask("updated", UNKNOWN);
+ Task newProgress = progress.beginSubTask("new", UNKNOWN);
+ Task replaceProgress = progress.beginSubTask("updated", UNKNOWN);
- ImmutableList<CreateRequest> newChanges = ImmutableList.of();
- try {
- if (magicBranch != null && magicBranch.cmd.getResult() == NOT_ATTEMPTED) {
- try {
- newChanges = selectNewAndReplacedChangesFromMagicBranch(newProgress);
- } catch (IOException e) {
- throw new StorageException("Failed to select new changes in " + project.getName(), e);
+ ImmutableList<CreateRequest> newChanges = ImmutableList.of();
+ try {
+ if (magicBranch != null && magicBranch.cmd.getResult() == NOT_ATTEMPTED) {
+ try {
+ newChanges =
+ selectNewAndReplacedChangesFromMagicBranch(globalRevWalk, ins, newProgress);
+ } catch (IOException e) {
+ throw new StorageException("Failed to select new changes in " + project.getName(), e);
+ }
}
- }
- // Commit validation has already happened, so any changes without Change-Id are for the
- // deprecated feature.
- warnAboutMissingChangeId(newChanges);
- preparePatchSetsForReplace(newChanges);
- insertChangesAndPatchSets(newChanges, replaceProgress);
- } finally {
- newProgress.end();
- replaceProgress.end();
- }
+ // Commit validation has already happened, so any changes without Change-Id are for the
+ // deprecated feature.
+ warnAboutMissingChangeId(globalRevWalk, newChanges);
+ preparePatchSetsForReplace(globalRevWalk, newChanges);
+ insertChangesAndPatchSets(globalRevWalk, ins, newChanges, replaceProgress);
+ } finally {
+ newProgress.end();
+ replaceProgress.end();
+ }
- queueSuccessMessages(newChanges);
+ queueSuccessMessages(newChanges);
- logger.atFine().log(
- "Command results: %s",
- lazy(() -> commands.stream().map(ReceiveCommits::commandToString).collect(joining(","))));
+ logger.atFine().log(
+ "Command results: %s",
+ lazy(() -> commands.stream().map(ReceiveCommits::commandToString).collect(joining(","))));
+ }
}
private String getUpdateType(List<ReceiveCommand> commands) {
@@ -837,20 +855,23 @@ class ReceiveCommits {
}
}
- private void handleRegularCommands(List<ReceiveCommand> cmds, MultiProgressMonitor progress)
+ private void handleRegularCommands(
+ RevWalk globalRevWalk,
+ ObjectInserter ins,
+ List<ReceiveCommand> cmds,
+ MultiProgressMonitor progress)
throws PermissionBackendException, IOException, NoSuchProjectException {
try (TraceTimer traceTimer =
newTimer("handleRegularCommands", Metadata.builder().resourceCount(cmds.size()))) {
result.magicPush(false);
for (ReceiveCommand cmd : cmds) {
- parseRegularCommand(cmd);
+ parseRegularCommand(globalRevWalk, ins, cmd);
}
Map<BranchNameKey, ReceiveCommand> branches;
try (BatchUpdate bu =
batchUpdateFactory.create(
project.getNameKey(), user.materializedCopy(), TimeUtil.now());
- ObjectInserter ins = repo.newObjectInserter();
ObjectReader reader = ins.newReader();
RevWalk rw = new RevWalk(reader);
MergeOpRepoManager orm = ormProvider.get()) {
@@ -867,7 +888,7 @@ class ReceiveCommits {
logger.atFine().log("Added %d additional ref updates", added);
SubmissionExecutor submissionExecutor =
- new SubmissionExecutor(false, superprojectUpdateSubmissionListeners);
+ new SubmissionExecutor(batchUpdates, false, superprojectUpdateSubmissionListeners);
submissionExecutor.execute(ImmutableList.of(bu));
@@ -894,7 +915,7 @@ class ReceiveCommits {
Task closeProgress = progress.beginSubTask("closed", UNKNOWN);
try (RefUpdateContext ctx =
RefUpdateContext.open(RefUpdateType.AUTO_CLOSE_CHANGES)) {
- autoCloseChanges(c, closeProgress);
+ autoCloseChanges(globalRevWalk, ins, c, closeProgress);
}
closeProgress.end();
break;
@@ -1023,7 +1044,10 @@ class ReceiveCommits {
}
private void insertChangesAndPatchSets(
- ImmutableList<CreateRequest> newChanges, Task replaceProgress) {
+ RevWalk globalRevWalk,
+ ObjectInserter ins,
+ ImmutableList<CreateRequest> newChanges,
+ Task replaceProgress) {
try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
try (TraceTimer traceTimer =
newTimer(
@@ -1042,17 +1066,21 @@ class ReceiveCommits {
// TODO: Retry lock failures on new change insertions. The retry will
// likely have to move to a higher layer to be able to achieve that
// due to state that needs to be reset with each retry attempt.
- insertChangesAndPatchSets(magicBranchCmd, newChanges, replaceProgress);
+ insertChangesAndPatchSets(
+ globalRevWalk, ins, magicBranchCmd, newChanges, replaceProgress);
} else {
- retryHelper
- .changeUpdate(
- "insertPatchSets",
- updateFactory -> {
- insertChangesAndPatchSets(magicBranchCmd, newChanges, replaceProgress);
- return null;
- })
- .defaultTimeoutMultiplier(5)
- .call();
+ @SuppressWarnings("unused")
+ var unused =
+ retryHelper
+ .changeUpdate(
+ "insertPatchSets",
+ updateFactory -> {
+ insertChangesAndPatchSets(
+ globalRevWalk, ins, magicBranchCmd, newChanges, replaceProgress);
+ return null;
+ })
+ .defaultTimeoutMultiplier(5)
+ .call();
}
} catch (ResourceConflictException e) {
addError(e.getMessage());
@@ -1085,12 +1113,15 @@ class ReceiveCommits {
}
private void insertChangesAndPatchSets(
- ReceiveCommand magicBranchCmd, List<CreateRequest> newChanges, Task replaceProgress)
+ RevWalk globalRevWalk,
+ ObjectInserter ins,
+ ReceiveCommand magicBranchCmd,
+ List<CreateRequest> newChanges,
+ Task replaceProgress)
throws RestApiException, IOException {
try (BatchUpdate bu =
batchUpdateFactory.create(
project.getNameKey(), user.materializedCopy(), TimeUtil.now());
- ObjectInserter ins = repo.newObjectInserter();
ObjectReader reader = ins.newReader();
RevWalk rw = new RevWalk(reader)) {
bu.setRepository(repo, rw, ins);
@@ -1101,7 +1132,7 @@ class ReceiveCommits {
logger.atFine().log("Adding %d replace requests", newChanges.size());
for (ReplaceRequest replace : replaceByChange.values()) {
- replace.addOps(bu, replaceProgress);
+ replace.addOps(globalRevWalk, bu, replaceProgress);
if (magicBranch != null) {
bu.setNotifyHandling(replace.ontoChange, magicBranch.getNotifyHandling(replace.notes));
if (magicBranch.shouldPublishComments()) {
@@ -1130,7 +1161,7 @@ class ReceiveCommits {
logger.atFine().log("Adding %d create requests", newChanges.size());
for (CreateRequest create : newChanges) {
- create.addOps(bu);
+ create.addOps(globalRevWalk, bu);
}
logger.atFine().log("Executing batch");
@@ -1255,7 +1286,7 @@ class ReceiveCommits {
/*
* Interpret a normal push.
*/
- private void parseRegularCommand(ReceiveCommand cmd)
+ private void parseRegularCommand(RevWalk globalRevWalk, ObjectInserter ins, ReceiveCommand cmd)
throws PermissionBackendException, NoSuchProjectException, IOException {
try (TraceTimer traceTimer = newTimer("parseRegularCommand")) {
if (cmd.getResult() != NOT_ATTEMPTED) {
@@ -1297,11 +1328,11 @@ class ReceiveCommits {
switch (cmd.getType()) {
case CREATE:
- parseCreate(cmd);
+ parseCreate(globalRevWalk, ins, cmd);
break;
case UPDATE:
- parseUpdate(cmd);
+ parseUpdate(globalRevWalk, ins, cmd);
break;
case DELETE:
@@ -1309,7 +1340,7 @@ class ReceiveCommits {
break;
case UPDATE_NONFASTFORWARD:
- parseRewind(cmd);
+ parseRewind(globalRevWalk, ins, cmd);
break;
default:
@@ -1322,13 +1353,14 @@ class ReceiveCommits {
}
if (isConfig(cmd)) {
- validateConfigPush(cmd);
+ validateConfigPush(globalRevWalk, cmd);
}
}
}
/** Validates a push to refs/meta/config, and reject the command if it fails. */
- private void validateConfigPush(ReceiveCommand cmd) throws PermissionBackendException {
+ private void validateConfigPush(RevWalk globalRevWalk, ReceiveCommand cmd)
+ throws PermissionBackendException {
try (TraceTimer traceTimer = newTimer("validateConfigPush")) {
logger.atFine().log("Processing %s command", cmd.getRefName());
if (!permissions.test(ProjectPermission.WRITE_CONFIG)) {
@@ -1346,7 +1378,7 @@ class ReceiveCommits {
case UPDATE_NONFASTFORWARD:
try {
ProjectConfig cfg = projectConfigFactory.create(project.getNameKey());
- cfg.load(project.getNameKey(), receivePack.getRevWalk(), cmd.getNewId());
+ cfg.load(project.getNameKey(), globalRevWalk, cmd.getNewId());
if (!cfg.getValidationErrors().isEmpty()) {
addError("Invalid project configuration:");
for (ValidationError err : cfg.getValidationErrors()) {
@@ -1456,7 +1488,7 @@ class ReceiveCommits {
}
}
- private void parseCreate(ReceiveCommand cmd)
+ private void parseCreate(RevWalk globalRevWalk, ObjectInserter ins, ReceiveCommand cmd)
throws PermissionBackendException, NoSuchProjectException, IOException {
try (TraceTimer traceTimer = newTimer("parseCreate")) {
if (repo.resolve(cmd.getRefName()) != null) {
@@ -1467,7 +1499,7 @@ class ReceiveCommits {
}
RevObject obj;
try {
- obj = receivePack.getRevWalk().parseAny(cmd.getNewId());
+ obj = globalRevWalk.parseAny(cmd.getNewId());
} catch (IOException e) {
throw new StorageException(
String.format(
@@ -1476,7 +1508,7 @@ class ReceiveCommits {
}
logger.atFine().log("Creating %s", cmd);
- if (isHead(cmd) && !isCommit(cmd)) {
+ if (isHead(cmd) && !isCommit(globalRevWalk, cmd)) {
return;
}
@@ -1496,23 +1528,27 @@ class ReceiveCommits {
if (validRefOperation(cmd)) {
validateRegularPushCommits(
- BranchNameKey.create(project.getNameKey(), cmd.getRefName()), cmd);
+ globalRevWalk, ins, BranchNameKey.create(project.getNameKey(), cmd.getRefName()), cmd);
}
}
}
- private void parseUpdate(ReceiveCommand cmd) throws PermissionBackendException {
+ private void parseUpdate(RevWalk globalRevWalk, ObjectInserter ins, ReceiveCommand cmd)
+ throws PermissionBackendException {
try (TraceTimer traceTimer = TraceContext.newTimer("parseUpdate")) {
logger.atFine().log("Updating %s", cmd);
Optional<AuthException> err = checkRefPermission(cmd, RefPermission.UPDATE);
if (!err.isPresent()) {
- if (isHead(cmd) && !isCommit(cmd)) {
+ if (isHead(cmd) && !isCommit(globalRevWalk, cmd)) {
reject(cmd, "head must point to commit");
return;
}
if (validRefOperation(cmd)) {
validateRegularPushCommits(
- BranchNameKey.create(project.getNameKey(), cmd.getRefName()), cmd);
+ globalRevWalk,
+ ins,
+ BranchNameKey.create(project.getNameKey(), cmd.getRefName()),
+ cmd);
}
} else {
rejectProhibited(cmd, err.get());
@@ -1520,10 +1556,10 @@ class ReceiveCommits {
}
}
- private boolean isCommit(ReceiveCommand cmd) {
+ private boolean isCommit(RevWalk globalRevWalk, ReceiveCommand cmd) {
RevObject obj;
try {
- obj = receivePack.getRevWalk().parseAny(cmd.getNewId());
+ obj = globalRevWalk.parseAny(cmd.getNewId());
} catch (IOException e) {
throw new StorageException(
String.format(
@@ -1551,17 +1587,19 @@ class ReceiveCommits {
Optional<AuthException> err = checkRefPermission(cmd, RefPermission.DELETE);
if (!err.isPresent()) {
- validRefOperation(cmd);
+ @SuppressWarnings("unused")
+ var unused = validRefOperation(cmd);
} else {
rejectProhibited(cmd, err.get());
}
}
}
- private void parseRewind(ReceiveCommand cmd) throws PermissionBackendException {
+ private void parseRewind(RevWalk globalRevWalk, ObjectInserter ins, ReceiveCommand cmd)
+ throws PermissionBackendException {
try (TraceTimer traceTimer = newTimer("parseRewind")) {
try {
- receivePack.getRevWalk().parseCommit(cmd.getNewId());
+ globalRevWalk.parseCommit(cmd.getNewId());
} catch (IOException e) {
throw new StorageException(
String.format(
@@ -1573,7 +1611,8 @@ class ReceiveCommits {
if (!validRefOperation(cmd)) {
return;
}
- validateRegularPushCommits(BranchNameKey.create(project.getNameKey(), cmd.getRefName()), cmd);
+ validateRegularPushCommits(
+ globalRevWalk, ins, BranchNameKey.create(project.getNameKey(), cmd.getRefName()), cmd);
if (cmd.getResult() != NOT_ATTEMPTED) {
return;
}
@@ -1947,7 +1986,8 @@ class ReceiveCommits {
*
* <p>Assumes we are handling a magic branch here.
*/
- private void parseMagicBranch(ReceiveCommand cmd) throws PermissionBackendException, IOException {
+ private void parseMagicBranch(RevWalk globalRevWalk, ReceiveCommand cmd)
+ throws PermissionBackendException, IOException {
try (TraceTimer traceTimer = newTimer("parseMagicBranch")) {
logger.atFine().log("Found magic branch %s", cmd.getRefName());
MagicBranchInput magicBranch = new MagicBranchInput(user, projectState, cmd, labelTypes);
@@ -2076,10 +2116,9 @@ class ReceiveCommits {
}
}
- RevWalk walk = receivePack.getRevWalk();
RevCommit tip;
try {
- tip = walk.parseCommit(magicBranch.cmd.getNewId());
+ tip = globalRevWalk.parseCommit(magicBranch.cmd.getNewId());
logger.atFine().log("Tip of push: %s", tip.name());
} catch (IOException ex) {
magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
@@ -2100,8 +2139,8 @@ class ReceiveCommits {
reject(cmd, magicBranch.dest.branch() + " not found");
return;
}
- RevCommit branchTip = receivePack.getRevWalk().parseCommit(refTip.getObjectId());
- if (!walk.isMergedInto(tip, branchTip)) {
+ RevCommit branchTip = globalRevWalk.parseCommit(refTip.getObjectId());
+ if (!globalRevWalk.isMergedInto(tip, branchTip)) {
reject(cmd, "not merged into branch");
return;
}
@@ -2122,7 +2161,7 @@ class ReceiveCommits {
magicBranch.baseCommit = Lists.newArrayListWithCapacity(magicBranch.base.size());
for (ObjectId id : magicBranch.base) {
try {
- magicBranch.baseCommit.add(walk.parseCommit(id));
+ magicBranch.baseCommit.add(globalRevWalk.parseCommit(id));
} catch (IncorrectObjectTypeException notCommit) {
reject(cmd, "base must be a commit");
return;
@@ -2137,7 +2176,7 @@ class ReceiveCommits {
} else if (newChangeForAllNotInTarget) {
Ref refTip = receivePackRefCache.exactRef(magicBranch.dest.branch());
if (refTip != null) {
- RevCommit branchTip = receivePack.getRevWalk().parseCommit(refTip.getObjectId());
+ RevCommit branchTip = globalRevWalk.parseCommit(refTip.getObjectId());
magicBranch.baseCommit = Collections.singletonList(branchTip);
logger.atFine().log("Set baseCommit = %s", magicBranch.baseCommit.get(0).name());
} else {
@@ -2159,7 +2198,7 @@ class ReceiveCommits {
String.format("Error walking to %s in project %s", destBranch, project.getName()), e);
}
- if (validateConnected(magicBranch.cmd, magicBranch.dest, tip)) {
+ if (validateConnected(globalRevWalk, magicBranch.cmd, magicBranch.dest, tip)) {
this.magicBranch = magicBranch;
this.result.magicPush(true);
}
@@ -2178,10 +2217,10 @@ class ReceiveCommits {
// branch. If they aren't, we want to abort. We do this check by
// looking to see if we can compute a merge base between the new
// commits and the target branch head.
- private boolean validateConnected(ReceiveCommand cmd, BranchNameKey dest, RevCommit tip) {
+ private boolean validateConnected(
+ RevWalk globalRevWalk, ReceiveCommand cmd, BranchNameKey dest, RevCommit tip) {
try (TraceTimer traceTimer =
newTimer("validateConnected", Metadata.builder().branchName(dest.branch()))) {
- RevWalk walk = receivePack.getRevWalk();
try {
Ref targetRef = receivePackRefCache.exactRef(dest.branch());
if (targetRef == null || targetRef.getObjectId() == null) {
@@ -2194,21 +2233,21 @@ class ReceiveCommits {
return true;
}
- RevCommit h = walk.parseCommit(targetRef.getObjectId());
+ RevCommit h = globalRevWalk.parseCommit(targetRef.getObjectId());
logger.atFine().log("Current branch tip: %s", h.name());
- RevFilter oldRevFilter = walk.getRevFilter();
+ RevFilter oldRevFilter = globalRevWalk.getRevFilter();
try {
- walk.reset();
- walk.setRevFilter(RevFilter.MERGE_BASE);
- walk.markStart(tip);
- walk.markStart(h);
- if (walk.next() == null) {
+ globalRevWalk.reset();
+ globalRevWalk.setRevFilter(RevFilter.MERGE_BASE);
+ globalRevWalk.markStart(tip);
+ globalRevWalk.markStart(h);
+ if (globalRevWalk.next() == null) {
reject(cmd, "no common ancestry");
return false;
}
} finally {
- walk.reset();
- walk.setRevFilter(oldRevFilter);
+ globalRevWalk.reset();
+ globalRevWalk.setRevFilter(oldRevFilter);
}
} catch (IOException e) {
cmd.setResult(REJECTED_MISSING_OBJECT);
@@ -2236,7 +2275,11 @@ class ReceiveCommits {
* @return True if the command succeeded, false if it was rejected.
*/
private boolean requestReplaceAndValidateComments(
- ReceiveCommand cmd, boolean checkMergedInto, Change change, RevCommit newCommit)
+ RevWalk globalRevWalk,
+ ReceiveCommand cmd,
+ boolean checkMergedInto,
+ Change change,
+ RevCommit newCommit)
throws IOException {
try (TraceTimer traceTimer = newTimer("requestReplaceAndValidateComments")) {
if (change.isClosed()) {
@@ -2247,7 +2290,8 @@ class ReceiveCommits {
return false;
}
- ReplaceRequest req = new ReplaceRequest(change.getId(), newCommit, cmd, checkMergedInto);
+ ReplaceRequest req =
+ new ReplaceRequest(globalRevWalk, change.getId(), newCommit, cmd, checkMergedInto);
if (replaceByChange.containsKey(req.ontoChange)) {
reject(cmd, "duplicate request");
return false;
@@ -2287,10 +2331,11 @@ class ReceiveCommits {
}
}
- private void warnAboutMissingChangeId(ImmutableList<CreateRequest> newChanges) {
+ private void warnAboutMissingChangeId(
+ RevWalk globalRevWalk, ImmutableList<CreateRequest> newChanges) {
for (CreateRequest create : newChanges) {
try {
- receivePack.getRevWalk().parseBody(create.commit);
+ globalRevWalk.parseBody(create.commit);
} catch (IOException e) {
throw new StorageException("Can't parse commit", e);
}
@@ -2304,8 +2349,8 @@ class ReceiveCommits {
}
}
- private ImmutableList<CreateRequest> selectNewAndReplacedChangesFromMagicBranch(Task newProgress)
- throws IOException {
+ private ImmutableList<CreateRequest> selectNewAndReplacedChangesFromMagicBranch(
+ RevWalk globalRevWalk, ObjectInserter ins, Task newProgress) throws IOException {
try (TraceTimer traceTimer = newTimer("selectNewAndReplacedChangesFromMagicBranch")) {
logger.atFine().log("Finding new and replaced changes");
List<CreateRequest> newChanges = new ArrayList<>();
@@ -2317,7 +2362,7 @@ class ReceiveCommits {
commitValidatorFactory.create(projectState, magicBranch.dest, user);
try {
- RevCommit start = setUpWalkForSelectingChanges();
+ RevCommit start = setUpWalkForSelectingChanges(globalRevWalk);
if (start == null) {
return ImmutableList.of();
}
@@ -2345,15 +2390,15 @@ class ReceiveCommits {
}
for (; ; ) {
- RevCommit c = receivePack.getRevWalk().next();
+ RevCommit c = globalRevWalk.next();
if (c == null) {
break;
}
total++;
- receivePack.getRevWalk().parseBody(c);
+ globalRevWalk.parseBody(c);
String name = c.name();
groupCollector.visit(c);
- Collection<PatchSet.Id> existingPatchSets =
+ ImmutableList<PatchSet.Id> existingPatchSets =
receivePackRefCache.patchSetIdsFromObjectId(c);
if (rejectImplicitMerges) {
@@ -2404,7 +2449,9 @@ class ReceiveCommits {
BranchCommitValidator.Result validationResult =
validator.validateCommit(
repo,
- receivePack.getRevWalk().getObjectReader(),
+ globalRevWalk.getObjectReader(),
+ diffOperationsForCommitValidationFactory.create(
+ new RepoView(repo, globalRevWalk, ins), ins),
magicBranch.cmd,
c,
ImmutableListMultimap.copyOf(pushOptions),
@@ -2440,7 +2487,7 @@ class ReceiveCommits {
total, alreadyTracked, newChanges.size(), pending.size());
if (rejectImplicitMerges) {
- rejectImplicitMerges(mergedParents);
+ rejectImplicitMerges(globalRevWalk, mergedParents);
}
for (Iterator<ChangeLookup> itr = pending.values().iterator(); itr.hasNext(); ) {
@@ -2490,7 +2537,7 @@ class ReceiveCommits {
}
}
if (requestReplaceAndValidateComments(
- magicBranch.cmd, false, changes.get(0).change(), p.commit)) {
+ globalRevWalk, magicBranch.cmd, false, changes.get(0).change(), p.commit)) {
continue;
}
return ImmutableList.of();
@@ -2535,7 +2582,7 @@ class ReceiveCommits {
}
SortedSetMultimap<ObjectId, String> groups = groupCollector.getGroups();
- List<Integer> newIds = seq.nextChangeIds(newChanges.size());
+ ImmutableList<Integer> newIds = seq.nextChangeIds(newChanges.size());
for (int i = 0; i < newChanges.size(); i++) {
CreateRequest create = newChanges.get(i);
create.setChangeId(newIds.get(i));
@@ -2566,34 +2613,34 @@ class ReceiveCommits {
}
}
- private RevCommit setUpWalkForSelectingChanges() throws IOException {
+ private RevCommit setUpWalkForSelectingChanges(RevWalk globalRevWalk) throws IOException {
try (TraceTimer traceTimer = newTimer("setUpWalkForSelectingChanges")) {
- RevWalk rw = receivePack.getRevWalk();
- RevCommit start = rw.parseCommit(magicBranch.cmd.getNewId());
+ RevCommit start = globalRevWalk.parseCommit(magicBranch.cmd.getNewId());
- rw.reset();
- rw.sort(RevSort.TOPO);
- rw.sort(RevSort.REVERSE, true);
- receivePack.getRevWalk().markStart(start);
+ globalRevWalk.reset();
+ globalRevWalk.sort(RevSort.TOPO);
+ globalRevWalk.sort(RevSort.REVERSE, true);
+ globalRevWalk.markStart(start);
if (magicBranch.baseCommit != null) {
- markExplicitBasesUninteresting();
+ markExplicitBasesUninteresting(globalRevWalk);
} else if (magicBranch.merged) {
logger.atFine().log("Marking parents of merged commit %s uninteresting", start.name());
for (RevCommit c : start.getParents()) {
- rw.markUninteresting(c);
+ globalRevWalk.markUninteresting(c);
}
} else {
- markHeadsAsUninteresting(rw, magicBranch.dest != null ? magicBranch.dest.branch() : null);
+ markHeadsAsUninteresting(
+ globalRevWalk, magicBranch.dest != null ? magicBranch.dest.branch() : null);
}
return start;
}
}
- private void markExplicitBasesUninteresting() throws IOException {
+ private void markExplicitBasesUninteresting(RevWalk globalRevWalk) throws IOException {
try (TraceTimer traceTimer = newTimer("markExplicitBasesUninteresting")) {
logger.atFine().log("Marking %d base commits uninteresting", magicBranch.baseCommit.size());
for (RevCommit c : magicBranch.baseCommit) {
- receivePack.getRevWalk().markUninteresting(c);
+ globalRevWalk.markUninteresting(c);
}
Ref targetRef = receivePackRefCache.exactRef(magicBranch.dest.branch());
if (targetRef != null) {
@@ -2602,36 +2649,36 @@ class ReceiveCommits {
magicBranch.dest.branch(), targetRef.getObjectId().name());
receivePack
.getRevWalk()
- .markUninteresting(receivePack.getRevWalk().parseCommit(targetRef.getObjectId()));
+ .markUninteresting(globalRevWalk.parseCommit(targetRef.getObjectId()));
}
}
}
- private void rejectImplicitMerges(Set<RevCommit> mergedParents) throws IOException {
+ private void rejectImplicitMerges(RevWalk globalRevWalk, Set<RevCommit> mergedParents)
+ throws IOException {
try (TraceTimer traceTimer = newTimer("rejectImplicitMerges")) {
if (!mergedParents.isEmpty()) {
Ref targetRef = receivePackRefCache.exactRef(magicBranch.dest.branch());
if (targetRef != null) {
- RevWalk rw = receivePack.getRevWalk();
- RevCommit tip = rw.parseCommit(targetRef.getObjectId());
+ RevCommit tip = globalRevWalk.parseCommit(targetRef.getObjectId());
boolean containsImplicitMerges = true;
for (RevCommit p : mergedParents) {
- containsImplicitMerges &= !rw.isMergedInto(p, tip);
+ containsImplicitMerges &= !globalRevWalk.isMergedInto(p, tip);
}
if (containsImplicitMerges) {
- rw.reset();
+ globalRevWalk.reset();
for (RevCommit p : mergedParents) {
- rw.markStart(p);
+ globalRevWalk.markStart(p);
}
- rw.markUninteresting(tip);
+ globalRevWalk.markUninteresting(tip);
RevCommit c;
- while ((c = rw.next()) != null) {
- rw.parseBody(c);
+ while ((c = globalRevWalk.next()) != null) {
+ globalRevWalk.parseBody(c);
messages.add(
new CommitValidationMessage(
"Implicit Merge of "
- + abbreviateName(c, rw.getObjectReader())
+ + abbreviateName(c, globalRevWalk.getObjectReader())
+ " "
+ c.getShortMessage(),
ValidationMessage.Type.ERROR));
@@ -2645,7 +2692,8 @@ class ReceiveCommits {
// Mark all branch tips as uninteresting in the given revwalk,
// so we get only the new commits when walking rw.
- private void markHeadsAsUninteresting(RevWalk rw, @Nullable String forRef) throws IOException {
+ private void markHeadsAsUninteresting(RevWalk globalRevWalk, @Nullable String forRef)
+ throws IOException {
try (TraceTimer traceTimer =
newTimer("markHeadsAsUninteresting", Metadata.builder().branchName(forRef))) {
int i = 0;
@@ -2655,7 +2703,7 @@ class ReceiveCommits {
Collections.singletonList(receivePackRefCache.exactRef(forRef)))) {
if (ref != null && ref.getObjectId() != null) {
try {
- rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
+ globalRevWalk.markUninteresting(globalRevWalk.parseCommit(ref.getObjectId()));
i++;
} catch (IOException e) {
logger.atWarning().withCause(e).log(
@@ -2710,7 +2758,7 @@ class ReceiveCommits {
Change.Id changeId;
ReceiveCommand cmd;
ChangeInserter ins;
- List<String> groups = ImmutableList.of();
+ ImmutableList<String> groups = ImmutableList.of();
Change change;
@@ -2742,12 +2790,11 @@ class ReceiveCommits {
}
}
- private void addOps(BatchUpdate bu) throws RestApiException {
+ private void addOps(RevWalk globalRevWalk, BatchUpdate bu) throws RestApiException {
try (TraceTimer traceTimer = newTimer(CreateRequest.class, "addOps")) {
checkState(changeId != null, "must call setChangeId before addOps");
try {
- RevWalk rw = receivePack.getRevWalk();
- rw.parseBody(commit);
+ globalRevWalk.parseBody(commit);
final PatchSet.Id psId = ins.setGroups(groups).getPatchSetId();
Account.Id me = user.getAccountId();
List<FooterLine> footerLines = commit.getFooterLines();
@@ -2852,7 +2899,8 @@ class ReceiveCommits {
}
}
- private void preparePatchSetsForReplace(ImmutableList<CreateRequest> newChanges) {
+ private void preparePatchSetsForReplace(
+ RevWalk globalRevWalk, ImmutableList<CreateRequest> newChanges) {
try (TraceTimer traceTimer =
newTimer(
"preparePatchSetsForReplace", Metadata.builder().resourceCount(newChanges.size()))) {
@@ -2860,7 +2908,9 @@ class ReceiveCommits {
readChangesForReplace();
for (ReplaceRequest req : replaceByChange.values()) {
if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
- req.validateNewPatchSet();
+ // TODO: Is it OK to ignore the return value?
+ @SuppressWarnings("unused")
+ var unused = req.validateNewPatchSet(globalRevWalk);
}
}
} catch (IOException | PermissionBackendException e) {
@@ -2906,11 +2956,15 @@ class ReceiveCommits {
ReceiveCommand cmd;
PatchSetInfo info;
PatchSet.Id priorPatchSet;
- List<String> groups = ImmutableList.of();
+ ImmutableList<String> groups = ImmutableList.of();
ReplaceOp replaceOp;
ReplaceRequest(
- Change.Id toChange, RevCommit newCommit, ReceiveCommand cmd, boolean checkMergedInto)
+ RevWalk globalRevWalk,
+ Change.Id toChange,
+ RevCommit newCommit,
+ ReceiveCommand cmd,
+ boolean checkMergedInto)
throws IOException {
this.ontoChange = toChange;
this.newCommitId = newCommit.copy();
@@ -2918,7 +2972,7 @@ class ReceiveCommits {
this.checkMergedInto = checkMergedInto;
try {
- revCommit = receivePack.getRevWalk().parseCommit(newCommitId);
+ revCommit = globalRevWalk.parseCommit(newCommitId);
} catch (IOException e) {
revCommit = null;
}
@@ -2927,7 +2981,7 @@ class ReceiveCommits {
try {
PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
if (psId != null) {
- revisions.forcePut(receivePack.getRevWalk().parseCommit(ref.getObjectId()), psId);
+ revisions.forcePut(globalRevWalk.parseCommit(ref.getObjectId()), psId);
}
} catch (IOException err) {
logger.atWarning().withCause(err).log(
@@ -2944,17 +2998,18 @@ class ReceiveCommits {
* <ul>
* <li>May add error or warning messages to the progress monitor
* <li>Will reject {@code cmd} prior to returning false
- * <li>May reset {@code receivePack.getRevWalk()}; do not call in the middle of a walk.
+ * <li>May reset t; do not call in the middle of a walk.
* </ul>
*
* @return whether the new commit is valid
*/
- boolean validateNewPatchSet() throws IOException, PermissionBackendException {
+ boolean validateNewPatchSet(RevWalk globalRevWalk)
+ throws IOException, PermissionBackendException {
try (TraceTimer traceTimer = newTimer("validateNewPatchSet")) {
- if (!validateNewPatchSetNoteDb()) {
+ if (!validateNewPatchSetNoteDb(globalRevWalk)) {
return false;
}
- sameTreeWarning();
+ sameTreeWarning(globalRevWalk);
if (magicBranch != null) {
validateMagicBranchWipStatusChange();
@@ -2967,22 +3022,24 @@ class ReceiveCommits {
}
}
- newPatchSet();
+ newPatchSet(globalRevWalk);
return true;
}
}
- boolean validateNewPatchSetForAutoClose() throws IOException, PermissionBackendException {
- if (!validateNewPatchSetNoteDb()) {
+ boolean validateNewPatchSetForAutoClose(RevWalk globalRevWalk)
+ throws IOException, PermissionBackendException {
+ if (!validateNewPatchSetNoteDb(globalRevWalk)) {
return false;
}
- newPatchSet();
+ newPatchSet(globalRevWalk);
return true;
}
/** Validates the new PS against permissions and notedb status. */
- private boolean validateNewPatchSetNoteDb() throws IOException, PermissionBackendException {
+ private boolean validateNewPatchSetNoteDb(RevWalk globalRevWalk)
+ throws IOException, PermissionBackendException {
try (TraceTimer traceTimer = newTimer("validateNewPatchSetNoteDb")) {
if (notes == null) {
reject(inputCommand, "change " + ontoChange + " not found");
@@ -3007,7 +3064,7 @@ class ReceiveCommits {
return false;
}
- RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId);
+ RevCommit newCommit = globalRevWalk.parseCommit(newCommitId);
// Not allowed to create a new patch set if the current patch set is locked.
if (psUtil.isPatchSetLocked(notes)) {
@@ -3024,11 +3081,15 @@ class ReceiveCommits {
reject(inputCommand, "change " + ontoChange + " closed");
return false;
} else if (revisions.containsKey(newCommit)) {
- reject(inputCommand, "commit already exists (in the change)");
+ reject(
+ inputCommand,
+ String.format(
+ "commit %s already exists in change %s",
+ newCommit.name().substring(0, 10), change.getId()));
return false;
}
- List<PatchSet.Id> existingPatchSetsWithSameCommit =
+ ImmutableList<PatchSet.Id> existingPatchSetsWithSameCommit =
receivePackRefCache.patchSetIdsFromObjectId(newCommit);
if (!existingPatchSetsWithSameCommit.isEmpty()) {
// TODO(hiesel, hanwen): Remove this check entirely when Gerrit requires change IDs
@@ -3045,7 +3106,7 @@ class ReceiveCommits {
// Don't allow a change to directly depend upon itself. This is a
// very common error due to users making a new commit rather than
// amending when trying to address review comments.
- if (receivePack.getRevWalk().isMergedInto(prior, newCommit)) {
+ if (globalRevWalk.isMergedInto(prior, newCommit)) {
reject(inputCommand, SAME_CHANGE_ID_IN_MULTIPLE_CHANGES);
return false;
}
@@ -3071,20 +3132,19 @@ class ReceiveCommits {
}
/** prints a warning if the new PS has the same tree as the previous commit. */
- private void sameTreeWarning() throws IOException {
+ private void sameTreeWarning(RevWalk globalRevWalk) throws IOException {
try (TraceTimer traceTimer = newTimer("sameTreeWarning")) {
- RevWalk rw = receivePack.getRevWalk();
- RevCommit newCommit = rw.parseCommit(newCommitId);
+ RevCommit newCommit = globalRevWalk.parseCommit(newCommitId);
RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
if (newCommit.getTree().equals(priorCommit.getTree())) {
- rw.parseBody(newCommit);
- rw.parseBody(priorCommit);
+ globalRevWalk.parseBody(newCommit);
+ globalRevWalk.parseBody(priorCommit);
boolean messageEq =
Objects.equals(newCommit.getFullMessage(), priorCommit.getFullMessage());
boolean parentsEq = parentsEqual(newCommit, priorCommit);
boolean authorEq = authorEqual(newCommit, priorCommit);
- ObjectReader reader = receivePack.getRevWalk().getObjectReader();
+ ObjectReader reader = globalRevWalk.getObjectReader();
if (messageEq && parentsEq && authorEq) {
addMessage(
@@ -3158,11 +3218,11 @@ class ReceiveCommits {
}
/** Updates 'this' to add a new patchset. */
- private void newPatchSet() throws IOException {
+ private void newPatchSet(RevWalk globalRevWalk) throws IOException {
try (TraceTimer traceTimer = newTimer("newPatchSet")) {
- RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId);
+ RevCommit newCommit = globalRevWalk.parseCommit(newCommitId);
psId = nextPatchSetId(notes.getChange().currentPatchSetId());
- info = patchSetInfoFactory.get(receivePack.getRevWalk(), newCommit, psId);
+ info = patchSetInfoFactory.get(globalRevWalk, newCommit, psId);
cmd = new ReceiveCommand(ObjectId.zeroId(), newCommitId, psId.toRefName());
}
}
@@ -3175,7 +3235,7 @@ class ReceiveCommits {
return next;
}
- void addOps(BatchUpdate bu, @Nullable Task progress) throws IOException {
+ void addOps(RevWalk globalRevWalk, BatchUpdate bu, @Nullable Task progress) throws IOException {
try (TraceTimer traceTimer = newTimer("addOps")) {
if (magicBranch != null && magicBranch.edit) {
bu.addOp(notes.getChangeId(), new ReindexOnlyOp());
@@ -3185,10 +3245,9 @@ class ReceiveCommits {
bu.addRepoOnlyOp(new UpdateOneRefOp(cmd));
return;
}
- RevWalk rw = receivePack.getRevWalk();
// TODO(dborowitz): Move to ReplaceOp#updateRepo.
- RevCommit newCommit = rw.parseCommit(newCommitId);
- rw.parseBody(newCommit);
+ RevCommit newCommit = globalRevWalk.parseCommit(newCommitId);
+ globalRevWalk.parseBody(newCommit);
RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
replaceOp =
@@ -3221,7 +3280,6 @@ class ReceiveCommits {
Optional<ReceiveCommand> autoMerge =
autoMerger.createAutoMergeCommitIfNecessary(
ctx.getRepoView(),
- ctx.getRevWalk(),
ctx.getInserter(),
ctx.getRevWalk().parseCommit(newCommitId));
if (autoMerge.isPresent()) {
@@ -3342,7 +3400,8 @@ class ReceiveCommits {
*
* <p>On validation failure, the command is rejected.
*/
- private void validateRegularPushCommits(BranchNameKey branch, ReceiveCommand cmd)
+ private void validateRegularPushCommits(
+ RevWalk globalRevWalk, ObjectInserter ins, BranchNameKey branch, ReceiveCommand cmd)
throws PermissionBackendException {
try (TraceTimer traceTimer =
newTimer("validateRegularPushCommits", Metadata.builder().branchName(branch.branch()))) {
@@ -3369,19 +3428,18 @@ class ReceiveCommits {
}
BranchCommitValidator validator = commitValidatorFactory.create(projectState, branch, user);
- RevWalk walk = receivePack.getRevWalk();
- walk.reset();
- walk.sort(RevSort.NONE);
+ globalRevWalk.reset();
+ globalRevWalk.sort(RevSort.NONE);
try {
- RevObject parsedObject = walk.parseAny(cmd.getNewId());
+ RevObject parsedObject = globalRevWalk.parseAny(cmd.getNewId());
if (!(parsedObject instanceof RevCommit)) {
return;
}
- walk.markStart((RevCommit) parsedObject);
- markHeadsAsUninteresting(walk, cmd.getRefName());
+ globalRevWalk.markStart((RevCommit) parsedObject);
+ markHeadsAsUninteresting(globalRevWalk, cmd.getRefName());
int limit = receiveConfig.maxBatchCommits;
int n = 0;
- for (RevCommit c; (c = walk.next()) != null; ) {
+ for (RevCommit c; (c = globalRevWalk.next()) != null; ) {
// Even if skipValidation is set, we still get here when at least one plugin
// commit validator requires to validate all commits. In this case, however,
// we don't need to check the commit limit.
@@ -3400,7 +3458,9 @@ class ReceiveCommits {
BranchCommitValidator.Result validationResult =
validator.validateCommit(
repo,
- walk.getObjectReader(),
+ globalRevWalk.getObjectReader(),
+ diffOperationsForCommitValidationFactory.create(
+ new RepoView(repo, globalRevWalk, ins), ins),
cmd,
c,
ImmutableListMultimap.copyOf(pushOptions),
@@ -3422,7 +3482,8 @@ class ReceiveCommits {
}
}
- private void autoCloseChanges(ReceiveCommand cmd, Task progress) {
+ private void autoCloseChanges(
+ RevWalk globalRevWalk, ObjectInserter ins, ReceiveCommand cmd, Task progress) {
try (TraceTimer traceTimer = newTimer("autoCloseChanges")) {
logger.atFine().log("Starting auto-closing of changes");
String refName = cmd.getRefName();
@@ -3431,140 +3492,149 @@ class ReceiveCommits {
// TODO(dborowitz): Combine this BatchUpdate with the main one in
// handleRegularCommands
try {
- retryHelper
- .changeUpdate(
- "autoCloseChanges",
- updateFactory -> {
- try (BatchUpdate bu =
- updateFactory.create(projectState.getNameKey(), user, TimeUtil.now());
- ObjectInserter ins = repo.newObjectInserter();
- ObjectReader reader = ins.newReader();
- RevWalk rw = new RevWalk(reader)) {
- if (ObjectId.zeroId().equals(cmd.getOldId())) {
- // The user is creating a new branch. The branch can't contain any changes, so
- // auto-closing doesn't apply. Exiting here early to spare any further,
- // potentially expensive computation that loop over all commits.
- return null;
- }
+ @SuppressWarnings("unused")
+ var unused =
+ retryHelper
+ .changeUpdate(
+ "autoCloseChanges",
+ updateFactory -> {
+ try (BatchUpdate bu =
+ updateFactory.create(
+ projectState.getNameKey(), user, TimeUtil.now());
+ ObjectReader reader = ins.newReader();
+ RevWalk rw = new RevWalk(reader)) {
+ if (ObjectId.zeroId().equals(cmd.getOldId())) {
+ // The user is creating a new branch. The branch can't contain any
+ // changes, so
+ // auto-closing doesn't apply. Exiting here early to spare any further,
+ // potentially expensive computation that loop over all commits.
+ return null;
+ }
- bu.setRepository(repo, rw, ins);
- // TODO(dborowitz): Teach BatchUpdate to ignore missing changes.
-
- RevCommit newTip = rw.parseCommit(cmd.getNewId());
- BranchNameKey branch = BranchNameKey.create(project.getNameKey(), refName);
-
- rw.reset();
- rw.sort(RevSort.REVERSE);
- rw.markStart(newTip);
- rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
-
- Map<Change.Key, ChangeData> changeDataByKey = null;
- List<ReplaceRequest> replaceAndClose = new ArrayList<>();
-
- int existingPatchSets = 0;
- int newPatchSets = 0;
- SubmissionId submissionId = null;
- COMMIT:
- for (RevCommit c; (c = rw.next()) != null; ) {
- rw.parseBody(c);
-
- // Check if change refs point to this commit. Usually there are 0-1 change
- // refs pointing to this commit.
- for (PatchSet.Id psId :
- receivePackRefCache.patchSetIdsFromObjectId(c.copy())) {
- Optional<ChangeNotes> notes = getChangeNotes(psId.changeId());
- if (notes.isPresent() && notes.get().getChange().getDest().equals(branch)) {
- if (submissionId == null) {
- submissionId = new SubmissionId(notes.get().getChange());
+ bu.setRepository(repo, rw, ins);
+ // TODO(dborowitz): Teach BatchUpdate to ignore missing changes.
+
+ RevCommit newTip = rw.parseCommit(cmd.getNewId());
+ BranchNameKey branch = BranchNameKey.create(project.getNameKey(), refName);
+
+ rw.reset();
+ rw.sort(RevSort.REVERSE);
+ rw.markStart(newTip);
+ rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
+
+ Map<Change.Key, ChangeData> changeDataByKey = null;
+ List<ReplaceRequest> replaceAndClose = new ArrayList<>();
+
+ int existingPatchSets = 0;
+ int newPatchSets = 0;
+ SubmissionId submissionId = null;
+ COMMIT:
+ for (RevCommit c; (c = rw.next()) != null; ) {
+ rw.parseBody(c);
+
+ // Check if change refs point to this commit. Usually there are 0-1 change
+ // refs pointing to this commit.
+ for (PatchSet.Id psId :
+ receivePackRefCache.patchSetIdsFromObjectId(c.copy())) {
+ Optional<ChangeNotes> notes = getChangeNotes(psId.changeId());
+ if (notes.isPresent()
+ && notes.get().getChange().getDest().equals(branch)) {
+ if (submissionId == null) {
+ submissionId = new SubmissionId(notes.get().getChange());
+ }
+ existingPatchSets++;
+ bu.addOp(
+ notes.get().getChangeId(),
+ setPrivateOpFactory.create(false, null));
+ bu.addOp(
+ psId.changeId(),
+ mergedByPushOpFactory.create(
+ requestScopePropagator,
+ psId,
+ submissionId,
+ refName,
+ newTip.getId().getName()));
+ continue COMMIT;
+ }
}
- existingPatchSets++;
- bu.addOp(
- notes.get().getChangeId(), setPrivateOpFactory.create(false, null));
- bu.addOp(
- psId.changeId(),
- mergedByPushOpFactory.create(
- requestScopePropagator,
- psId,
- submissionId,
- refName,
- newTip.getId().getName()));
- continue COMMIT;
- }
- }
- for (String changeId : changeUtil.getChangeIdsFromFooter(c)) {
- if (changeDataByKey == null) {
- changeDataByKey =
- retryHelper
- .changeIndexQuery(
- "queryOpenChangesByKeyByBranch",
- q -> openChangesByKeyByBranch(q, branch))
- .call();
+ for (String changeId : changeUtil.getChangeIdsFromFooter(c)) {
+ if (changeDataByKey == null) {
+ changeDataByKey =
+ retryHelper
+ .changeIndexQuery(
+ "queryOpenChangesByKeyByBranch",
+ q -> openChangesByKeyByBranch(q, branch))
+ .call();
+ }
+
+ ChangeData onto = changeDataByKey.get(Change.key(changeId.trim()));
+ if (onto != null) {
+ newPatchSets++;
+ // Hold onto this until we're done with the walk, as the call to
+ // req.validate below calls isMergedInto which resets the walk.
+ ChangeNotes ontoNotes = onto.notes();
+ ReplaceRequest req =
+ new ReplaceRequest(
+ globalRevWalk, ontoNotes.getChangeId(), c, cmd, false);
+ req.notes = ontoNotes;
+ replaceAndClose.add(req);
+ continue COMMIT;
+ }
+ }
}
- ChangeData onto = changeDataByKey.get(Change.key(changeId.trim()));
- if (onto != null) {
- newPatchSets++;
- // Hold onto this until we're done with the walk, as the call to
- // req.validate below calls isMergedInto which resets the walk.
- ChangeNotes ontoNotes = onto.notes();
- ReplaceRequest req =
- new ReplaceRequest(ontoNotes.getChangeId(), c, cmd, false);
- req.notes = ontoNotes;
- replaceAndClose.add(req);
- continue COMMIT;
+ for (ReplaceRequest req : replaceAndClose) {
+ Change.Id id = req.notes.getChangeId();
+ if (!req.validateNewPatchSetForAutoClose(globalRevWalk)) {
+ logger.atFine().log("Not closing %s because validation failed", id);
+ continue;
+ }
+ if (submissionId == null) {
+ submissionId = new SubmissionId(req.notes.getChange());
+ }
+ req.addOps(globalRevWalk, bu, null);
+ bu.addOp(id, setPrivateOpFactory.create(false, null));
+ bu.addOp(
+ id,
+ mergedByPushOpFactory
+ .create(
+ requestScopePropagator,
+ req.psId,
+ submissionId,
+ refName,
+ newTip.getId().getName())
+ .setPatchSetProvider(req.replaceOp::getPatchSet));
+ bu.addOp(id, new ChangeProgressOp(progress));
+ ids.add(id);
}
- }
- }
- for (ReplaceRequest req : replaceAndClose) {
- Change.Id id = req.notes.getChangeId();
- if (!req.validateNewPatchSetForAutoClose()) {
- logger.atFine().log("Not closing %s because validation failed", id);
- continue;
- }
- if (submissionId == null) {
- submissionId = new SubmissionId(req.notes.getChange());
+ logger.atFine().log(
+ "Auto-closing %d changes with existing patch sets and %d with new patch"
+ + " sets",
+ existingPatchSets, newPatchSets);
+ bu.execute();
+ } catch (IOException | StorageException | PermissionBackendException e) {
+ throw new StorageException("Failed to auto-close changes", e);
}
- req.addOps(bu, null);
- bu.addOp(id, setPrivateOpFactory.create(false, null));
- bu.addOp(
- id,
- mergedByPushOpFactory
- .create(
- requestScopePropagator,
- req.psId,
- submissionId,
- refName,
- newTip.getId().getName())
- .setPatchSetProvider(req.replaceOp::getPatchSet));
- bu.addOp(id, new ChangeProgressOp(progress));
- ids.add(id);
- }
- logger.atFine().log(
- "Auto-closing %d changes with existing patch sets and %d with new patch"
- + " sets",
- existingPatchSets, newPatchSets);
- bu.execute();
- } catch (IOException | StorageException | PermissionBackendException e) {
- throw new StorageException("Failed to auto-close changes", e);
- }
+ // If we are here, we didn't throw UpdateException. Record the result.
+ // The ordering is indeterminate due to the HashSet; unfortunately, Change.Id
+ // doesn't
+ // fit into TreeSet.
+ ids.stream()
+ .forEach(
+ id ->
+ result.addChange(
+ ReceiveCommitsResult.ChangeStatus.AUTOCLOSED, id));
- // If we are here, we didn't throw UpdateException. Record the result.
- // The ordering is indeterminate due to the HashSet; unfortunately, Change.Id
- // doesn't
- // fit into TreeSet.
- ids.stream()
- .forEach(
- id -> result.addChange(ReceiveCommitsResult.ChangeStatus.AUTOCLOSED, id));
-
- return null;
- })
- // Use a multiple of the default timeout to account for inner retries that may otherwise
- // eat up the whole timeout so that no time is left to retry this outer action.
- .defaultTimeoutMultiplier(5)
- .call();
+ return null;
+ })
+ // Use a multiple of the default timeout to account for inner retries that may
+ // otherwise
+ // eat up the whole timeout so that no time is left to retry this outer action.
+ .defaultTimeoutMultiplier(5)
+ .call();
} catch (RestApiException e) {
logger.atSevere().withCause(e).log("Can't insert patchset");
} catch (UpdateException e) {
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommitsResult.java b/java/com/google/gerrit/server/git/receive/ReceiveCommitsResult.java
index ecbdcbc903..c2c368211c 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommitsResult.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommitsResult.java
@@ -18,6 +18,7 @@ import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Change;
import java.util.Arrays;
import java.util.EnumMap;
@@ -59,11 +60,13 @@ public abstract class ReceiveCommitsResult {
}
/** Record a change ID update as having completed. */
+ @CanIgnoreReturnValue
public Builder addChange(ChangeStatus key, Change.Id id) {
changes.get(key).add(id);
return this;
}
+ @CanIgnoreReturnValue
public abstract Builder magicPush(boolean isMagicPush);
public ReceiveCommitsResult build() {
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 254e57ba91..c0ffde3188 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -352,13 +352,14 @@ public class ReplaceOp implements BatchUpdateOp {
approvalCopierResult =
approvalsUtil.copyApprovalsToNewPatchSet(
- ctx.getNotes(), newPatchSet, ctx.getRevWalk(), ctx.getRepoView().getConfig(), update);
+ ctx.getNotes(), newPatchSet, ctx.getRepoView(), update);
mailMessage = insertChangeMessage(update, ctx, reviewMessage);
if (mergedByPushOp == null) {
resetChange(ctx);
} else {
- mergedByPushOp.setPatchSetProvider(Providers.of(newPatchSet)).updateChange(ctx);
+ @SuppressWarnings("unused")
+ var unused = mergedByPushOp.setPatchSetProvider(Providers.of(newPatchSet)).updateChange(ctx);
}
return true;
diff --git a/java/com/google/gerrit/server/git/receive/package-info.java b/java/com/google/gerrit/server/git/receive/package-info.java
new file mode 100644
index 0000000000..110fb4e789
--- /dev/null
+++ b/java/com/google/gerrit/server/git/receive/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.git.receive;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/git/receive/testing/BUILD b/java/com/google/gerrit/server/git/receive/testing/BUILD
index 06407aefea..fc18cbc880 100644
--- a/java/com/google/gerrit/server/git/receive/testing/BUILD
+++ b/java/com/google/gerrit/server/git/receive/testing/BUILD
@@ -10,5 +10,6 @@ java_library(
"//lib:jgit",
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
+ "//lib/errorprone:annotations",
],
)
diff --git a/java/com/google/gerrit/server/git/receive/testing/TestRefAdvertiser.java b/java/com/google/gerrit/server/git/receive/testing/TestRefAdvertiser.java
index 4d2805d7ef..47df03a8e1 100644
--- a/java/com/google/gerrit/server/git/receive/testing/TestRefAdvertiser.java
+++ b/java/com/google/gerrit/server/git/receive/testing/TestRefAdvertiser.java
@@ -19,12 +19,12 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.StreamSupport;
@@ -61,7 +61,7 @@ public class TestRefAdvertiser extends RefAdvertiser {
@Override
protected void writeOne(CharSequence line) throws IOException {
- List<String> lineParts =
+ ImmutableList<String> lineParts =
StreamSupport.stream(Splitter.on(' ').split(line).spliterator(), false)
.map(String::trim)
.collect(toImmutableList());
diff --git a/java/com/google/gerrit/server/git/receive/testing/package-info.java b/java/com/google/gerrit/server/git/receive/testing/package-info.java
new file mode 100644
index 0000000000..90f34d7a95
--- /dev/null
+++ b/java/com/google/gerrit/server/git/receive/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.git.receive.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/git/validators/CommentSizeValidator.java b/java/com/google/gerrit/server/git/validators/CommentSizeValidator.java
index 58b0cb19bf..f31f148eb6 100644
--- a/java/com/google/gerrit/server/git/validators/CommentSizeValidator.java
+++ b/java/com/google/gerrit/server/git/validators/CommentSizeValidator.java
@@ -46,7 +46,7 @@ public class CommentSizeValidator implements CommentValidator {
private boolean exceedsSizeLimit(CommentForValidation comment) {
switch (comment.getSource()) {
case HUMAN:
- return comment.getApproximateSize() > commentSizeLimit;
+ return commentSizeLimit > 0 && comment.getApproximateSize() > commentSizeLimit;
case ROBOT:
return robotCommentSizeLimit > 0 && comment.getApproximateSize() > robotCommentSizeLimit;
}
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidationListener.java b/java/com/google/gerrit/server/git/validators/CommitValidationListener.java
index fbc582bbbf..9f68c0d7d2 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidationListener.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidationListener.java
@@ -23,6 +23,11 @@ import java.util.List;
*
* <p>Invoked by Gerrit when a new commit is received, has passed basic Gerrit validation and can be
* then subject to extra validation checks.
+ *
+ * <p>Do not use {@link com.google.gerrit.server.patch.DiffOperations} from {@code
+ * CommitValidationListener} implementations to get the modified files for the received commit,
+ * instead use {@link com.google.gerrit.server.patch.DiffOperationsForCommitValidation} that is
+ * provided in {@link CommitReceivedEvent#diffOperations}.
*/
@ExtensionPoint
public interface CommitValidationListener {
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 6adaae214b..5c7d524e16 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -25,6 +25,7 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
@@ -36,6 +37,10 @@ import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
@@ -52,9 +57,7 @@ import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.gerrit.server.patch.DiffNotAvailableException;
-import com.google.gerrit.server.patch.DiffOperations;
-import com.google.gerrit.server.patch.DiffOptions;
-import com.google.gerrit.server.patch.filediff.FileDiffOutput;
+import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.RefPermission;
@@ -63,6 +66,7 @@ import com.google.gerrit.server.project.LabelConfigValidator;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
import com.google.gerrit.server.ssh.HostKey;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.gerrit.server.util.MagicBranch;
@@ -112,9 +116,10 @@ public class CommitValidators {
private final AccountCache accountCache;
private final ProjectCache projectCache;
private final ProjectConfig.Factory projectConfigFactory;
- private final DiffOperations diffOperations;
private final Config config;
private final ChangeUtil changeUtil;
+ private final MetricMaker metricMaker;
+ private final ApprovalQueryBuilder approvalQueryBuilder;
@Inject
Factory(
@@ -130,8 +135,9 @@ public class CommitValidators {
AccountCache accountCache,
ProjectCache projectCache,
ProjectConfig.Factory projectConfigFactory,
- DiffOperations diffOperations,
- ChangeUtil changeUtil) {
+ ChangeUtil changeUtil,
+ MetricMaker metricMaker,
+ ApprovalQueryBuilder approvalQueryBuilder) {
this.gerritIdent = gerritIdent;
this.urlFormatter = urlFormatter;
this.config = config;
@@ -144,8 +150,9 @@ public class CommitValidators {
this.accountCache = accountCache;
this.projectCache = projectCache;
this.projectConfigFactory = projectConfigFactory;
- this.diffOperations = diffOperations;
this.changeUtil = changeUtil;
+ this.metricMaker = metricMaker;
+ this.approvalQueryBuilder = approvalQueryBuilder;
}
public CommitValidators forReceiveCommits(
@@ -166,7 +173,7 @@ public class CommitValidators {
.add(new ProjectStateValidationListener(projectState))
.add(new AmendedGerritMergeCommitValidationListener(perm, gerritIdent))
.add(new AuthorUploaderValidator(user, perm, urlFormatter.get()))
- .add(new FileCountValidator(config, urlFormatter.get(), diffOperations))
+ .add(new FileCountValidator(config, urlFormatter.get(), metricMaker))
.add(new CommitterUploaderValidator(user, perm, urlFormatter.get()))
.add(new SignedOffByValidator(user, perm, projectState))
.add(
@@ -178,7 +185,7 @@ public class CommitValidators {
.add(new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker, accountCache))
.add(new AccountCommitValidator(repoManager, allUsers, accountValidator))
.add(new GroupCommitValidator(allUsers))
- .add(new LabelConfigValidator(diffOperations));
+ .add(new LabelConfigValidator(approvalQueryBuilder));
return new CommitValidators(validators.build());
}
@@ -198,7 +205,7 @@ public class CommitValidators {
.add(new ProjectStateValidationListener(projectState))
.add(new AmendedGerritMergeCommitValidationListener(perm, gerritIdent))
.add(new AuthorUploaderValidator(user, perm, urlFormatter.get()))
- .add(new FileCountValidator(config, urlFormatter.get(), diffOperations))
+ .add(new FileCountValidator(config, urlFormatter.get(), metricMaker))
.add(new SignedOffByValidator(user, perm, projectState))
.add(
new ChangeIdValidator(
@@ -208,7 +215,7 @@ public class CommitValidators {
.add(new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker, accountCache))
.add(new AccountCommitValidator(repoManager, allUsers, accountValidator))
.add(new GroupCommitValidator(allUsers))
- .add(new LabelConfigValidator(diffOperations));
+ .add(new LabelConfigValidator(approvalQueryBuilder));
return new CommitValidators(validators.build());
}
@@ -246,6 +253,7 @@ public class CommitValidators {
this.validators = validators;
}
+ @CanIgnoreReturnValue
public List<CommitValidationMessage> validate(CommitReceivedEvent receiveEvent)
throws CommitValidationException {
List<CommitValidationMessage> messages = new ArrayList<>();
@@ -444,11 +452,20 @@ public class CommitValidators {
private final int maxFileCount;
private final UrlFormatter urlFormatter;
- private final DiffOperations diffOperations;
+ private final Counter2<Integer, String> metricCountManyFilesPerChange;
- FileCountValidator(Config config, UrlFormatter urlFormatter, DiffOperations diffOperations) {
+ FileCountValidator(Config config, UrlFormatter urlFormatter, MetricMaker metricMaker) {
this.urlFormatter = urlFormatter;
- this.diffOperations = diffOperations;
+ this.metricCountManyFilesPerChange =
+ metricMaker.newCounter(
+ "validation/file_count",
+ new Description("Count commits with many files per change."),
+ Field.ofInteger("file_count", (meta, value) -> {})
+ .description("number of files in the patchset")
+ .build(),
+ Field.ofString("host_repo", (meta, value) -> {})
+ .description("host and repository of the change in the format 'host/repo'")
+ .build());
maxFileCount = config.getInt("change", null, "maxFiles", 100_000);
}
@@ -482,6 +499,9 @@ public class CommitValidators {
logger.atWarning().log(
"Warning: Change with %d files on host %s, project %s, ref %s",
changedFiles, host, project, refName);
+
+ this.metricCountManyFilesPerChange.increment(
+ Math.toIntExact(changedFiles), String.format("%s/%s", host, project));
}
} catch (DiffNotAvailableException e) {
// This happens e.g. for cherrypicks.
@@ -496,11 +516,14 @@ public class CommitValidators {
private int countChangedFiles(CommitReceivedEvent receiveEvent)
throws DiffNotAvailableException {
// For merge commits this will compare against auto-merge.
- Map<String, FileDiffOutput> modifiedFiles =
- diffOperations.listModifiedFilesAgainstParent(
- receiveEvent.getProjectNameKey(), receiveEvent.commit, 0, DiffOptions.DEFAULTS);
+ Map<String, ModifiedFile> modifiedFiles =
+ receiveEvent.diffOperations.loadModifiedFilesAgainstParentIfNecessary(
+ receiveEvent.getProjectNameKey(),
+ receiveEvent.commit,
+ 0,
+ /* enableRenameDetection= */ true);
// We don't want to count the COMMIT_MSG and MERGE_LIST files.
- List<FileDiffOutput> modifiedFilesList =
+ List<ModifiedFile> modifiedFilesList =
modifiedFiles.values().stream()
.filter(p -> !Patch.isMagic(p.newPath().orElse("")))
.collect(Collectors.toList());
@@ -977,7 +1000,7 @@ public class CommitValidators {
}
}
- private static CommitValidationMessage invalidEmail(
+ public static CommitValidationMessage invalidEmail(
String type, PersonIdent who, IdentifiedUser currentUser, UrlFormatter urlFormatter) {
StringBuilder sb = new StringBuilder();
diff --git a/java/com/google/gerrit/server/git/validators/MergeValidators.java b/java/com/google/gerrit/server/git/validators/MergeValidators.java
index 514dee1255..710e688442 100644
--- a/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -101,7 +101,7 @@ public class MergeValidators {
PatchSet.Id patchSetId,
IdentifiedUser caller)
throws MergeValidationException {
- List<MergeValidationListener> validators =
+ ImmutableList<MergeValidationListener> validators =
ImmutableList.of(
new PluginMergeValidationListener(mergeValidationListeners),
projectConfigValidatorFactory.create(),
diff --git a/java/com/google/gerrit/server/git/validators/RefOperationValidators.java b/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
index 9ac3c898b1..b11400b90f 100644
--- a/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
+++ b/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
@@ -18,6 +18,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
@@ -89,6 +90,7 @@ public class RefOperationValidators {
* when the first validator fails. Will not process any more validators after the first failure
* was encountered.
*/
+ @CanIgnoreReturnValue
public List<ValidationMessage> validateForRefOperation() throws RefOperationValidationException {
List<ValidationMessage> messages = new ArrayList<>();
boolean withException = false;
diff --git a/java/com/google/gerrit/server/git/validators/package-info.java b/java/com/google/gerrit/server/git/validators/package-info.java
new file mode 100644
index 0000000000..74c8cec5d4
--- /dev/null
+++ b/java/com/google/gerrit/server/git/validators/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.git.validators;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/group/db/GroupConfig.java b/java/com/google/gerrit/server/group/db/GroupConfig.java
index 682fd15f27..d378d51efb 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfig.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfig.java
@@ -23,6 +23,7 @@ import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
@@ -292,6 +293,7 @@ public class GroupConfig extends VersionedMetaData {
}
@Override
+ @CanIgnoreReturnValue
public RevCommit commit(MetaDataUpdate update) throws IOException {
RevCommit c = super.commit(update);
loadedGroup = Optional.of(loadedGroup.get().toBuilder().setRefState(c.toObjectId()).build());
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index 14f8825551..31538d3078 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -22,6 +22,7 @@ import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
@@ -263,6 +264,7 @@ public class GroupsUpdate {
* @throws IOException if indexing fails, or an error occurs while reading/writing from/to NoteDb
* @return the created {@link InternalGroup}
*/
+ @CanIgnoreReturnValue
public InternalGroup createGroup(InternalGroupCreation groupCreation, GroupDelta groupDelta)
throws DuplicateKeyException, IOException, ConfigInvalidException {
try (TraceTimer ignored =
@@ -364,6 +366,7 @@ public class GroupsUpdate {
}
@VisibleForTesting
+ @CanIgnoreReturnValue
public UpdateResult updateGroupInNoteDb(AccountGroup.UUID groupUuid, GroupDelta groupDelta)
throws IOException, ConfigInvalidException, DuplicateKeyException, NoSuchGroupException {
try (RefUpdateContext ctx = RefUpdateContext.open(GROUPS_UPDATE)) {
diff --git a/java/com/google/gerrit/server/group/db/package-info.java b/java/com/google/gerrit/server/group/db/package-info.java
new file mode 100644
index 0000000000..3c93d74b0c
--- /dev/null
+++ b/java/com/google/gerrit/server/group/db/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.group.db;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/group/db/testing/BUILD b/java/com/google/gerrit/server/group/db/testing/BUILD
index a4f49e9d40..c34f96f772 100644
--- a/java/com/google/gerrit/server/group/db/testing/BUILD
+++ b/java/com/google/gerrit/server/group/db/testing/BUILD
@@ -12,5 +12,6 @@ java_library(
"//lib:guava",
"//lib:jgit",
"//lib:jgit-junit",
+ "//lib/errorprone:annotations",
],
)
diff --git a/java/com/google/gerrit/server/group/db/testing/package-info.java b/java/com/google/gerrit/server/group/db/testing/package-info.java
new file mode 100644
index 0000000000..856ad1cd7e
--- /dev/null
+++ b/java/com/google/gerrit/server/group/db/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.group.db.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/group/package-info.java b/java/com/google/gerrit/server/group/package-info.java
new file mode 100644
index 0000000000..39ad2d9a9f
--- /dev/null
+++ b/java/com/google/gerrit/server/group/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.group;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/group/testing/BUILD b/java/com/google/gerrit/server/group/testing/BUILD
index bb2b20dcee..fcbda9fdce 100644
--- a/java/com/google/gerrit/server/group/testing/BUILD
+++ b/java/com/google/gerrit/server/group/testing/BUILD
@@ -12,6 +12,7 @@ java_library(
"//java/com/google/gerrit/server",
"//lib:guava",
"//lib:jgit",
+ "//lib/errorprone:annotations",
"//lib/truth",
],
)
diff --git a/java/com/google/gerrit/server/group/testing/TestGroupBackend.java b/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
index 1f3dbcb1f2..e181c2bbf2 100644
--- a/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
+++ b/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
@@ -55,6 +56,7 @@ public class TestGroupBackend implements GroupBackend {
* @param uuid the group UUID to add.
* @return the created group
*/
+ @CanIgnoreReturnValue
public GroupDescription.Basic create(AccountGroup.UUID uuid) {
checkState(uuid.get().startsWith(PREFIX), "test group UUID must have prefix '" + PREFIX + "'");
if (groups.containsKey(uuid)) {
diff --git a/java/com/google/gerrit/server/group/testing/package-info.java b/java/com/google/gerrit/server/group/testing/package-info.java
new file mode 100644
index 0000000000..d0529ccd18
--- /dev/null
+++ b/java/com/google/gerrit/server/group/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.group.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/index/AbstractIndexModule.java b/java/com/google/gerrit/server/index/AbstractIndexModule.java
index 9e9da9177d..e24a2b113d 100644
--- a/java/com/google/gerrit/server/index/AbstractIndexModule.java
+++ b/java/com/google/gerrit/server/index/AbstractIndexModule.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.index;
+import com.google.common.collect.ImmutableMap;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.project.ProjectIndex;
import com.google.gerrit.lifecycle.LifecycleModule;
@@ -25,7 +26,6 @@ import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.assistedinject.FactoryModuleBuilder;
-import java.util.Map;
import org.eclipse.jgit.lib.Config;
/**
@@ -37,10 +37,11 @@ public abstract class AbstractIndexModule extends AbstractModule {
public static final String INDEX_MODULE = "index-module";
private final int threads;
- private final Map<String, Integer> singleVersions;
+ private final ImmutableMap<String, Integer> singleVersions;
private final boolean slave;
- protected AbstractIndexModule(Map<String, Integer> singleVersions, int threads, boolean slave) {
+ protected AbstractIndexModule(
+ ImmutableMap<String, Integer> singleVersions, int threads, boolean slave) {
this.singleVersions = singleVersions;
this.threads = threads;
this.slave = slave;
diff --git a/java/com/google/gerrit/server/index/IndexModule.java b/java/com/google/gerrit/server/index/IndexModule.java
index c0bd62fee5..c048e3ccaa 100644
--- a/java/com/google/gerrit/server/index/IndexModule.java
+++ b/java/com/google/gerrit/server/index/IndexModule.java
@@ -19,12 +19,13 @@ import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
import com.google.common.base.Ticker;
import com.google.common.collect.FluentIterable;
-import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.IndexDefinition;
import com.google.gerrit.index.IndexType;
import com.google.gerrit.index.SchemaDefinitions;
@@ -34,6 +35,7 @@ import com.google.gerrit.index.project.ProjectIndexer;
import com.google.gerrit.index.project.ProjectSchemaDefinitions;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MultiProgressMonitor;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.index.account.AccountIndexCollection;
@@ -42,11 +44,13 @@ import com.google.gerrit.server.index.account.AccountIndexRewriter;
import com.google.gerrit.server.index.account.AccountIndexer;
import com.google.gerrit.server.index.account.AccountIndexerImpl;
import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
+import com.google.gerrit.server.index.change.AllChangesIndexer;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
import com.google.gerrit.server.index.change.ChangeIndexDefinition;
import com.google.gerrit.server.index.change.ChangeIndexRewriter;
import com.google.gerrit.server.index.change.ChangeIndexer;
import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
+import com.google.gerrit.server.index.change.StalenessChecker;
import com.google.gerrit.server.index.group.GroupIndexCollection;
import com.google.gerrit.server.index.group.GroupIndexDefinition;
import com.google.gerrit.server.index.group.GroupIndexRewriter;
@@ -65,7 +69,6 @@ import com.google.inject.ProvisionException;
import com.google.inject.Singleton;
import com.google.inject.multibindings.OptionalBinder;
import java.util.Collection;
-import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.Config;
@@ -77,7 +80,7 @@ import org.eclipse.jgit.lib.Config;
*/
@SuppressWarnings("ProvidesMethodOutsideOfModule")
public class IndexModule extends LifecycleModule {
- public static final ImmutableCollection<SchemaDefinitions<?>> ALL_SCHEMA_DEFS =
+ public static final ImmutableList<SchemaDefinitions<?>> ALL_SCHEMA_DEFS =
ImmutableList.of(
AccountSchemaDefinitions.INSTANCE,
ChangeSchemaDefinitions.INSTANCE,
@@ -130,6 +133,8 @@ public class IndexModule extends LifecycleModule {
bind(ChangeIndexCollection.class);
listener().to(ChangeIndexCollection.class);
factory(ChangeIndexer.Factory.class);
+ factory(StalenessChecker.Factory.class);
+ factory(AllChangesIndexer.Factory.class);
bind(GroupIndexRewriter.class);
// GroupIndexCollection is already bound very high up in SchemaModule.
@@ -174,9 +179,10 @@ public class IndexModule extends LifecycleModule {
ImmutableList<IndexDefinition<?, ?, ?>> result =
ImmutableList.of(accounts, groups, changes, projects);
- Set<String> expected =
+ ImmutableSet<String> expected =
FluentIterable.from(ALL_SCHEMA_DEFS).transform(SchemaDefinitions::getName).toSet();
- Set<String> actual = FluentIterable.from(result).transform(IndexDefinition::getName).toSet();
+ ImmutableSet<String> actual =
+ FluentIterable.from(result).transform(IndexDefinition::getName).toSet();
if (!expected.equals(actual)) {
throw new ProvisionException(
"need index definitions for all schemas: " + expected + " != " + actual);
@@ -255,6 +261,13 @@ public class IndexModule extends LifecycleModule {
return MoreExecutors.listeningDecorator(workQueue.createQueue(threads, "Index-Batch", true));
}
+ @Provides
+ @Singleton
+ StalenessChecker getChangeStalenessChecker(
+ ChangeIndexCollection indexes, GitRepositoryManager repoManager, IndexConfig indexConfig) {
+ return new StalenessChecker(indexes, repoManager, indexConfig);
+ }
+
@Singleton
private static class ShutdownIndexExecutors implements LifecycleListener {
private final ListeningExecutorService interactiveExecutor;
diff --git a/java/com/google/gerrit/server/index/IndexUtils.java b/java/com/google/gerrit/server/index/IndexUtils.java
index 352d376e9a..f81a9ceed9 100644
--- a/java/com/google/gerrit/server/index/IndexUtils.java
+++ b/java/com/google/gerrit/server/index/IndexUtils.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.index;
+import static com.google.gerrit.server.index.change.ChangeField.CHANGENUM_SPEC;
import static com.google.gerrit.server.index.change.ChangeField.CHANGE_SPEC;
import static com.google.gerrit.server.index.change.ChangeField.NUMERIC_ID_STR_SPEC;
import static com.google.gerrit.server.index.change.ChangeField.PROJECT_SPEC;
@@ -76,15 +77,23 @@ public final class IndexUtils {
// Ensure we request enough fields to construct a ChangeData. We need both
// change ID and project, which can either come via the Change field or
// separate fields.
- Set<String> fs = opts.fields();
+ ImmutableSet<String> fs = opts.fields();
if (fs.contains(CHANGE_SPEC.getName())) {
// A Change is always sufficient.
return fs;
}
- if (fs.contains(PROJECT_SPEC.getName()) && fs.contains(NUMERIC_ID_STR_SPEC.getName())) {
+
+ Set<String> requiredFields =
+ CHANGENUM_SPEC.getName() != null
+ ? ImmutableSet.of(
+ NUMERIC_ID_STR_SPEC.getName(), PROJECT_SPEC.getName(), CHANGENUM_SPEC.getName())
+ : ImmutableSet.of(NUMERIC_ID_STR_SPEC.getName(), PROJECT_SPEC.getName());
+
+ if (fs.containsAll(requiredFields)) {
return fs;
}
- return Sets.union(fs, ImmutableSet.of(NUMERIC_ID_STR_SPEC.getName(), PROJECT_SPEC.getName()));
+
+ return Sets.union(fs, ImmutableSet.copyOf(requiredFields));
}
/**
@@ -93,7 +102,7 @@ public final class IndexUtils {
* is temporary and should be removed after the migration is done.
*/
public static Set<String> groupFields(QueryOptions opts) {
- Set<String> fs = opts.fields();
+ ImmutableSet<String> fs = opts.fields();
return fs.contains(GroupField.UUID_FIELD_SPEC.getName())
? fs
: Sets.union(fs, ImmutableSet.of(GroupField.UUID_FIELD_SPEC.getName()));
@@ -115,7 +124,7 @@ public final class IndexUtils {
* doesn't support.
*/
public static Set<String> projectFields(QueryOptions opts) {
- Set<String> fs = opts.fields();
+ ImmutableSet<String> fs = opts.fields();
return fs.contains(ProjectField.NAME_SPEC.getName())
? fs
: Sets.union(fs, ImmutableSet.of(ProjectField.NAME_SPEC.getName()));
diff --git a/java/com/google/gerrit/server/index/IndexVersionReindexer.java b/java/com/google/gerrit/server/index/IndexVersionReindexer.java
new file mode 100644
index 0000000000..84be97e5cf
--- /dev/null
+++ b/java/com/google/gerrit/server/index/IndexVersionReindexer.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.index;
+
+import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.index.Index;
+import com.google.gerrit.index.IndexDefinition;
+import com.google.gerrit.index.SiteIndexer;
+import com.google.inject.Inject;
+import java.util.concurrent.Future;
+
+public class IndexVersionReindexer {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private ListeningExecutorService executor;
+
+ @Inject
+ IndexVersionReindexer(@IndexExecutor(BATCH) ListeningExecutorService executor) {
+ this.executor = executor;
+ }
+
+ public <K, V, I extends Index<K, V>> Future<SiteIndexer.Result> reindex(
+ IndexDefinition<K, V, I> def, int version, boolean reuse, boolean notifyListeners) {
+ I index = def.getIndexCollection().getWriteIndex(version);
+ SiteIndexer<K, V, I> siteIndexer = def.getSiteIndexer(reuse);
+ return executor.submit(
+ () -> {
+ String name = def.getName();
+ logger.atInfo().log("Starting reindex of %s version %d", name, version);
+ SiteIndexer.Result result = siteIndexer.indexAll(index, notifyListeners);
+ if (result.success()) {
+ logger.atInfo().log("Reindex %s version %s complete", name, version);
+ } else {
+ logger.atInfo().log(
+ "Reindex %s version %s failed. Successfully indexed %s, failed to index %s",
+ name, version, result.doneCount(), result.failedCount());
+ }
+ return result;
+ });
+ }
+}
diff --git a/java/com/google/gerrit/server/index/OnlineReindexer.java b/java/com/google/gerrit/server/index/OnlineReindexer.java
index eef394d66d..98abf46458 100644
--- a/java/com/google/gerrit/server/index/OnlineReindexer.java
+++ b/java/com/google/gerrit/server/index/OnlineReindexer.java
@@ -42,18 +42,21 @@ public class OnlineReindexer<K, V, I extends Index<K, V>> {
private final PluginSetContext<OnlineUpgradeListener> listeners;
private I index;
private final AtomicBoolean running = new AtomicBoolean();
+ private final boolean reuseExistingDocuments;
public OnlineReindexer(
IndexDefinition<K, V, I> def,
int oldVersion,
int newVersion,
- PluginSetContext<OnlineUpgradeListener> listeners) {
+ PluginSetContext<OnlineUpgradeListener> listeners,
+ boolean reuseExistingDocuments) {
this.name = def.getName();
this.indexes = def.getIndexCollection();
this.batchIndexer = def.getSiteIndexer();
this.oldVersion = oldVersion;
this.newVersion = newVersion;
this.listeners = listeners;
+ this.reuseExistingDocuments = reuseExistingDocuments;
}
/** Starts the background process. */
@@ -106,7 +109,7 @@ public class OnlineReindexer<K, V, I extends Index<K, V>> {
"Starting online reindex of %s from schema version %s to %s",
name, version(indexes.getSearchIndex()), version(index));
- if (oldVersion != newVersion) {
+ if (!reuseExistingDocuments && oldVersion != newVersion) {
index.deleteAll();
}
SiteIndexer.Result result = batchIndexer.indexAll(index);
diff --git a/java/com/google/gerrit/server/index/SingleVersionModule.java b/java/com/google/gerrit/server/index/SingleVersionModule.java
index 50dc4e9cfb..bad5ffe0db 100644
--- a/java/com/google/gerrit/server/index/SingleVersionModule.java
+++ b/java/com/google/gerrit/server/index/SingleVersionModule.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.index;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.index.Index;
@@ -30,7 +31,6 @@ import com.google.inject.name.Names;
import com.google.inject.util.Providers;
import java.util.Collection;
import java.util.Map;
-import java.util.Set;
import org.eclipse.jgit.lib.Config;
/**
@@ -41,9 +41,9 @@ import org.eclipse.jgit.lib.Config;
public class SingleVersionModule extends LifecycleModule {
public static final String SINGLE_VERSIONS = "IndexModule/SingleVersions";
- private final Map<String, Integer> singleVersions;
+ private final ImmutableMap<String, Integer> singleVersions;
- public SingleVersionModule(Map<String, Integer> singleVersions) {
+ public SingleVersionModule(ImmutableMap<String, Integer> singleVersions) {
this.singleVersions = singleVersions;
}
@@ -58,7 +58,7 @@ public class SingleVersionModule extends LifecycleModule {
/** Listener to Gerrit's lifecycle events to specify which index versions to use. */
@Singleton
public static class SingleVersionListener implements LifecycleListener {
- private final Set<String> disabled;
+ private final ImmutableSet<String> disabled;
private final Collection<IndexDefinition<?, ?, ?>> defs;
private final Map<String, Integer> singleVersions;
diff --git a/java/com/google/gerrit/server/index/VersionManager.java b/java/com/google/gerrit/server/index/VersionManager.java
index cdb69c6a51..2c38caf388 100644
--- a/java/com/google/gerrit/server/index/VersionManager.java
+++ b/java/com/google/gerrit/server/index/VersionManager.java
@@ -59,6 +59,7 @@ public abstract class VersionManager implements LifecycleListener {
}
protected final boolean onlineUpgrade;
+ protected final boolean reuseExistingDocuments;
protected final String runReindexMsg;
protected final SitePaths sitePaths;
@@ -72,7 +73,8 @@ public abstract class VersionManager implements LifecycleListener {
SitePaths sitePaths,
PluginSetContext<OnlineUpgradeListener> listeners,
Collection<IndexDefinition<?, ?, ?>> defs,
- boolean onlineUpgrade) {
+ boolean onlineUpgrade,
+ boolean reuseExistingDocuments) {
this.sitePaths = sitePaths;
this.listeners = listeners;
this.defs = Maps.newHashMapWithExpectedSize(defs.size());
@@ -82,6 +84,7 @@ public abstract class VersionManager implements LifecycleListener {
this.reindexers = Maps.newHashMapWithExpectedSize(defs.size());
this.onlineUpgrade = onlineUpgrade;
+ this.reuseExistingDocuments = reuseExistingDocuments;
this.runReindexMsg =
"No index versions for index '%s' ready; run java -jar "
+ sitePaths.gerrit_war.toAbsolutePath()
@@ -190,7 +193,7 @@ public abstract class VersionManager implements LifecycleListener {
if (!reindexers.containsKey(def.getName())) {
int latest = write.get(0).version;
OnlineReindexer<K, V, I> reindexer =
- new OnlineReindexer<>(def, search.version, latest, listeners);
+ new OnlineReindexer<>(def, search.version, latest, listeners, reuseExistingDocuments);
reindexers.put(def.getName(), reindexer);
}
}
@@ -206,7 +209,7 @@ public abstract class VersionManager implements LifecycleListener {
search != null, "no search index ready for %s; should have failed at startup", name);
int searchVersion = search.getSchema().getVersion();
- List<Index<?, ?>> write = ImmutableList.copyOf(indexes.getWriteIndexes());
+ ImmutableList<Index<?, ?>> write = ImmutableList.copyOf(indexes.getWriteIndexes());
checkState(
!write.isEmpty(),
"no write indexes set for %s; should have been initialized at startup",
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexer.java b/java/com/google/gerrit/server/index/account/AccountIndexer.java
index a0551138e4..03982e7f79 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexer.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexer.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.index.account;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Account;
/** Interface for indexing a Gerrit account. */
@@ -32,5 +33,6 @@ public interface AccountIndexer {
* @param id account id to index.
* @return whether the account was reindexed
*/
+ @CanIgnoreReturnValue
boolean reindexIfStale(Account.Id id);
}
diff --git a/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java b/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
index fd264a19ba..d8e2a7b8e6 100644
--- a/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
@@ -88,7 +88,10 @@ public class AccountSchemaDefinitions extends SchemaDefinitions<AccountState> {
@Deprecated static final Schema<AccountState> V12 = schema(V11);
// Upgrade Lucene to 8.x requires reindexing.
- static final Schema<AccountState> V13 = schema(V12);
+ @Deprecated static final Schema<AccountState> V13 = schema(V12);
+
+ // Upgrade Lucene to 9.x requires reindexing.
+ static final Schema<AccountState> V14 = schema(V13);
/**
* Name of the account index to be used when contacting index backends or loading configurations.
diff --git a/java/com/google/gerrit/server/index/account/ReindexAccountsAfterRefUpdate.java b/java/com/google/gerrit/server/index/account/ReindexAccountsAfterRefUpdate.java
new file mode 100644
index 0000000000..4a5e3faec2
--- /dev/null
+++ b/java/com/google/gerrit/server/index/account/ReindexAccountsAfterRefUpdate.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.index.account;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.events.GitBatchRefUpdateListener;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+/**
+ * Listener for ref update events that reindexes accounts in case the updated Git reference was used
+ * to compute contents of an index document.
+ *
+ * <p>Will reindex accounts when the account's NoteDb ref changes.
+ */
+public class ReindexAccountsAfterRefUpdate implements GitBatchRefUpdateListener {
+ private final AllUsersName allUsersName;
+ private final Provider<AccountIndexer> accountIndexer;
+
+ @Inject
+ ReindexAccountsAfterRefUpdate(
+ AllUsersName allUsersName, Provider<AccountIndexer> accountIndexer) {
+ this.allUsersName = allUsersName;
+ this.accountIndexer = accountIndexer;
+ }
+
+ @Override
+ public void onGitBatchRefUpdate(Event event) {
+ if (!allUsersName.get().equals(event.getProjectName())) {
+ return;
+ }
+ for (UpdatedRef ref : event.getUpdatedRefs()) {
+ if (RefNames.isRefsUsers(ref.getRefName()) && !RefNames.isRefsEdit(ref.getRefName())) {
+ Account.Id accountId = Account.Id.fromRef(ref.getRefName());
+ if (accountId != null) {
+ accountIndexer.get().index(accountId);
+ }
+ }
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/index/account/StalenessChecker.java b/java/com/google/gerrit/server/index/account/StalenessChecker.java
index 699dfbe72d..f98f893a40 100644
--- a/java/com/google/gerrit/server/index/account/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/account/StalenessChecker.java
@@ -42,7 +42,6 @@ import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
-import java.util.Set;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
@@ -137,7 +136,7 @@ public class StalenessChecker {
}
}
- Set<ExternalId> extIds = externalIds.byAccount(id);
+ ImmutableSet<ExternalId> extIds = externalIds.byAccount(id);
ListMultimap<ObjectId, ObjectId> extIdStates =
parseExternalIdStates(
diff --git a/java/com/google/gerrit/server/index/account/package-info.java b/java/com/google/gerrit/server/index/account/package-info.java
new file mode 100644
index 0000000000..88821eb5af
--- /dev/null
+++ b/java/com/google/gerrit/server/index/account/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.index.account;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index 3935108199..19a0223927 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -43,7 +43,8 @@ import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeNotes.Factory.ChangeNotesResult;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.query.change.ChangeData;
-import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -52,6 +53,7 @@ import java.util.concurrent.Callable;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
@@ -65,6 +67,13 @@ import org.eclipse.jgit.lib.Repository;
*/
public class AllChangesIndexer extends SiteIndexer<Change.Id, ChangeData, ChangeIndex> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ public interface Factory {
+ AllChangesIndexer create();
+
+ AllChangesIndexer create(boolean reuseExistingDocuments);
+ }
+
private MultiProgressMonitor mpm;
private VolatileTask doneTask;
private Task failedTask;
@@ -84,25 +93,54 @@ public class AllChangesIndexer extends SiteIndexer<Change.Id, ChangeData, Change
private final GitRepositoryManager repoManager;
private final ListeningExecutorService executor;
private final ChangeIndexer.Factory indexerFactory;
+ private final StalenessChecker.Factory stalenessCheckerFactory;
private final ChangeNotes.Factory notesFactory;
private final ProjectCache projectCache;
private final Set<Project.NameKey> projectsToSkip;
+ private final boolean reuseExistingDocuments;
- @Inject
+ @AssistedInject
AllChangesIndexer(
MultiProgressMonitor.Factory multiProgressMonitorFactory,
ChangeData.Factory changeDataFactory,
GitRepositoryManager repoManager,
@IndexExecutor(BATCH) ListeningExecutorService executor,
ChangeIndexer.Factory indexerFactory,
+ StalenessChecker.Factory stalenessCheckerFactory,
ChangeNotes.Factory notesFactory,
ProjectCache projectCache,
@GerritServerConfig Config config) {
+ this(
+ multiProgressMonitorFactory,
+ changeDataFactory,
+ repoManager,
+ executor,
+ indexerFactory,
+ stalenessCheckerFactory,
+ notesFactory,
+ projectCache,
+ config,
+ config.getBoolean("index", null, "reuseExistingDocuments", false));
+ }
+
+ @AssistedInject
+ AllChangesIndexer(
+ MultiProgressMonitor.Factory multiProgressMonitorFactory,
+ ChangeData.Factory changeDataFactory,
+ GitRepositoryManager repoManager,
+ @IndexExecutor(BATCH) ListeningExecutorService executor,
+ ChangeIndexer.Factory indexerFactory,
+ StalenessChecker.Factory stalenessCheckerFactory,
+ ChangeNotes.Factory notesFactory,
+ ProjectCache projectCache,
+ @GerritServerConfig Config config,
+ @Assisted boolean reuseExistingDocuments) {
this.multiProgressMonitorFactory = multiProgressMonitorFactory;
this.changeDataFactory = changeDataFactory;
this.repoManager = repoManager;
this.executor = executor;
this.indexerFactory = indexerFactory;
+ this.stalenessCheckerFactory = stalenessCheckerFactory;
this.notesFactory = notesFactory;
this.projectCache = projectCache;
this.projectsToSkip =
@@ -110,6 +148,7 @@ public class AllChangesIndexer extends SiteIndexer<Change.Id, ChangeData, Change
.stream()
.map(p -> Project.NameKey.parse(p))
.collect(Collectors.toSet());
+ this.reuseExistingDocuments = reuseExistingDocuments;
}
@AutoValue
@@ -138,6 +177,11 @@ public class AllChangesIndexer extends SiteIndexer<Change.Id, ChangeData, Change
@Override
public Result indexAll(ChangeIndex index) {
+ return indexAll(index, true);
+ }
+
+ @Override
+ public Result indexAll(ChangeIndex index, boolean notifyListeners) {
// The simplest approach to distribute indexing would be to let each thread grab a project
// and index it fully. But if a site has one big project and 100s of small projects, then
// in the beginning all CPUs would be busy reindexing projects. But soon enough all small
@@ -160,7 +204,7 @@ public class AllChangesIndexer extends SiteIndexer<Change.Id, ChangeData, Change
failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
List<ListenableFuture<?>> futures;
try {
- futures = new SliceScheduler(index, ok).schedule();
+ futures = new SliceScheduler(index, ok, notifyListeners).schedule();
} catch (ProjectsCollectionFailure e) {
logger.atSevere().log("%s", e.getMessage());
return Result.create(sw, false, 0, 0);
@@ -218,20 +262,27 @@ public class AllChangesIndexer extends SiteIndexer<Change.Id, ChangeData, Change
}
private class ProjectSliceIndexer implements Callable<Void> {
- private final ChangeIndexer indexer;
private final ProjectSlice projectSlice;
private final ProgressMonitor done;
private final ProgressMonitor failed;
+ private final Consumer<ChangeData> indexAction;
private ProjectSliceIndexer(
ChangeIndexer indexer,
ProjectSlice projectSlice,
ProgressMonitor done,
ProgressMonitor failed) {
- this.indexer = indexer;
this.projectSlice = projectSlice;
this.done = done;
this.failed = failed;
+ if (reuseExistingDocuments) {
+ indexAction =
+ cd -> {
+ var unused = indexer.reindexIfStale(cd);
+ };
+ } else {
+ indexAction = cd -> indexer.index(cd);
+ }
}
@Override
@@ -271,7 +322,7 @@ public class AllChangesIndexer extends SiteIndexer<Change.Id, ChangeData, Change
return;
}
try {
- indexer.index(changeDataFactory.create(r.notes()));
+ indexAction.accept(changeDataFactory.create(r.notes()));
done.update(1);
verboseWriter.format(
"Reindexed change %d (project: %s)\n", r.id().get(), r.notes().getProjectName().get());
@@ -313,6 +364,7 @@ public class AllChangesIndexer extends SiteIndexer<Change.Id, ChangeData, Change
private class SliceScheduler {
final ChangeIndex index;
final AtomicBoolean ok;
+ final boolean notifyListeners;
final AtomicInteger changeCount = new AtomicInteger(0);
final AtomicInteger projectsFailed = new AtomicInteger(0);
final List<ListenableFuture<?>> sliceIndexerFutures = new ArrayList<>();
@@ -320,9 +372,10 @@ public class AllChangesIndexer extends SiteIndexer<Change.Id, ChangeData, Change
VolatileTask projTask = mpm.beginVolatileSubTask("project-slices");
Task slicingProjects;
- public SliceScheduler(ChangeIndex index, AtomicBoolean ok) {
+ public SliceScheduler(ChangeIndex index, AtomicBoolean ok, boolean notifyListeners) {
this.index = index;
this.ok = ok;
+ this.notifyListeners = notifyListeners;
}
private List<ListenableFuture<?>> schedule() throws ProjectsCollectionFailure {
@@ -330,7 +383,7 @@ public class AllChangesIndexer extends SiteIndexer<Change.Id, ChangeData, Change
int projectCount = projects.size();
slicingProjects = mpm.beginSubTask("Slicing projects", projectCount);
for (Project.NameKey name : projects) {
- sliceCreationFutures.add(executor.submit(new ProjectSliceCreator(name)));
+ sliceCreationFutures.add(executor.submit(new ProjectSliceCreator(name, notifyListeners)));
}
try {
@@ -361,9 +414,11 @@ public class AllChangesIndexer extends SiteIndexer<Change.Id, ChangeData, Change
private class ProjectSliceCreator implements Callable<Void> {
final Project.NameKey name;
+ final boolean notifyListeners;
- public ProjectSliceCreator(Project.NameKey name) {
+ public ProjectSliceCreator(Project.NameKey name, boolean notifyListeners) {
this.name = name;
+ this.notifyListeners = notifyListeners;
}
@Override
@@ -385,13 +440,16 @@ public class AllChangesIndexer extends SiteIndexer<Change.Id, ChangeData, Change
for (int slice = 0; slice < slices; slice++) {
ProjectSlice projectSlice = ProjectSlice.create(name, slice, slices, metaIdByChange);
+ ChangeIndexer indexer;
+ if (reuseExistingDocuments) {
+ indexer =
+ indexerFactory.create(
+ executor, index, stalenessCheckerFactory.create(index), notifyListeners);
+ } else {
+ indexer = indexerFactory.create(executor, index, notifyListeners);
+ }
ListenableFuture<?> future =
- executor.submit(
- reindexProjectSlice(
- indexerFactory.create(executor, index),
- projectSlice,
- doneTask,
- failedTask));
+ executor.submit(reindexProjectSlice(indexer, projectSlice, doneTask, failedTask));
String description = "project " + name + " (" + slice + "/" + slices + ")";
addErrorListener(future, description, projTask, ok);
sliceIndexerFutures.add(future);
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 2b91d9adc8..045482af52 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.index.change;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_CHANGE_NUMBER;
import static com.google.gerrit.server.util.AttentionSetUtil.additionsOnly;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.joining;
@@ -133,6 +134,15 @@ public class ChangeField {
public static final IndexedField<ChangeData, String>.SearchSpec NUMERIC_ID_STR_SPEC =
NUMERIC_ID_STR_FIELD.exact("legacy_id_str");
+ public static final IndexedField<ChangeData, Integer> CHANGENUM_FIELD =
+ IndexedField.<ChangeData>integerBuilder("ChangeNumber")
+ .stored()
+ .required()
+ .build(cd -> cd.getId().get());
+
+ public static final IndexedField<ChangeData, Integer>.SearchSpec CHANGENUM_SPEC =
+ CHANGENUM_FIELD.integer(FIELD_CHANGE_NUMBER);
+
/** Newer style Change-Id key. */
public static final IndexedField<ChangeData, String> CHANGE_ID_FIELD =
IndexedField.<ChangeData>stringBuilder("ChangeId")
@@ -910,7 +920,7 @@ public class ChangeField {
return SchemaUtil.getPersonParts(cd.getAuthor());
}
- public static Set<String> getAuthorNameAndEmail(ChangeData cd) {
+ public static ImmutableSet<String> getAuthorNameAndEmail(ChangeData cd) {
return getNameAndEmail(cd.getAuthor());
}
@@ -918,11 +928,11 @@ public class ChangeField {
return SchemaUtil.getPersonParts(cd.getCommitter());
}
- public static Set<String> getCommitterNameAndEmail(ChangeData cd) {
+ public static ImmutableSet<String> getCommitterNameAndEmail(ChangeData cd) {
return getNameAndEmail(cd.getCommitter());
}
- private static Set<String> getNameAndEmail(PersonIdent person) {
+ private static ImmutableSet<String> getNameAndEmail(PersonIdent person) {
if (person == null) {
return ImmutableSet.of();
}
@@ -1740,14 +1750,14 @@ public class ChangeField {
return converter.toProto(object);
}
- private static <V extends MessageLite, T> List<V> entitiesToProtos(
+ private static <V extends MessageLite, T> ImmutableList<V> entitiesToProtos(
ProtoConverter<V, T> converter, Collection<T> objects) {
return objects.stream()
.map(object -> entityToProto(converter, object))
.collect(toImmutableList());
}
- private static <V extends MessageLite, T> List<T> decodeProtosToEntities(
+ private static <V extends MessageLite, T> ImmutableList<T> decodeProtosToEntities(
Iterable<V> raw, ProtoConverter<V, T> converter) {
return StreamSupport.stream(raw.spliterator(), false)
.map(proto -> decodeProtoToEntity(proto, converter))
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexDefinition.java b/java/com/google/gerrit/server/index/change/ChangeIndexDefinition.java
index dde9d867b6..342796b1b6 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexDefinition.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexDefinition.java
@@ -14,20 +14,34 @@
package com.google.gerrit.server.index.change;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Change;
import com.google.gerrit.index.IndexDefinition;
+import com.google.gerrit.index.SiteIndexer;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
/** Bundle of service classes that make up the change index. */
public class ChangeIndexDefinition extends IndexDefinition<Change.Id, ChangeData, ChangeIndex> {
+ private final AllChangesIndexer.Factory allChangesIndexerFactory;
+
@Inject
ChangeIndexDefinition(
ChangeIndexCollection indexCollection,
ChangeIndex.Factory indexFactory,
- @Nullable AllChangesIndexer allChangesIndexer) {
- super(ChangeSchemaDefinitions.INSTANCE, indexCollection, indexFactory, allChangesIndexer);
+ AllChangesIndexer.Factory allChangesIndexerFactory) {
+ super(ChangeSchemaDefinitions.INSTANCE, indexCollection, indexFactory, null);
+ this.allChangesIndexerFactory = allChangesIndexerFactory;
+ }
+
+ @Override
+ public SiteIndexer<Change.Id, ChangeData, ChangeIndex> getSiteIndexer() {
+ return allChangesIndexerFactory.create();
+ }
+
+ @Override
+ public SiteIndexer<Change.Id, ChangeData, ChangeIndex> getSiteIndexer(
+ boolean reuseExistingDocuments) {
+ return allChangesIndexerFactory.create(reuseExistingDocuments);
}
}
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
index bb4b24c0ee..233125508a 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.index.change;
import static com.google.gerrit.server.query.change.ChangeStatusPredicate.closed;
import static com.google.gerrit.server.query.change.ChangeStatusPredicate.open;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gerrit.common.Nullable;
@@ -58,10 +59,10 @@ import org.eclipse.jgit.util.MutableInteger;
@Singleton
public class ChangeIndexRewriter implements IndexRewriter<ChangeData> {
/** Set of all open change statuses. */
- public static final Set<Change.Status> OPEN_STATUSES;
+ public static final ImmutableSet<Change.Status> OPEN_STATUSES;
/** Set of all closed change statuses. */
- public static final Set<Change.Status> CLOSED_STATUSES;
+ public static final ImmutableSet<Change.Status> CLOSED_STATUSES;
static {
EnumSet<Change.Status> open = EnumSet.noneOf(Change.Status.class);
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index faa5629be9..fc666ada46 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -21,6 +21,7 @@ import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
@@ -69,7 +70,19 @@ public class ChangeIndexer {
public interface Factory {
ChangeIndexer create(ListeningExecutorService executor, ChangeIndex index);
+ ChangeIndexer create(
+ ListeningExecutorService executor, ChangeIndex index, boolean notifyListeners);
+
+ ChangeIndexer create(
+ ListeningExecutorService executor,
+ ChangeIndex index,
+ StalenessChecker stalenessChecker,
+ boolean notifyListeners);
+
ChangeIndexer create(ListeningExecutorService executor, ChangeIndexCollection indexes);
+
+ ChangeIndexer create(
+ ListeningExecutorService executor, ChangeIndexCollection indexes, boolean notifyListeners);
}
@Nullable private final ChangeIndexCollection indexes;
@@ -83,6 +96,7 @@ public class ChangeIndexer {
private final StalenessChecker stalenessChecker;
private final boolean autoReindexIfStale;
private final IsFirstInsertForEntry isFirstInsertForEntry;
+ private final boolean notifyListeners;
private final Map<Change.Id, IndexTask> queuedIndexTasks = new ConcurrentHashMap<>();
private final Set<ReindexIfStaleTask> queuedReindexIfStaleTasks =
@@ -100,6 +114,33 @@ public class ChangeIndexer {
@Assisted ListeningExecutorService executor,
@Assisted ChangeIndex index,
IsFirstInsertForEntry isFirstInsertForEntry) {
+ this(
+ cfg,
+ changeDataFactory,
+ notesFactory,
+ context,
+ indexedListeners,
+ stalenessChecker,
+ batchExecutor,
+ executor,
+ index,
+ true,
+ isFirstInsertForEntry);
+ }
+
+ @AssistedInject
+ ChangeIndexer(
+ @GerritServerConfig Config cfg,
+ ChangeData.Factory changeDataFactory,
+ ChangeNotes.Factory notesFactory,
+ ThreadLocalRequestContext context,
+ PluginSetContext<ChangeIndexedListener> indexedListeners,
+ StalenessChecker stalenessChecker,
+ @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
+ @Assisted ListeningExecutorService executor,
+ @Assisted ChangeIndex index,
+ @Assisted boolean notifyListeners,
+ IsFirstInsertForEntry isFirstInsertForEntry) {
this.executor = executor;
this.changeDataFactory = changeDataFactory;
this.notesFactory = notesFactory;
@@ -111,6 +152,34 @@ public class ChangeIndexer {
this.index = index;
this.indexes = null;
this.isFirstInsertForEntry = isFirstInsertForEntry;
+ this.notifyListeners = notifyListeners;
+ }
+
+ @AssistedInject
+ ChangeIndexer(
+ @GerritServerConfig Config cfg,
+ ChangeData.Factory changeDataFactory,
+ ChangeNotes.Factory notesFactory,
+ ThreadLocalRequestContext context,
+ PluginSetContext<ChangeIndexedListener> indexedListeners,
+ @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
+ IsFirstInsertForEntry isFirstInsertForEntry,
+ @Assisted ListeningExecutorService executor,
+ @Assisted ChangeIndex index,
+ @Assisted StalenessChecker stalenessChecker,
+ @Assisted boolean notifyListeners) {
+ this.executor = executor;
+ this.changeDataFactory = changeDataFactory;
+ this.notesFactory = notesFactory;
+ this.context = context;
+ this.indexedListeners = indexedListeners;
+ this.batchExecutor = batchExecutor;
+ this.autoReindexIfStale = autoReindexIfStale(cfg);
+ this.isFirstInsertForEntry = isFirstInsertForEntry;
+ this.index = index;
+ this.indexes = null;
+ this.stalenessChecker = stalenessChecker;
+ this.notifyListeners = notifyListeners;
}
@AssistedInject
@@ -125,6 +194,33 @@ public class ChangeIndexer {
@Assisted ListeningExecutorService executor,
@Assisted ChangeIndexCollection indexes,
IsFirstInsertForEntry isFirstInsertForEntry) {
+ this(
+ cfg,
+ changeDataFactory,
+ notesFactory,
+ context,
+ indexedListeners,
+ stalenessChecker,
+ batchExecutor,
+ executor,
+ indexes,
+ true,
+ isFirstInsertForEntry);
+ }
+
+ @AssistedInject
+ ChangeIndexer(
+ @GerritServerConfig Config cfg,
+ ChangeData.Factory changeDataFactory,
+ ChangeNotes.Factory notesFactory,
+ ThreadLocalRequestContext context,
+ PluginSetContext<ChangeIndexedListener> indexedListeners,
+ StalenessChecker stalenessChecker,
+ @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
+ @Assisted ListeningExecutorService executor,
+ @Assisted ChangeIndexCollection indexes,
+ @Assisted boolean notifyListeners,
+ IsFirstInsertForEntry isFirstInsertForEntry) {
this.executor = executor;
this.changeDataFactory = changeDataFactory;
this.notesFactory = notesFactory;
@@ -135,6 +231,7 @@ public class ChangeIndexer {
this.autoReindexIfStale = autoReindexIfStale(cfg);
this.index = null;
this.indexes = indexes;
+ this.notifyListeners = notifyListeners;
this.isFirstInsertForEntry = isFirstInsertForEntry;
}
@@ -261,19 +358,27 @@ public class ChangeIndexer {
}
private void fireChangeScheduledForIndexingEvent(String projectName, int id) {
- indexedListeners.runEach(l -> l.onChangeScheduledForIndexing(projectName, id));
+ if (notifyListeners) {
+ indexedListeners.runEach(l -> l.onChangeScheduledForIndexing(projectName, id));
+ }
}
private void fireChangeIndexedEvent(String projectName, int id) {
- indexedListeners.runEach(l -> l.onChangeIndexed(projectName, id));
+ if (notifyListeners) {
+ indexedListeners.runEach(l -> l.onChangeIndexed(projectName, id));
+ }
}
private void fireChangeScheduledForDeletionFromIndexEvent(int id) {
- indexedListeners.runEach(l -> l.onChangeScheduledForDeletionFromIndex(id));
+ if (notifyListeners) {
+ indexedListeners.runEach(l -> l.onChangeScheduledForDeletionFromIndex(id));
+ }
}
private void fireChangeDeletedFromIndexEvent(int id) {
- indexedListeners.runEach(l -> l.onChangeDeleted(id));
+ if (notifyListeners) {
+ indexedListeners.runEach(l -> l.onChangeDeleted(id));
+ }
}
/**
@@ -349,7 +454,7 @@ public class ChangeIndexer {
* @param id ID of the change to index.
* @return future for reindexing the change; returns true if the change was stale.
*/
- public ListenableFuture<Boolean> reindexIfStale(Project.NameKey project, Change.Id id) {
+ public ListenableFuture<Boolean> asyncReindexIfStale(Project.NameKey project, Change.Id id) {
ReindexIfStaleTask task = new ReindexIfStaleTask(project, id);
if (queuedReindexIfStaleTasks.add(task)) {
return submit(task, batchExecutor);
@@ -357,6 +462,41 @@ public class ChangeIndexer {
return Futures.immediateFuture(false);
}
+ /**
+ * Synchronously check if a change is stale, and reindex if it is.
+ *
+ * @param cd the change data to be checked for staleness.
+ * @return true if the change was stale, false if it was up-to-date
+ */
+ public boolean reindexIfStale(ChangeData cd) {
+ return reindexIfStale(cd.project(), cd.getId());
+ }
+
+ /**
+ * Synchronously check if a change is stale, and reindex if it is.
+ *
+ * @param project the project to which the change belongs.
+ * @param id ID of the change to index.
+ * @return true if the change was stale, false if it was up-to-date
+ */
+ public boolean reindexIfStale(Project.NameKey project, Change.Id id) {
+ try {
+ StalenessCheckResult stalenessCheckResult = stalenessChecker.check(id);
+ if (stalenessCheckResult.isStale()) {
+ logger.atInfo().log("Reindexing stale document %s", stalenessCheckResult);
+ indexImpl(changeDataFactory.create(project, id));
+ return true;
+ }
+ } catch (Exception e) {
+ if (!isCausedByRepositoryNotFoundException(e)) {
+ throw e;
+ }
+ logger.atFine().log(
+ "Change %s belongs to deleted project %s, aborting reindexing the change.", id, project);
+ }
+ return false;
+ }
+
private void autoReindexIfStale(ChangeData cd) {
autoReindexIfStale(cd.project(), cd.getId());
}
@@ -365,7 +505,7 @@ public class ChangeIndexer {
if (autoReindexIfStale) {
// Don't retry indefinitely; if this fails the change will be stale.
@SuppressWarnings("unused")
- Future<?> possiblyIgnoredError = reindexIfStale(project, id);
+ Future<?> possiblyIgnoredError = asyncReindexIfStale(project, id);
}
}
@@ -410,7 +550,8 @@ public class ChangeIndexer {
try {
return callImpl();
} finally {
- context.setContext(oldCtx);
+ @SuppressWarnings("unused")
+ var unused = context.setContext(oldCtx);
}
} catch (Exception e) {
logger.atSevere().withCause(e).log("Failed to execute %s", this);
@@ -503,6 +644,7 @@ public class ChangeIndexer {
@Nullable
@Override
+ @CanIgnoreReturnValue
public ChangeData call() {
logger.atFine().log("Delete change %d from index.", id.get());
// Don't bother setting a RequestContext to provide the DB.
@@ -543,22 +685,7 @@ public class ChangeIndexer {
@Override
public Boolean callImpl() throws Exception {
remove();
- try {
- StalenessCheckResult stalenessCheckResult = stalenessChecker.check(id);
- if (stalenessCheckResult.isStale()) {
- logger.atInfo().log("Reindexing stale document %s", stalenessCheckResult);
- indexImpl(changeDataFactory.create(project, id));
- return true;
- }
- } catch (Exception e) {
- if (!isCausedByRepositoryNotFoundException(e)) {
- throw e;
- }
- logger.atFine().log(
- "Change %s belongs to deleted project %s, aborting reindexing the change.",
- id.get(), project.get());
- }
- return false;
+ return reindexIfStale(project, id);
}
@Override
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 5474e6bce0..4921b3f96e 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -250,6 +250,7 @@ public class ChangeSchemaDefinitions extends SchemaDefinitions<ChangeData> {
/** Upgrade Lucene to 8.x requires reindexing. */
@Deprecated static final Schema<ChangeData> V83 = schema(V82);
+ @Deprecated
static final Schema<ChangeData> V84 =
new Schema.Builder<ChangeData>()
.add(V83)
@@ -257,6 +258,17 @@ public class ChangeSchemaDefinitions extends SchemaDefinitions<ChangeData> {
.addSearchSpecs(ChangeField.CUSTOM_KEYED_VALUES_SPEC)
.build();
+ /** Upgrade Lucene to 9.x requires reindexing. */
+ @Deprecated static final Schema<ChangeData> V85 = schema(V84);
+
+ /** Add ChangeNumber field */
+ static final Schema<ChangeData> V86 =
+ new Schema.Builder<ChangeData>()
+ .add(V85)
+ .addIndexedFields(ChangeField.CHANGENUM_FIELD)
+ .addSearchSpecs(ChangeField.CHANGENUM_SPEC)
+ .build();
+
/**
* Name of the change index to be used when contacting index backends or loading configurations.
*/
diff --git a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java b/java/com/google/gerrit/server/index/change/ReindexChangesAfterRefUpdate.java
index f6a4ce1e0b..aad6916c5b 100644
--- a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
+++ b/java/com/google/gerrit/server/index/change/ReindexChangesAfterRefUpdate.java
@@ -21,7 +21,6 @@ import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
@@ -32,7 +31,6 @@ import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.QueueProvider.QueueType;
import com.google.gerrit.server.index.IndexExecutor;
-import com.google.gerrit.server.index.account.AccountIndexer;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.util.ManualRequestContext;
import com.google.gerrit.server.util.OneOffRequestContext;
@@ -45,42 +43,37 @@ import java.util.concurrent.Future;
import org.eclipse.jgit.lib.Config;
/**
- * Listener for ref update events that reindexes entities in case the updated Git reference was used
+ * Listener for ref update events that reindexes changes in case the updated Git reference was used
* to compute contents of an index document.
*
* <p>Reindexes any open changes that has a destination branch that was updated to ensure that
* 'mergeable' is still current.
- *
- * <p>Will reindex accounts when the account's NoteDb ref changes.
*/
-public class ReindexAfterRefUpdate implements GitBatchRefUpdateListener {
+public class ReindexChangesAfterRefUpdate implements GitBatchRefUpdateListener {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final OneOffRequestContext requestContext;
private final Provider<InternalChangeQuery> queryProvider;
- private final ChangeIndexer.Factory indexerFactory;
- private final ChangeIndexCollection indexes;
+ private final ChangeIndexer.Factory changeIndexerFactory;
+ private final ChangeIndexCollection changeIndexes;
private final AllUsersName allUsersName;
- private final Provider<AccountIndexer> indexer;
private final ListeningExecutorService executor;
private final boolean enabled;
@Inject
- ReindexAfterRefUpdate(
+ ReindexChangesAfterRefUpdate(
@GerritServerConfig Config cfg,
OneOffRequestContext requestContext,
Provider<InternalChangeQuery> queryProvider,
- ChangeIndexer.Factory indexerFactory,
- ChangeIndexCollection indexes,
+ ChangeIndexer.Factory changeIndexerFactory,
+ ChangeIndexCollection changeIndexes,
AllUsersName allUsersName,
- Provider<AccountIndexer> indexer,
@IndexExecutor(QueueType.BATCH) ListeningExecutorService executor) {
this.requestContext = requestContext;
this.queryProvider = queryProvider;
- this.indexerFactory = indexerFactory;
- this.indexes = indexes;
+ this.changeIndexerFactory = changeIndexerFactory;
+ this.changeIndexes = changeIndexes;
this.allUsersName = allUsersName;
- this.indexer = indexer;
this.executor = executor;
this.enabled = MergeabilityComputationBehavior.fromConfig(cfg).includeInIndex();
}
@@ -88,14 +81,6 @@ public class ReindexAfterRefUpdate implements GitBatchRefUpdateListener {
@Override
public void onGitBatchRefUpdate(GitBatchRefUpdateListener.Event event) {
if (allUsersName.get().equals(event.getProjectName())) {
- for (UpdatedRef ref : event.getUpdatedRefs()) {
- if (RefNames.isRefsUsers(ref.getRefName()) && !RefNames.isRefsEdit(ref.getRefName())) {
- Account.Id accountId = Account.Id.fromRef(ref.getRefName());
- if (accountId != null) {
- indexer.get().index(accountId);
- }
- }
- }
if (event.getUpdatedRefs().stream()
.noneMatch(ru -> ru.getRefName().equals(RefNames.REFS_CONFIG))) {
// The update is in All-Users and not on refs/meta/config. So it's not a change. Return
@@ -113,13 +98,15 @@ public class ReindexAfterRefUpdate implements GitBatchRefUpdateListener {
}
Futures.addCallback(
executor.submit(new GetChanges(event.getProjectName(), ref)),
- new FutureCallback<List<Change>>() {
+ new FutureCallback<>() {
@Override
public void onSuccess(List<Change> changes) {
for (Change c : changes) {
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError =
- indexerFactory.create(executor, indexes).indexAsync(c.getProject(), c.getId());
+ changeIndexerFactory
+ .create(executor, changeIndexes)
+ .indexAsync(c.getProject(), c.getId());
}
}
diff --git a/java/com/google/gerrit/server/index/change/StalenessChecker.java b/java/com/google/gerrit/server/index/change/StalenessChecker.java
index eb4af01c5d..83f61891e7 100644
--- a/java/com/google/gerrit/server/index/change/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/change/StalenessChecker.java
@@ -27,6 +27,7 @@ import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
@@ -36,8 +37,8 @@ import com.google.gerrit.index.RefState;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.index.StalenessCheckResult;
import com.google.gerrit.server.query.change.ChangeData;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
@@ -50,26 +51,39 @@ import org.eclipse.jgit.lib.Repository;
* Checker that compares values stored in the change index to metadata in NoteDb to detect index
* documents that should have been updated (= stale).
*/
-@Singleton
public class StalenessChecker {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ public interface Factory {
+ StalenessChecker create(ChangeIndex index);
+ }
+
public static final ImmutableSet<String> FIELDS =
ImmutableSet.of(
ChangeField.CHANGE_SPEC.getName(),
ChangeField.REF_STATE_SPEC.getName(),
ChangeField.REF_STATE_PATTERN_SPEC.getName());
- private final ChangeIndexCollection indexes;
+ @Nullable private final ChangeIndexCollection indexes;
+ @Nullable private final ChangeIndex index;
private final GitRepositoryManager repoManager;
private final IndexConfig indexConfig;
- @Inject
- StalenessChecker(
+ public StalenessChecker(
ChangeIndexCollection indexes, GitRepositoryManager repoManager, IndexConfig indexConfig) {
this.indexes = indexes;
+ this.index = null;
+ this.repoManager = repoManager;
+ this.indexConfig = indexConfig;
+ }
+
+ @AssistedInject
+ StalenessChecker(
+ GitRepositoryManager repoManager, IndexConfig indexConfig, @Assisted ChangeIndex index) {
+ this.indexes = null;
this.repoManager = repoManager;
this.indexConfig = indexConfig;
+ this.index = index;
}
/**
@@ -77,7 +91,18 @@ public class StalenessChecker {
* provided {@link com.google.gerrit.entities.Change.Id}.
*/
public StalenessCheckResult check(Change.Id id) {
- ChangeIndex i = indexes.getSearchIndex();
+ if (index != null) {
+ return check(id, index);
+ }
+ return check(id, indexes.getSearchIndex());
+ }
+
+ /**
+ * Returns a {@link StalenessCheckResult} with structured information about staleness of the
+ * provided {@link com.google.gerrit.entities.Change.Id} in the provided {@link
+ * com.google.gerrit.server.index.change.ChangeIndex}.
+ */
+ private StalenessCheckResult check(Change.Id id, ChangeIndex i) {
if (i == null) {
return StalenessCheckResult
.notStale(); // No index; caller couldn't do anything if it is stale.
diff --git a/java/com/google/gerrit/server/index/change/package-info.java b/java/com/google/gerrit/server/index/change/package-info.java
new file mode 100644
index 0000000000..f9aeaea3d3
--- /dev/null
+++ b/java/com/google/gerrit/server/index/change/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.index.change;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java b/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
index 3773d4354d..52668c5ff9 100644
--- a/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
+++ b/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
@@ -18,6 +18,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
import com.google.common.base.Stopwatch;
+import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@@ -74,7 +75,7 @@ public class AllGroupsIndexer extends SiteIndexer<AccountGroup.UUID, InternalGro
ProgressMonitor progress = new TextProgressMonitor(newPrintWriter(progressOut));
progress.start(2);
Stopwatch sw = Stopwatch.createStarted();
- List<AccountGroup.UUID> uuids;
+ ImmutableList<AccountGroup.UUID> uuids;
try {
uuids = collectGroups(progress);
} catch (IOException | ConfigInvalidException e) {
@@ -138,7 +139,7 @@ public class AllGroupsIndexer extends SiteIndexer<AccountGroup.UUID, InternalGro
return SiteIndexer.Result.create(sw, ok.get(), done.get(), failed.get());
}
- private List<AccountGroup.UUID> collectGroups(ProgressMonitor progress)
+ private ImmutableList<AccountGroup.UUID> collectGroups(ProgressMonitor progress)
throws IOException, ConfigInvalidException {
progress.beginTask("Collecting groups", ProgressMonitor.UNKNOWN);
try {
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexer.java b/java/com/google/gerrit/server/index/group/GroupIndexer.java
index d6b9186db1..f5e72a7f71 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndexer.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndexer.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.index.group;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.AccountGroup;
/** Interface for indexing an internal Gerrit group. */
@@ -32,5 +33,6 @@ public interface GroupIndexer {
* @param uuid group UUID to index.
* @return whether the group was reindexed
*/
+ @CanIgnoreReturnValue
boolean reindexIfStale(AccountGroup.UUID uuid);
}
diff --git a/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java b/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
index 1b87d27c22..33006b830e 100644
--- a/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
@@ -72,7 +72,10 @@ public class GroupSchemaDefinitions extends SchemaDefinitions<InternalGroup> {
@Deprecated static final Schema<InternalGroup> V9 = schema(V8);
// Upgrade Lucene to 8.x requires reindexing.
- static final Schema<InternalGroup> V10 = schema(V9);
+ @Deprecated static final Schema<InternalGroup> V10 = schema(V9);
+
+ // Upgrade Lucene to 9.x requires reindexing.
+ static final Schema<InternalGroup> V11 = schema(V10);
/** Singleton instance of the schema definitions. This is one per JVM. */
public static final GroupSchemaDefinitions INSTANCE = new GroupSchemaDefinitions();
diff --git a/java/com/google/gerrit/server/index/group/package-info.java b/java/com/google/gerrit/server/index/group/package-info.java
new file mode 100644
index 0000000000..f6b0ca39ff
--- /dev/null
+++ b/java/com/google/gerrit/server/index/group/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.index.group;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/index/options/package-info.java b/java/com/google/gerrit/server/index/options/package-info.java
new file mode 100644
index 0000000000..b0c2e95e58
--- /dev/null
+++ b/java/com/google/gerrit/server/index/options/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.index.options;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/index/package-info.java b/java/com/google/gerrit/server/index/package-info.java
new file mode 100644
index 0000000000..a5ff950453
--- /dev/null
+++ b/java/com/google/gerrit/server/index/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.index;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/index/project/StalenessChecker.java b/java/com/google/gerrit/server/index/project/StalenessChecker.java
index b2e24e46f4..5f0d9012ed 100644
--- a/java/com/google/gerrit/server/index/project/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/project/StalenessChecker.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.index.project;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.SetMultimap;
import com.google.gerrit.entities.Project;
@@ -73,7 +74,7 @@ public class StalenessChecker {
return StalenessCheckResult.stale("Document %s missing from index", project);
}
- SetMultimap<Project.NameKey, RefState> indexedRefStates =
+ ImmutableSetMultimap<Project.NameKey, RefState> indexedRefStates =
RefState.parseStates(result.get().getValue(ProjectField.REF_STATE_SPEC));
SetMultimap<Project.NameKey, RefState> currentRefStates =
diff --git a/java/com/google/gerrit/server/index/project/package-info.java b/java/com/google/gerrit/server/index/project/package-info.java
new file mode 100644
index 0000000000..7b6205575b
--- /dev/null
+++ b/java/com/google/gerrit/server/index/project/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.index.project;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/ioutil/BUILD b/java/com/google/gerrit/server/ioutil/BUILD
index 6b9ecdfaf1..a78330891b 100644
--- a/java/com/google/gerrit/server/ioutil/BUILD
+++ b/java/com/google/gerrit/server/ioutil/BUILD
@@ -11,5 +11,6 @@ java_library(
"//lib:guava",
"//lib:jgit",
"//lib:jgit-archive",
+ "//lib/errorprone:annotations",
],
)
diff --git a/java/com/google/gerrit/server/ioutil/package-info.java b/java/com/google/gerrit/server/ioutil/package-info.java
new file mode 100644
index 0000000000..6edaa8b3ef
--- /dev/null
+++ b/java/com/google/gerrit/server/ioutil/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.ioutil;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/logging/BUILD b/java/com/google/gerrit/server/logging/BUILD
index 7204c078f6..53f75b31f1 100644
--- a/java/com/google/gerrit/server/logging/BUILD
+++ b/java/com/google/gerrit/server/logging/BUILD
@@ -16,6 +16,7 @@ java_library(
"//lib:jgit",
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
],
diff --git a/java/com/google/gerrit/server/logging/CallerFinder.java b/java/com/google/gerrit/server/logging/CallerFinder.java
index 4cb4b7fb97..a6c2131f77 100644
--- a/java/com/google/gerrit/server/logging/CallerFinder.java
+++ b/java/com/google/gerrit/server/logging/CallerFinder.java
@@ -195,15 +195,17 @@ public abstract class CallerFinder {
public abstract CallerFinder build();
}
+ public String findCaller() {
+ return targets().stream()
+ .map(t -> findCallerOf(t, skip() + 1))
+ .filter(Optional::isPresent)
+ .findFirst()
+ .map(Optional::get)
+ .orElse("unknown");
+ }
+
public LazyArg<String> findCallerLazy() {
- return lazy(
- () ->
- targets().stream()
- .map(t -> findCallerOf(t, skip() + 1))
- .filter(Optional::isPresent)
- .findFirst()
- .map(Optional::get)
- .orElse("unknown"));
+ return lazy(() -> findCaller());
}
private Optional<String> findCallerOf(Class<?> target, int skip) {
diff --git a/java/com/google/gerrit/server/logging/LoggingContext.java b/java/com/google/gerrit/server/logging/LoggingContext.java
index eac96a676e..adc9c2b594 100644
--- a/java/com/google/gerrit/server/logging/LoggingContext.java
+++ b/java/com/google/gerrit/server/logging/LoggingContext.java
@@ -20,6 +20,7 @@ import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.flogger.context.Tags;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.inject.Provider;
import java.util.List;
import java.util.concurrent.Callable;
@@ -157,6 +158,7 @@ public class LoggingContext extends com.google.common.flogger.backend.system.Log
return Boolean.TRUE.equals(forceLogging.get());
}
+ @CanIgnoreReturnValue
boolean forceLogging(boolean force) {
Boolean oldValue = forceLogging.get();
if (force) {
@@ -273,6 +275,7 @@ public class LoggingContext extends com.google.common.flogger.backend.system.Log
* @param enable whether ACL logging should be enabled.
* @return whether ACL logging was be enabled before invoking this method (old value).
*/
+ @CanIgnoreReturnValue
boolean aclLogging(boolean enable) {
Boolean oldValue = aclLogging.get();
if (enable) {
diff --git a/java/com/google/gerrit/server/logging/Metadata.java b/java/com/google/gerrit/server/logging/Metadata.java
index b433e9f58f..b7f3404a15 100644
--- a/java/com/google/gerrit/server/logging/Metadata.java
+++ b/java/com/google/gerrit/server/logging/Metadata.java
@@ -20,6 +20,7 @@ import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.LazyArg;
import com.google.common.flogger.LazyArgs;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -40,6 +41,12 @@ public abstract class Metadata {
*/
public abstract Optional<String> actionType();
+ /**
+ * Number of attempt. The first execution has {@code attempt=1}, the first retry has {@code
+ * attempt=2}.
+ */
+ public abstract Optional<Integer> attempt();
+
/** An authentication domain name. */
public abstract Optional<String> authDomainName();
@@ -52,6 +59,9 @@ public abstract class Metadata {
/** The name of a cache. */
public abstract Optional<String> cacheName();
+ /** The caller that triggered the operation. */
+ public abstract Optional<String> caller();
+
/** The name of the implementation class. */
public abstract Optional<String> className();
@@ -185,20 +195,20 @@ public abstract class Metadata {
* few are populated this leads to long string representations such as
*
* <pre>
- * Metadata{accountId=Optional.empty, actionType=Optional.empty, authDomainName=Optional.empty,
- * branchName=Optional.empty, cacheKey=Optional.empty, cacheName=Optional.empty,
- * className=Optional.empty, cancellationReason=Optional.empty changeId=Optional[9212550],
- * changeIdType=Optional.empty, cause=Optional.empty, diffAlgorithm=Optional.empty,
- * eventType=Optional.empty, exportValue=Optional.empty, filePath=Optional.empty,
- * garbageCollectorName=Optional.empty, gitOperation=Optional.empty, groupId=Optional.empty,
- * groupName=Optional.empty, groupUuid=Optional.empty, httpStatus=Optional.empty,
- * indexName=Optional.empty, indexVersion=Optional[0], methodName=Optional.empty,
- * multiple=Optional.empty, operationName=Optional.empty, partial=Optional.empty,
- * noteDbFilePath=Optional.empty, noteDbRefName=Optional.empty,
- * noteDbSequenceType=Optional.empty, patchSetId=Optional.empty, pluginMetadata=[],
- * pluginName=Optional.empty, projectName=Optional.empty, pushType=Optional.empty,
- * requestType=Optional.empty, resourceCount=Optional.empty, restViewName=Optional.empty,
- * revision=Optional.empty, username=Optional.empty}
+ * Metadata{accountId=Optional.empty, actionType=Optional.empty, attempt=Optional.empty,
+ * authDomainName=Optional.empty, branchName=Optional.empty, cacheKey=Optional.empty,
+ * cacheName=Optional.empty, caller=Optional.empty, className=Optional.empty,
+ * cancellationReason=Optional.empty, changeId=Optional[9212550], changeIdType=Optional.empty,
+ * cause=Optional.empty, diffAlgorithm=Optional.empty, eventType=Optional.empty,
+ * exportValue=Optional.empty, filePath=Optional.empty, garbageCollectorName=Optional.empty,
+ * gitOperation=Optional.empty, groupId=Optional.empty, groupName=Optional.empty,
+ * groupUuid=Optional.empty, httpStatus=Optional.empty, indexName=Optional.empty,
+ * indexVersion=Optional[0], methodName=Optional.empty, multiple=Optional.empty,
+ * operationName=Optional.empty, partial=Optional.empty, noteDbFilePath=Optional.empty,
+ * noteDbRefName=Optional.empty, noteDbSequenceType=Optional.empty, patchSetId=Optional.empty,
+ * pluginMetadata=[], pluginName=Optional.empty, projectName=Optional.empty,
+ * pushType=Optional.empty, requestType=Optional.empty, resourceCount=Optional.empty,
+ * restViewName=Optional.empty, revision=Optional.empty, username=Optional.empty}
* </pre>
*
* <p>That's hard to read in logs. This is why this method
@@ -293,6 +303,8 @@ public abstract class Metadata {
public abstract Builder actionType(@Nullable String actionType);
+ public abstract Builder attempt(int attempt);
+
public abstract Builder authDomainName(@Nullable String authDomainName);
public abstract Builder branchName(@Nullable String branchName);
@@ -301,6 +313,8 @@ public abstract class Metadata {
public abstract Builder cacheName(@Nullable String cacheName);
+ public abstract Builder caller(@Nullable String caller);
+
public abstract Builder className(@Nullable String className);
public abstract Builder cancellationReason(@Nullable String cancellationReason);
@@ -363,6 +377,7 @@ public abstract class Metadata {
abstract ImmutableList.Builder<PluginMetadata> pluginMetadataBuilder();
+ @CanIgnoreReturnValue
public Builder addPluginMetadata(PluginMetadata pluginMetadata) {
pluginMetadataBuilder().add(pluginMetadata);
return this;
diff --git a/java/com/google/gerrit/server/logging/MutableTags.java b/java/com/google/gerrit/server/logging/MutableTags.java
index 3f48b596e7..1c7ce63d79 100644
--- a/java/com/google/gerrit/server/logging/MutableTags.java
+++ b/java/com/google/gerrit/server/logging/MutableTags.java
@@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.SetMultimap;
import com.google.common.flogger.context.Tags;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
public class MutableTags {
private final SetMultimap<String, String> tagMap =
@@ -39,6 +40,7 @@ public class MutableTags {
* @return {@code true} if the tag was added, {@code false} if the tag was not added because it
* already exists
*/
+ @CanIgnoreReturnValue
public boolean add(String name, String value) {
requireNonNull(name, "tag name is required");
requireNonNull(value, "tag value is required");
diff --git a/java/com/google/gerrit/server/logging/PerformanceLogRecord.java b/java/com/google/gerrit/server/logging/PerformanceLogRecord.java
index 046eeb3441..07d9b9028f 100644
--- a/java/com/google/gerrit/server/logging/PerformanceLogRecord.java
+++ b/java/com/google/gerrit/server/logging/PerformanceLogRecord.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.logging;
import com.google.auto.value.AutoValue;
+import java.time.Instant;
import java.util.Optional;
/**
@@ -33,7 +34,8 @@ public abstract class PerformanceLogRecord {
* @return the performance log record
*/
public static PerformanceLogRecord create(String operation, long durationMs) {
- return new AutoValue_PerformanceLogRecord(operation, durationMs, Optional.empty());
+ return new AutoValue_PerformanceLogRecord(
+ operation, durationMs, Instant.now(), Optional.empty());
}
/**
@@ -45,20 +47,23 @@ public abstract class PerformanceLogRecord {
* @return the performance log record
*/
public static PerformanceLogRecord create(String operation, long durationMs, Metadata metadata) {
- return new AutoValue_PerformanceLogRecord(operation, durationMs, Optional.of(metadata));
+ return new AutoValue_PerformanceLogRecord(
+ operation, durationMs, Instant.now(), Optional.of(metadata));
}
public abstract String operation();
public abstract long durationMs();
+ public abstract Instant endTime();
+
public abstract Optional<Metadata> metadata();
void writeTo(PerformanceLogger performanceLogger) {
if (metadata().isPresent()) {
- performanceLogger.log(operation(), durationMs(), metadata().get());
+ performanceLogger.log(operation(), durationMs(), endTime(), metadata().get());
} else {
- performanceLogger.log(operation(), durationMs());
+ performanceLogger.log(operation(), durationMs(), endTime());
}
}
}
diff --git a/java/com/google/gerrit/server/logging/PerformanceLogger.java b/java/com/google/gerrit/server/logging/PerformanceLogger.java
index 74a1684549..bed53baeb3 100644
--- a/java/com/google/gerrit/server/logging/PerformanceLogger.java
+++ b/java/com/google/gerrit/server/logging/PerformanceLogger.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.logging;
import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import java.time.Instant;
/**
* Extension point for logging performance records.
@@ -35,8 +36,8 @@ public interface PerformanceLogger {
* @param operation operation that was performed
* @param durationMs time that the execution of the operation took (in milliseconds)
*/
- default void log(String operation, long durationMs) {
- log(operation, durationMs, Metadata.empty());
+ default void log(String operation, long durationMs, Instant endTime) {
+ log(operation, durationMs, endTime, Metadata.empty());
}
/**
@@ -46,5 +47,5 @@ public interface PerformanceLogger {
* @param durationMs time that the execution of the operation took (in milliseconds)
* @param metadata metadata
*/
- void log(String operation, long durationMs, Metadata metadata);
+ void log(String operation, long durationMs, Instant endTime, Metadata metadata);
}
diff --git a/java/com/google/gerrit/server/logging/TraceContext.java b/java/com/google/gerrit/server/logging/TraceContext.java
index 487e0afe1c..fb698f72ac 100644
--- a/java/com/google/gerrit/server/logging/TraceContext.java
+++ b/java/com/google/gerrit/server/logging/TraceContext.java
@@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Table;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.server.cancellation.RequestStateContext;
import java.util.Optional;
@@ -238,10 +239,12 @@ public class TraceContext implements AutoCloseable {
this.oldAclLogRecords = LoggingContext.getInstance().getAclLogRecords();
}
+ @CanIgnoreReturnValue
public TraceContext addTag(RequestId.Type requestId, Object tagValue) {
return addTag(requireNonNull(requestId, "request ID is required").name(), tagValue);
}
+ @CanIgnoreReturnValue
public TraceContext addTag(String tagName, Object tagValue) {
String name = requireNonNull(tagName, "tag name is required");
String value = requireNonNull(tagValue, "tag value is required").toString();
@@ -255,10 +258,12 @@ public class TraceContext implements AutoCloseable {
return tagMap.build();
}
+ @CanIgnoreReturnValue
public TraceContext addPluginTag(String pluginName) {
return addTag(PLUGIN_TAG, pluginName);
}
+ @CanIgnoreReturnValue
public TraceContext forceLogging() {
if (stopForceLoggingOnClose) {
return this;
@@ -285,6 +290,7 @@ public class TraceContext implements AutoCloseable {
return LoggingContext.getInstance().getTagsAsMap().get(tagName).stream().findFirst();
}
+ @CanIgnoreReturnValue
public TraceContext enableAclLogging() {
if (stopAclLoggingOnClose) {
return this;
diff --git a/java/com/google/gerrit/server/logging/package-info.java b/java/com/google/gerrit/server/logging/package-info.java
new file mode 100644
index 0000000000..59dbd89235
--- /dev/null
+++ b/java/com/google/gerrit/server/logging/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.logging;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/mail/package-info.java b/java/com/google/gerrit/server/mail/package-info.java
new file mode 100644
index 0000000000..a8ba36234d
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.mail;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 1898a98055..6c38210997 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -22,6 +22,7 @@ import static java.util.stream.Collectors.toList;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
@@ -167,14 +168,16 @@ public class MailProcessor {
* @param message {@link MailMessage} to process
*/
public void process(MailMessage message) throws RestApiException, UpdateException {
- retryHelper
- .changeUpdate(
- "processCommentsReceivedByEmail",
- buf -> {
- processImpl(buf, message);
- return null;
- })
- .call();
+ @SuppressWarnings("unused")
+ var unused =
+ retryHelper
+ .changeUpdate(
+ "processCommentsReceivedByEmail",
+ buf -> {
+ processImpl(buf, message);
+ return null;
+ })
+ .call();
}
private void processImpl(BatchUpdate.Factory buf, MailMessage message)
@@ -198,7 +201,7 @@ public class MailProcessor {
return;
}
- Set<Account.Id> accountIds = emails.getAccountFor(metadata.author);
+ ImmutableSet<Account.Id> accountIds = emails.getAccountFor(metadata.author);
if (accountIds.size() != 1) {
logger.atSevere().log(
@@ -249,7 +252,7 @@ public class MailProcessor {
queryProvider
.get()
.enforceVisibility(true)
- .byLegacyChangeId(Change.id(metadata.changeNumber));
+ .byChangeNumber(Change.id(metadata.changeNumber));
if (changeDataList.isEmpty()) {
sendRejectionEmail(message, InboundEmailError.CHANGE_NOT_FOUND);
return;
@@ -449,6 +452,7 @@ public class MailProcessor {
(short) side.ordinal(),
mailComment.getMessage(),
false,
+ null,
null);
comment.tag = tag;
diff --git a/java/com/google/gerrit/server/mail/receive/package-info.java b/java/com/google/gerrit/server/mail/receive/package-info.java
new file mode 100644
index 0000000000..bee1966fbd
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/receive/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.mail.receive;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmailImpl.java b/java/com/google/gerrit/server/mail/send/ChangeEmailImpl.java
index 4ecbd521c5..388b0d039a 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmailImpl.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmailImpl.java
@@ -19,6 +19,7 @@ import static com.google.gerrit.server.util.AttentionSetUtil.additionsOnly;
import com.google.auto.factory.AutoFactory;
import com.google.auto.factory.Provided;
+import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
@@ -58,7 +59,6 @@ import java.text.MessageFormat;
import java.time.Instant;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -88,7 +88,7 @@ public class ChangeEmailImpl implements ChangeEmail {
// Available after init or after being explicitly set.
protected OutgoingEmail email;
- private List<Account.Id> stars;
+ private ImmutableList<Account.Id> stars;
protected PatchSet patchSet;
protected PatchSetInfo patchSetInfo;
private String changeMessage;
@@ -447,7 +447,7 @@ public class ChangeEmailImpl implements ChangeEmail {
return watch.getWatchers(type, includeWatchersFromNotifyConfig);
}
- /** Any user who has published comments on this change. */
+ /** CC all users who are added as reviewer or cc to the change. */
@Override
public void ccAllApprovals() {
if (!NotifyHandling.ALL.equals(email.getNotify().handling())
@@ -459,6 +459,9 @@ public class ChangeEmailImpl implements ChangeEmail {
for (Account.Id id : changeData.reviewers().all()) {
email.addByAccountId(RecipientType.CC, id);
}
+ for (Address addr : this.changeData.reviewersByEmail().all()) {
+ email.addByEmail(RecipientType.CC, addr);
+ }
} catch (StorageException err) {
logger.atWarning().withCause(err).log("Cannot CC users that reviewed updated change");
}
@@ -476,6 +479,9 @@ public class ChangeEmailImpl implements ChangeEmail {
for (Account.Id id : changeData.reviewers().byState(ReviewerStateInternal.REVIEWER)) {
email.addByAccountId(RecipientType.CC, id);
}
+ for (Address addr : changeData.reviewersByEmail().byState(ReviewerStateInternal.REVIEWER)) {
+ email.addByEmail(RecipientType.CC, addr);
+ }
} catch (StorageException err) {
logger.atWarning().withCause(err).log("Cannot CC users that commented on updated change");
}
@@ -619,17 +625,6 @@ public class ChangeEmailImpl implements ChangeEmail {
currentAttentionSet.stream().map(email::getNameFor).sorted().collect(toImmutableList()));
setChangeSubjectHeader();
- if (email.getNotify().handling().equals(NotifyHandling.OWNER_REVIEWERS)
- || email.getNotify().handling().equals(NotifyHandling.ALL)) {
- try {
- this.changeData.reviewersByEmail().byState(ReviewerStateInternal.CC).stream()
- .forEach(address -> email.addByEmail(RecipientType.CC, address));
- this.changeData.reviewersByEmail().byState(ReviewerStateInternal.REVIEWER).stream()
- .forEach(address -> email.addByEmail(RecipientType.CC, address));
- } catch (StorageException e) {
- throw new EmailException("Failed to add unregistered CCs " + change.getChangeId(), e);
- }
- }
if (email.useHtml()) {
email.appendHtml(email.soyHtmlTemplate("ChangeHeaderHtml"));
diff --git a/java/com/google/gerrit/server/mail/send/CommentChangeEmailDecoratorImpl.java b/java/com/google/gerrit/server/mail/send/CommentChangeEmailDecoratorImpl.java
index c54c488c2b..b5f80144c2 100644
--- a/java/com/google/gerrit/server/mail/send/CommentChangeEmailDecoratorImpl.java
+++ b/java/com/google/gerrit/server/mail/send/CommentChangeEmailDecoratorImpl.java
@@ -397,7 +397,7 @@ public class CommentChangeEmailDecoratorImpl implements CommentChangeEmailDecora
commentData.put("lines", getLinesOfComment(comment, group.fileData));
}
commentData.put("message", comment.message.trim());
- List<CommentFormatter.Block> blocks = CommentFormatter.parse(comment.message);
+ ImmutableList<CommentFormatter.Block> blocks = CommentFormatter.parse(comment.message);
commentData.put("messageBlocks", commentBlocksToSoyData(blocks));
// Set the prefix.
diff --git a/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java b/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java
index b32c43ac3c..29b914ff6f 100644
--- a/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java
+++ b/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java
@@ -1,58 +1,26 @@
-// Copyright (C) 2020 The Android Open Source Project
-//
-// 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.
-
package com.google.gerrit.server.mail.send;
-import static com.google.common.base.Preconditions.checkState;
-
import com.google.auto.value.AutoValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.mail.MailMessage;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.update.RepoView;
-import com.google.inject.Inject;
-import java.io.IOException;
import java.time.Instant;
-import java.util.Optional;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-
-/** A generator class that creates a {@link MessageId} */
-public class MessageIdGenerator {
- private final GitRepositoryManager repositoryManager;
- private final AllUsersName allUsersName;
-
- @Inject
- public MessageIdGenerator(GitRepositoryManager repositoryManager, AllUsersName allUsersName) {
- this.repositoryManager = repositoryManager;
- this.allUsersName = allUsersName;
- }
+public interface MessageIdGenerator {
/**
* A unique id used which is a part of the header of all emails sent through by Gerrit. All of the
* emails are sent via {@link OutgoingEmail#send()}.
*/
@AutoValue
- public abstract static class MessageId {
+ abstract class MessageId {
public abstract String id();
+
+ public static MessageId create(String id) {
+ return new AutoValue_MessageIdGenerator_MessageId(id);
+ }
}
/**
@@ -60,43 +28,19 @@ public class MessageIdGenerator {
*
* @return MessageId that depends on the patchset.
*/
- public MessageId fromChangeUpdate(RepoView repoView, PatchSet.Id patchsetId) {
- return fromChangeUpdateAndReason(repoView, patchsetId, null);
- }
+ MessageId fromChangeUpdate(RepoView repoView, PatchSet.Id patchsetId);
- public MessageId fromChangeUpdateAndReason(
- RepoView repoView, PatchSet.Id patchsetId, @Nullable String reason) {
- String suffix = (reason != null) ? ("-" + reason) : "";
- String metaRef = patchsetId.changeId().toRefPrefix() + "meta";
- Optional<ObjectId> metaSha1;
- try {
- metaSha1 = repoView.getRef(metaRef);
- } catch (IOException ex) {
- throw new StorageException("unable to extract info for Message-Id", ex);
- }
- return metaSha1
- .map(optional -> new AutoValue_MessageIdGenerator_MessageId(optional.getName() + suffix))
- .orElseThrow(() -> new IllegalStateException(metaRef + " doesn't exist"));
- }
+ MessageId fromChangeUpdateAndReason(
+ RepoView repoView, PatchSet.Id patchsetId, @Nullable String reason);
- public MessageId fromChangeUpdate(Project.NameKey project, PatchSet.Id patchsetId) {
- String metaRef = patchsetId.changeId().toRefPrefix() + "meta";
- Ref ref = getRef(metaRef, project);
- checkState(ref != null, metaRef + " must exist");
- return new AutoValue_MessageIdGenerator_MessageId(ref.getObjectId().getName());
- }
+ MessageId fromChangeUpdate(Project.NameKey project, PatchSet.Id patchsetId);
/**
* Create a {@link MessageId} as a result of an account update
*
* @return {@link MessageId} that depends on the account id.
*/
- public MessageId fromAccountUpdate(Account.Id accountId) {
- String userRef = RefNames.refsUsers(accountId);
- Ref ref = getRef(userRef, allUsersName);
- checkState(ref != null, userRef + " must exist");
- return new AutoValue_MessageIdGenerator_MessageId(ref.getObjectId().getName());
- }
+ MessageId fromAccountUpdate(Account.Id accountId);
/**
* Create a {@link MessageId} from a mail message.
@@ -104,9 +48,7 @@ public class MessageIdGenerator {
* @param mailMessage The message that was sent but was rejected.
* @return MessageId that depends on the MailMessage that was rejected.
*/
- public MessageId fromMailMessage(MailMessage mailMessage) {
- return new AutoValue_MessageIdGenerator_MessageId(mailMessage.id() + "-REJECTION");
- }
+ MessageId fromMailMessage(MailMessage mailMessage);
/**
* Create a {@link MessageId} from a reason, Account.Id, and timestamp.
@@ -114,17 +56,5 @@ public class MessageIdGenerator {
* @param reason for performing this account update
* @return MessageId that depends on the reason, accountId, and timestamp.
*/
- public MessageId fromReasonAccountIdAndTimestamp(
- String reason, Account.Id accountId, Instant timestamp) {
- return new AutoValue_MessageIdGenerator_MessageId(
- reason + "-" + accountId.toString() + "-" + timestamp.toString());
- }
-
- private Ref getRef(String userRef, Project.NameKey project) {
- try (Repository repository = repositoryManager.openRepository(project)) {
- return repository.getRefDatabase().findRef(userRef);
- } catch (IOException ex) {
- throw new StorageException("unable to extract info for Message-Id", ex);
- }
- }
+ MessageId fromReasonAccountIdAndTimestamp(String reason, Account.Id accountId, Instant timestamp);
}
diff --git a/java/com/google/gerrit/server/mail/send/MessageIdGeneratorImpl.java b/java/com/google/gerrit/server/mail/send/MessageIdGeneratorImpl.java
new file mode 100644
index 0000000000..292d042696
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/send/MessageIdGeneratorImpl.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.mail.send;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.mail.MailMessage;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.update.RepoView;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+/** A generator class that creates a {@link MessageId} */
+public class MessageIdGeneratorImpl implements MessageIdGenerator {
+ private final GitRepositoryManager repositoryManager;
+ private final AllUsersName allUsersName;
+
+ @Inject
+ public MessageIdGeneratorImpl(GitRepositoryManager repositoryManager, AllUsersName allUsersName) {
+ this.repositoryManager = repositoryManager;
+ this.allUsersName = allUsersName;
+ }
+
+ @Override
+ public MessageId fromChangeUpdate(RepoView repoView, PatchSet.Id patchsetId) {
+ return fromChangeUpdateAndReason(repoView, patchsetId, null);
+ }
+
+ @Override
+ public MessageId fromChangeUpdateAndReason(
+ RepoView repoView, PatchSet.Id patchsetId, @Nullable String reason) {
+ String suffix = (reason != null) ? ("-" + reason) : "";
+ String metaRef = patchsetId.changeId().toRefPrefix() + "meta";
+ Optional<ObjectId> metaSha1;
+ try {
+ metaSha1 = repoView.getRef(metaRef);
+ } catch (IOException ex) {
+ throw new StorageException("unable to extract info for Message-Id", ex);
+ }
+ return metaSha1
+ .map(optional -> MessageId.create(optional.getName() + suffix))
+ .orElseThrow(() -> new IllegalStateException(metaRef + " doesn't exist"));
+ }
+
+ @Override
+ public MessageId fromChangeUpdate(Project.NameKey project, PatchSet.Id patchsetId) {
+ String metaRef = patchsetId.changeId().toRefPrefix() + "meta";
+ Ref ref = getRef(metaRef, project);
+ checkState(ref != null, metaRef + " must exist");
+ return MessageId.create(ref.getObjectId().getName());
+ }
+
+ @Override
+ public MessageId fromAccountUpdate(Account.Id accountId) {
+ String userRef = RefNames.refsUsers(accountId);
+ Ref ref = getRef(userRef, allUsersName);
+ checkState(ref != null, userRef + " must exist");
+ return MessageId.create(ref.getObjectId().getName());
+ }
+
+ @Override
+ public MessageId fromMailMessage(MailMessage mailMessage) {
+ return MessageId.create(mailMessage.id() + "-REJECTION");
+ }
+
+ @Override
+ public MessageId fromReasonAccountIdAndTimestamp(
+ String reason, Account.Id accountId, Instant timestamp) {
+ return MessageId.create(reason + "-" + accountId.toString() + "-" + timestamp.toString());
+ }
+
+ private Ref getRef(String userRef, Project.NameKey project) {
+ try (Repository repository = repositoryManager.openRepository(project)) {
+ return repository.getRefDatabase().findRef(userRef);
+ } catch (IOException ex) {
+ throw new StorageException("unable to extract info for Message-Id", ex);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 4dda7f0004..a6c89dc194 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -184,16 +184,18 @@ public final class OutgoingEmail {
/** Format and enqueue the message for delivery. */
public void send() throws EmailException {
try {
- args.retryHelper
- .action(
- ActionType.SEND_EMAIL,
- "sendEmail",
- () -> {
- sendImpl();
- return null;
- })
- .retryWithTrace(Exception.class::isInstance)
- .call();
+ @SuppressWarnings("unused")
+ var unused =
+ args.retryHelper
+ .action(
+ ActionType.SEND_EMAIL,
+ "sendEmail",
+ () -> {
+ sendImpl();
+ return null;
+ })
+ .retryWithTrace(Exception.class::isInstance)
+ .call();
} catch (Exception e) {
Throwables.throwIfUnchecked(e);
Throwables.throwIfInstanceOf(e, EmailException.class);
diff --git a/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
index 34443f417c..d71033a25d 100644
--- a/java/com/google/gerrit/server/mail/send/ProjectWatch.java
+++ b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -18,6 +18,7 @@ import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Address;
@@ -215,6 +216,7 @@ public class ProjectWatch {
}
}
+ @CanIgnoreReturnValue
private boolean add(
Watchers matching,
Account.Id accountId,
diff --git a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
index 17a59a4a0c..59d1b9b38b 100644
--- a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
@@ -142,7 +142,7 @@ public class SmtpEmailSender implements EmailSender {
@Override
public boolean canEmail(String address) {
if (!isEnabled()) {
- logger.atWarning().log("Not emailing %s (email is disabled)", address);
+ logger.atFine().log("Not emailing %s (email is disabled)", address);
return false;
}
@@ -163,7 +163,7 @@ public class SmtpEmailSender implements EmailSender {
if (denyrcpt.contains(address)
|| denyrcpt.contains(domain)
|| denyrcpt.contains("@" + domain)) {
- logger.atInfo().log("Not emailing %s (prohibited by sendemail.denyrcpt)", address);
+ logger.atFine().log("Not emailing %s (prohibited by sendemail.denyrcpt)", address);
return true;
}
@@ -182,7 +182,7 @@ public class SmtpEmailSender implements EmailSender {
return true;
}
- logger.atWarning().log("Not emailing %s (prohibited by sendemail.allowrcpt)", address);
+ logger.atFine().log("Not emailing %s (prohibited by sendemail.allowrcpt)", address);
return false;
}
@@ -258,7 +258,8 @@ public class SmtpEmailSender implements EmailSender {
"Server " + smtpHost + " rejected message body: " + client.getReplyString());
}
- client.logout();
+ @SuppressWarnings("unused")
+ var unused = client.logout();
if (rejected.length() > 0) {
throw new EmailException(rejected.toString());
}
diff --git a/java/com/google/gerrit/server/mail/send/package-info.java b/java/com/google/gerrit/server/mail/send/package-info.java
new file mode 100644
index 0000000000..8b099cb9d8
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/send/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.mail.send;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/mime/package-info.java b/java/com/google/gerrit/server/mime/package-info.java
new file mode 100644
index 0000000000..e024c4c66d
--- /dev/null
+++ b/java/com/google/gerrit/server/mime/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.mime;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index 5ffb5fb576..8bd18d7105 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -18,6 +18,7 @@ import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.Change;
@@ -142,6 +143,7 @@ public abstract class AbstractChangeNotes<T> {
return revision;
}
+ @CanIgnoreReturnValue
public T load() {
try (Repository repo = args.repoManager.openRepository(getProjectName())) {
load(repo);
@@ -151,6 +153,7 @@ public abstract class AbstractChangeNotes<T> {
}
}
+ @CanIgnoreReturnValue
public T load(Repository repo) {
if (loaded) {
return self();
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index 708d59fb01..0a82b4c1bc 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -24,6 +24,7 @@ import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.CommentVerifier;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.InternalUser;
@@ -55,7 +56,7 @@ public abstract class AbstractChangeUpdate {
private ObjectId result;
boolean rootOnly;
- AbstractChangeUpdate(
+ protected AbstractChangeUpdate(
ChangeNotes notes,
CurrentUser user,
PersonIdent serverIdent,
@@ -65,11 +66,11 @@ public abstract class AbstractChangeUpdate {
this.serverIdent = new PersonIdent(serverIdent, when);
this.notes = notes;
this.change = notes.getChange();
+ this.when = when;
this.accountId = accountId(user);
Account.Id realAccountId = accountId(user.getRealUser());
this.realAccountId = realAccountId != null ? realAccountId : accountId;
this.authorIdent = ident(noteUtil, serverIdent, user, when);
- this.when = when;
}
AbstractChangeUpdate(
@@ -236,9 +237,14 @@ public abstract class AbstractChangeUpdate {
setParentCommit(cb, curr);
if (cb.getTreeId() == null) {
if (curr.equals(z)) {
- cb.setTreeId(emptyTree(ins)); // No parent, assume empty tree.
+ ObjectId emptyTreeId = emptyTree(ins);
+ logger.atFine().log("setting empty tree %s for new change meta commit", emptyTreeId.name());
+ cb.setTreeId(emptyTreeId); // No parent, assume empty tree.
} else {
RevCommit p = rw.parseCommit(curr);
+ logger.atFine().log(
+ "setting tree %s of previous commit %s for new change meta commit",
+ p.getTree().name(), p.name());
cb.setTreeId(p.getTree()); // Copy tree from parent.
}
}
@@ -272,23 +278,12 @@ public abstract class AbstractChangeUpdate {
}
private static ObjectId emptyTree(ObjectInserter ins) throws IOException {
- return ins.insert(Constants.OBJ_TREE, new byte[] {});
+ ObjectId treeId = ins.insert(Constants.OBJ_TREE, new byte[] {});
+ logger.atFine().log("inserted empty tree %s (inserter: %s)", treeId.name(), ins);
+ return treeId;
}
- void verifyComment(Comment c) {
- checkArgument(c.getCommitId() != null, "commit ID required for comment: %s", c);
- checkArgument(
- c.author.getId().equals(getAccountId()),
- "The author for the following comment does not match the author of this %s (%s): %s",
- getClass().getSimpleName(),
- getAccountId(),
- c);
- checkArgument(
- c.getRealAuthor().getId().equals(realAccountId),
- "The real author for the following comment does not match the real"
- + " author of this %s (%s): %s",
- getClass().getSimpleName(),
- realAccountId,
- c);
+ protected void verifyComment(Comment c) {
+ CommentVerifier.verify(c, accountId, realAccountId, authorIdent);
}
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeDraftNotesUpdate.java b/java/com/google/gerrit/server/notedb/ChangeDraftNotesUpdate.java
index b15cb503a1..4bb347abdc 100644
--- a/java/com/google/gerrit/server/notedb/ChangeDraftNotesUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeDraftNotesUpdate.java
@@ -16,9 +16,12 @@ package com.google.gerrit.server.notedb;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.logging.TraceContext.newTimer;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ListMultimap;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
@@ -28,9 +31,18 @@ import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.server.ChangeDraftUpdate;
+import com.google.gerrit.server.ChangeDraftUpdateExecutor;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.query.change.ChangeNumberVirtualIdAlgorithm;
+import com.google.gerrit.server.update.BatchUpdateListener;
+import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
@@ -42,12 +54,16 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.ReceiveCommand;
/**
* A single delta to apply atomically to a change.
@@ -95,7 +111,121 @@ public class ChangeDraftNotesUpdate extends AbstractChangeUpdate implements Chan
return new AutoValue_ChangeDraftNotesUpdate_Key(c.getCommitId(), c.key);
}
+ public static class Executor implements ChangeDraftUpdateExecutor, AutoCloseable {
+ public interface Factory extends ChangeDraftUpdateExecutor.Factory<Executor> {
+ @Override
+ Executor create(CurrentUser currentUser);
+ }
+
+ private final GitRepositoryManager repoManager;
+ private final AllUsersName allUsersName;
+ private final NoteDbUpdateExecutor noteDbUpdateExecutor;
+ private final CurrentUser currentUser;
+ private final AllUsersAsyncUpdate updateAllUsersAsync;
+ private OpenRepo allUsersRepo;
+ private boolean shouldAllowFastForward = false;
+
+ @Inject
+ Executor(
+ GitRepositoryManager repoManager,
+ AllUsersName allUsersName,
+ NoteDbUpdateExecutor noteDbUpdateExecutor,
+ AllUsersAsyncUpdate updateAllUsersAsync,
+ @Assisted CurrentUser currentUser) {
+ this.updateAllUsersAsync = updateAllUsersAsync;
+ this.repoManager = repoManager;
+ this.allUsersName = allUsersName;
+ this.noteDbUpdateExecutor = noteDbUpdateExecutor;
+ this.currentUser = currentUser;
+ }
+
+ @Override
+ public void queueAllDraftUpdates(ListMultimap<String, ChangeDraftUpdate> updaters)
+ throws IOException {
+ ListMultimap<String, ChangeDraftNotesUpdate> noteDbUpdaters =
+ filterTypedUpdates(updaters, ChangeDraftNotesUpdate.class);
+ if (canRunAsync(noteDbUpdaters.values())) {
+ updateAllUsersAsync.setDraftUpdates(noteDbUpdaters);
+ } else {
+ initAllUsersRepoIfNull();
+ shouldAllowFastForward = true;
+ allUsersRepo.addUpdatesNoLimits(noteDbUpdaters);
+ }
+ }
+
+ @Override
+ public void queueDeletionForChangeDrafts(Change.Id id) throws IOException {
+ initAllUsersRepoIfNull();
+ // Just scan repo for ref names, but get "old" values from cmds.
+ for (Ref r :
+ allUsersRepo
+ .repo
+ .getRefDatabase()
+ .getRefsByPrefix(RefNames.refsDraftCommentsPrefix(id))) {
+ Optional<ObjectId> old = allUsersRepo.cmds.get(r.getName());
+ old.ifPresent(
+ objectId ->
+ allUsersRepo.cmds.add(
+ new ReceiveCommand(objectId, ObjectId.zeroId(), r.getName())));
+ }
+ }
+
+ /**
+ * Note this method does not fire {@link BatchUpdateListener#beforeUpdateRefs} events. However,
+ * since the {@link BatchRefUpdate} object is returned, {@link
+ * BatchUpdateListener#afterUpdateRefs} can be fired by the caller.
+ */
+ @Override
+ public Optional<BatchRefUpdate> executeAllSyncUpdates(
+ boolean dryRun, @Nullable PersonIdent refLogIdent, @Nullable String refLogMessage)
+ throws IOException {
+ if (allUsersRepo == null) {
+ return Optional.empty();
+ }
+ try (TraceContext.TraceTimer ignored =
+ newTimer("ChangeDraftNotesUpdate#Executor#updateAllUsersSync", Metadata.empty())) {
+ return noteDbUpdateExecutor.execute(
+ allUsersRepo,
+ dryRun,
+ shouldAllowFastForward,
+ /* batchUpdateListeners= */ ImmutableList.of(),
+ /* pushCert= */ null,
+ refLogIdent,
+ refLogMessage);
+ }
+ }
+
+ @Override
+ public void executeAllAsyncUpdates(
+ @Nullable PersonIdent refLogIdent,
+ @Nullable String refLogMessage,
+ @Nullable PushCertificate pushCert) {
+ updateAllUsersAsync.execute(refLogIdent, refLogMessage, pushCert, currentUser);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return (allUsersRepo == null || allUsersRepo.cmds.isEmpty()) && updateAllUsersAsync.isEmpty();
+ }
+
+ @Override
+ public void close() throws Exception {
+ if (allUsersRepo != null) {
+ OpenRepo r = allUsersRepo;
+ allUsersRepo = null;
+ r.close();
+ }
+ }
+
+ private void initAllUsersRepoIfNull() throws IOException {
+ if (allUsersRepo == null) {
+ allUsersRepo = OpenRepo.open(repoManager, allUsersName);
+ }
+ }
+ }
+
private final AllUsersName draftsProject;
+ private final ExperimentFeatures experimentFeatures;
private List<HumanComment> put = new ArrayList<>();
private Map<Key, DeleteReason> delete = new HashMap<>();
@@ -106,6 +236,7 @@ public class ChangeDraftNotesUpdate extends AbstractChangeUpdate implements Chan
@GerritPersonIdent PersonIdent serverIdent,
AllUsersName allUsers,
ChangeNoteUtil noteUtil,
+ ExperimentFeatures experimentFeatures,
@Nullable ChangeNumberVirtualIdAlgorithm virtualIdFunc,
@Assisted ChangeNotes notes,
@Assisted("effective") Account.Id accountId,
@@ -114,6 +245,7 @@ public class ChangeDraftNotesUpdate extends AbstractChangeUpdate implements Chan
@Assisted Instant when) {
super(noteUtil, serverIdent, notes, null, accountId, realAccountId, authorIdent, when);
this.draftsProject = allUsers;
+ this.experimentFeatures = experimentFeatures;
this.virtualIdFunc = virtualIdFunc;
}
@@ -122,6 +254,7 @@ public class ChangeDraftNotesUpdate extends AbstractChangeUpdate implements Chan
@GerritPersonIdent PersonIdent serverIdent,
AllUsersName allUsers,
ChangeNoteUtil noteUtil,
+ ExperimentFeatures experimentFeatures,
@Nullable ChangeNumberVirtualIdAlgorithm virtualIdFunc,
@Assisted Change change,
@Assisted("effective") Account.Id accountId,
@@ -130,6 +263,7 @@ public class ChangeDraftNotesUpdate extends AbstractChangeUpdate implements Chan
@Assisted Instant when) {
super(noteUtil, serverIdent, null, change, accountId, realAccountId, authorIdent, when);
this.draftsProject = allUsers;
+ this.experimentFeatures = experimentFeatures;
this.virtualIdFunc = virtualIdFunc;
}
@@ -164,6 +298,14 @@ public class ChangeDraftNotesUpdate extends AbstractChangeUpdate implements Chan
});
}
+ /**
+ * Returns whether all the updates in this instance can run asynchronously.
+ *
+ * <p>An update can run asynchronously only if it contains nothing but {@code PUBLISHED} or {@code
+ * FIXED} draft deletions. User-initiated inversions/deletions must run synchronously in order to
+ * return status.
+ */
+ @Override
public boolean canRunAsync() {
return put.isEmpty()
&& delete.values().stream()
@@ -184,6 +326,7 @@ public class ChangeDraftNotesUpdate extends AbstractChangeUpdate implements Chan
authorIdent,
draftsProject,
noteUtil,
+ experimentFeatures,
virtualIdFunc,
new Change(getChange()),
accountId,
@@ -202,6 +345,10 @@ public class ChangeDraftNotesUpdate extends AbstractChangeUpdate implements Chan
RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
for (HumanComment c : put) {
+ if (!experimentFeatures.isFeatureEnabled(
+ ExperimentFeaturesConstants.ALLOW_FIX_SUGGESTIONS_IN_COMMENTS)) {
+ checkState(c.fixSuggestions == null, "feature flag prohibits setting fixSuggestions");
+ }
if (!delete.keySet().contains(key(c))) {
cache.get(c.getCommitId()).putComment(c);
}
@@ -295,6 +442,11 @@ public class ChangeDraftNotesUpdate extends AbstractChangeUpdate implements Chan
}
@Override
+ public String getStorageKey() {
+ return getRefName();
+ }
+
+ @Override
protected void setParentCommit(CommitBuilder cb, ObjectId parentCommitId) {
cb.setParentIds(); // Draft updates should not keep history of parent commits
}
@@ -304,17 +456,6 @@ public class ChangeDraftNotesUpdate extends AbstractChangeUpdate implements Chan
return delete.isEmpty() && put.isEmpty();
}
- public static Optional<ChangeDraftNotesUpdate> asChangeDraftNotesUpdate(
- @Nullable ChangeDraftUpdate obj) {
- if (obj == null) {
- return Optional.empty();
- }
- if (obj instanceof ChangeDraftNotesUpdate) {
- return Optional.of((ChangeDraftNotesUpdate) obj);
- }
- return Optional.empty();
- }
-
private Change.Id getVirtualId() {
Change change = getChange();
return virtualIdFunc == null
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index 881cd963a8..8df29033e0 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.notedb;
import com.google.auto.value.AutoValue;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.json.OutputFormat;
@@ -52,6 +53,7 @@ public class ChangeNoteUtil {
*
* @return The passed in {@link StringBuilder} instance to which the identifier has been appended.
*/
+ @CanIgnoreReturnValue
StringBuilder appendAccountIdIdentString(StringBuilder stringBuilder, Account.Id accountId) {
return stringBuilder
.append(getAccountIdAsUsername(accountId))
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index cee98d154b..a7da7adb86 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -219,6 +219,8 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
* requires using the Change index and should only be used when {@link
* com.google.gerrit.entities.Project.NameKey} and the numeric change ID are not available.
*/
+ @UsedAt(UsedAt.Project.PLUGINS_ALL)
+ @Deprecated(since = "3.10", forRemoval = true)
public List<ChangeNotes> createUsingIndexLookup(Collection<Change.Id> changeIds) {
List<ChangeNotes> notes = new ArrayList<>();
for (Change.Id changeId : changeIds) {
@@ -553,11 +555,12 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
return getDraftComments(author, null, null);
}
- public ImmutableList<HumanComment> getDraftComments(Account.Id author, Ref ref) {
+ public ImmutableList<HumanComment> getDraftComments(Account.Id author, @Nullable Ref ref) {
return getDraftComments(author, null, ref);
}
- public ImmutableList<HumanComment> getDraftComments(Account.Id author, Change.Id virtualId) {
+ public ImmutableList<HumanComment> getDraftComments(
+ Account.Id author, @Nullable Change.Id virtualId) {
return getDraftComments(author, virtualId, null);
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
index 0566316752..08490a3cf5 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -62,7 +62,7 @@ public class ChangeNotesCache {
.weigher(Weigher.class)
.maximumWeight(10 << 20)
.diskLimit(-1)
- .version(8)
+ .version(11)
.keySerializer(Key.Serializer.INSTANCE)
.valueSerializer(ChangeNotesState.Serializer.INSTANCE);
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index ea26dbc780..011c5e8855 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -49,6 +49,7 @@ import com.google.common.base.Splitter;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableTable;
@@ -393,7 +394,8 @@ class ChangeNotesParser {
return copiedApprovals;
}
List<PatchSetApproval> approvalsOnLatestPs = allApprovals.get(latestPs);
- ListMultimap<Account.Id, PatchSetApproval> approvalsByUser = getApprovalsByUser(allApprovals);
+ ImmutableListMultimap<Account.Id, PatchSetApproval> approvalsByUser =
+ getApprovalsByUser(allApprovals);
List<SubmitRecord.Label> submitRecordLabels =
submitRecords.stream()
.filter(r -> r.labels != null)
@@ -431,7 +433,7 @@ class ChangeNotesParser {
return allApprovals.values().stream().anyMatch(approval -> approval.copied());
}
- private ListMultimap<Account.Id, PatchSetApproval> getApprovalsByUser(
+ private ImmutableListMultimap<Account.Id, PatchSetApproval> getApprovalsByUser(
ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals) {
return allApprovals.values().stream()
.collect(
@@ -442,10 +444,21 @@ class ChangeNotesParser {
private List<ReviewerStatusUpdate> buildReviewerUpdates() {
List<ReviewerStatusUpdate> result = new ArrayList<>();
HashMap<Account.Id, ReviewerStateInternal> lastState = new HashMap<>();
+ HashMap<Address, ReviewerStateInternal> lastStateReviewerByEmail = new HashMap<>();
for (ReviewerStatusUpdate u : Lists.reverse(reviewerUpdates)) {
- if (!Objects.equals(ownerId, u.reviewer()) && lastState.get(u.reviewer()) != u.state()) {
- result.add(u);
- lastState.put(u.reviewer(), u.state());
+ if (u.reviewer().isPresent()) {
+ if (!Objects.equals(ownerId, u.reviewer().get())
+ && lastState.get(u.reviewer().get()) != u.state()) {
+ result.add(u);
+ lastState.put(u.reviewer().get(), u.state());
+ }
+ }
+
+ if (u.reviewerByEmail().isPresent()) {
+ if (lastStateReviewerByEmail.get(u.reviewerByEmail().get()) != u.state()) {
+ result.add(u);
+ lastStateReviewerByEmail.put(u.reviewerByEmail().get(), u.state());
+ }
}
}
return result;
@@ -940,7 +953,7 @@ class ChangeNotesParser {
revisionNoteMap =
RevisionNoteMap.parse(
changeNoteJson, reader, NoteMap.read(reader, tipCommit), HumanComment.Status.PUBLISHED);
- Map<ObjectId, ChangeRevisionNote> rns = revisionNoteMap.revisionNotes;
+ ImmutableMap<ObjectId, ChangeRevisionNote> rns = revisionNoteMap.revisionNotes;
for (Map.Entry<ObjectId, ChangeRevisionNote> e : rns.entrySet()) {
for (HumanComment c : e.getValue().getEntities()) {
@@ -1233,7 +1246,8 @@ class ChangeNotesParser {
throw invalidFooter(state.getFooterKey(), line);
}
Account.Id accountId = parseIdent(ident);
- ReviewerStatusUpdate update = ReviewerStatusUpdate.create(ts, ownerId, accountId, state);
+ ReviewerStatusUpdate update =
+ ReviewerStatusUpdate.createForReviewer(ts, ownerId, accountId, state);
reviewerUpdates.add(update);
if (update.state() == ReviewerStateInternal.REMOVED) {
removedReviewers.add(accountId);
@@ -1254,6 +1268,7 @@ class ChangeNotesParser {
cie.initCause(e);
throw cie;
}
+ reviewerUpdates.add(ReviewerStatusUpdate.createForReviewerByEmail(ts, ownerId, adr, state));
if (!reviewersByEmail.containsRow(adr)) {
reviewersByEmail.put(adr, state, ts);
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 6b208f3fae..eb6c15a2aa 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -609,12 +609,24 @@ public abstract class ChangeNotesState {
}
private static ReviewerStatusUpdateProto toReviewerStatusUpdateProto(ReviewerStatusUpdate u) {
- return ReviewerStatusUpdateProto.newBuilder()
- .setTimestampMillis(u.date().toEpochMilli())
- .setUpdatedBy(u.updatedBy().get())
- .setReviewer(u.reviewer().get())
- .setState(REVIEWER_STATE_CONVERTER.reverse().convert(u.state()))
- .build();
+ ReviewerStatusUpdateProto.Builder protoBuilder =
+ ReviewerStatusUpdateProto.newBuilder()
+ .setTimestampMillis(u.date().toEpochMilli())
+ .setUpdatedBy(u.updatedBy().get())
+ .setState(REVIEWER_STATE_CONVERTER.reverse().convert(u.state()));
+ u.reviewer()
+ .ifPresent(
+ accountId -> {
+ protoBuilder.setHasReviewer(true);
+ protoBuilder.setReviewer(accountId.get());
+ });
+ u.reviewerByEmail()
+ .ifPresent(
+ address -> {
+ protoBuilder.setHasReviewerByEmail(true);
+ protoBuilder.setReviewerByEmail(address.toHeaderString());
+ });
+ return protoBuilder.build();
}
private static AttentionSetUpdateProto toAttentionSetUpdateProto(
@@ -746,12 +758,26 @@ public abstract class ChangeNotesState {
List<ReviewerStatusUpdateProto> protos) {
ImmutableList.Builder<ReviewerStatusUpdate> b = ImmutableList.builder();
for (ReviewerStatusUpdateProto proto : protos) {
- b.add(
- ReviewerStatusUpdate.create(
- Instant.ofEpochMilli(proto.getTimestampMillis()),
- Account.id(proto.getUpdatedBy()),
- Account.id(proto.getReviewer()),
- REVIEWER_STATE_CONVERTER.convert(proto.getState())));
+ if (proto.getHasReviewerByEmail()) {
+ b.add(
+ ReviewerStatusUpdate.createForReviewerByEmail(
+ Instant.ofEpochMilli(proto.getTimestampMillis()),
+ Account.id(proto.getUpdatedBy()),
+ Address.parse(proto.getReviewerByEmail()),
+ REVIEWER_STATE_CONVERTER.convert(proto.getState())));
+ } else {
+ // If the "has_reviewer_by_email" field is not set, then either the "has_reviewer" field
+ // is true and the "reviewer" field is populated, or the proto was created before the
+ // "has_reviewer", "has_reviewer_by_email" and "reviewer_by_email" fields have been added
+ // and the "reviewer" field is always populated. This means by not checking that
+ // "proto.getHasReviewer()" is true here we allow the new code to read old protos.
+ b.add(
+ ReviewerStatusUpdate.createForReviewer(
+ Instant.ofEpochMilli(proto.getTimestampMillis()),
+ Account.id(proto.getUpdatedBy()),
+ Account.id(proto.getReviewer()),
+ REVIEWER_STATE_CONVERTER.convert(proto.getState())));
+ }
}
return b.build();
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 17f41d41df..c97065b037 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.notedb;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.gerrit.entities.RefNames.changeMetaRef;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ATTENTION;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_BRANCH;
@@ -52,12 +53,15 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Iterables;
import com.google.common.collect.Table;
import com.google.common.collect.Table.Cell;
import com.google.common.collect.TreeBasedTable;
+import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Address;
@@ -81,6 +85,8 @@ import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.ServiceUserClassifier;
import com.google.gerrit.server.approval.PatchSetApprovalUuidGenerator;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
import com.google.gerrit.server.git.validators.TopicValidator;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.update.context.RefUpdateContext;
@@ -135,6 +141,8 @@ import org.eclipse.jgit.revwalk.RevWalk;
* the attached {@link ChangeRevisionNote}.
*/
public class ChangeUpdate extends AbstractChangeUpdate {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
public interface Factory {
ChangeUpdate create(ChangeNotes notes, CurrentUser user, Instant when);
@@ -158,13 +166,14 @@ public class ChangeUpdate extends AbstractChangeUpdate {
private final Map<Account.Id, ReviewerStateInternal> reviewers = new LinkedHashMap<>();
private final Map<Address, ReviewerStateInternal> reviewersByEmail = new LinkedHashMap<>();
private final List<HumanComment> comments = new ArrayList<>();
+ private final ExperimentFeatures experimentFeatures;
private String commitSubject;
private String subject;
private String changeId;
private String branch;
private Change.Status status;
- private List<SubmitRecord> submitRecords;
+ private ImmutableList<SubmitRecord> submitRecords;
private String submissionId;
private String topic;
private String commit;
@@ -209,6 +218,7 @@ public class ChangeUpdate extends AbstractChangeUpdate {
ProjectCache projectCache,
ServiceUserClassifier serviceUserClassifier,
PatchSetApprovalUuidGenerator patchSetApprovalUuidGenerator,
+ ExperimentFeatures experimentFeatures,
@Assisted ChangeNotes notes,
@Assisted CurrentUser user,
@Assisted Instant when,
@@ -221,6 +231,7 @@ public class ChangeUpdate extends AbstractChangeUpdate {
deleteCommentRewriterFactory,
serviceUserClassifier,
patchSetApprovalUuidGenerator,
+ experimentFeatures,
notes,
user,
when,
@@ -246,6 +257,7 @@ public class ChangeUpdate extends AbstractChangeUpdate {
DeleteCommentRewriter.Factory deleteCommentRewriterFactory,
ServiceUserClassifier serviceUserClassifier,
PatchSetApprovalUuidGenerator patchSetApprovalUuidGenerator,
+ ExperimentFeatures experimentFeatures,
@Assisted ChangeNotes notes,
@Assisted CurrentUser user,
@Assisted Instant when,
@@ -258,10 +270,12 @@ public class ChangeUpdate extends AbstractChangeUpdate {
this.deleteCommentRewriterFactory = deleteCommentRewriterFactory;
this.serviceUserClassifier = serviceUserClassifier;
this.patchSetApprovalUuidGenerator = patchSetApprovalUuidGenerator;
+ this.experimentFeatures = experimentFeatures;
this.approvals = approvals(labelNameComparator);
this.user = user;
}
+ @CanIgnoreReturnValue
public ObjectId commit() throws IOException {
try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
try (NoteDbUpdateManager updateManager =
@@ -423,6 +437,7 @@ public class ChangeUpdate extends AbstractChangeUpdate {
}
@VisibleForTesting
+ @CanIgnoreReturnValue
ChangeDraftUpdate createDraftUpdateIfNull() {
if (draftUpdate == null) {
ChangeNotes notes = getNotes();
@@ -526,13 +541,9 @@ public class ChangeUpdate extends AbstractChangeUpdate {
plannedAttentionSetUpdates = new HashMap<>();
}
- Set<Account.Id> currentAccountUpdates =
- plannedAttentionSetUpdates.values().stream()
- .map(AttentionSetUpdate::account)
- .collect(Collectors.toSet());
- updates.stream()
- .filter(u -> !currentAccountUpdates.contains(u.account()))
- .forEach(u -> plannedAttentionSetUpdates.putIfAbsent(u.account(), u));
+ // Only add attention set updates for users for which no attention set update has been planned
+ // yet.
+ updates.stream().forEach(u -> plannedAttentionSetUpdates.putIfAbsent(u.account(), u));
}
public void addToPlannedAttentionSetUpdates(AttentionSetUpdate update) {
@@ -606,6 +617,10 @@ public class ChangeUpdate extends AbstractChangeUpdate {
RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
for (HumanComment c : comments) {
c.tag = tag;
+ if (!experimentFeatures.isFeatureEnabled(
+ ExperimentFeaturesConstants.ALLOW_FIX_SUGGESTIONS_IN_COMMENTS)) {
+ checkState(c.fixSuggestions == null, "feature flag prohibits setting fixSuggestions");
+ }
cache.get(c.getCommitId()).putComment(c);
}
if (submitRequirementResults != null) {
@@ -899,7 +914,10 @@ public class ChangeUpdate extends AbstractChangeUpdate {
try {
ObjectId treeId = storeRevisionNotes(rw, ins, curr);
if (treeId != null) {
+ logger.atFine().log("change meta tree ID: %s (inserter: %s)", treeId.name(), ins);
cb.setTreeId(treeId);
+ } else {
+ logger.atFine().log("no revision notes to write, hence no change meta tree was created");
}
} catch (ConfigInvalidException e) {
throw new StorageException(e);
@@ -997,7 +1015,7 @@ public class ChangeUpdate extends AbstractChangeUpdate {
}
Set<AttentionSetUpdate> updates = new HashSet<>();
- Set<Account.Id> currentReviewers =
+ ImmutableSet<Account.Id> currentReviewers =
getNotes().getReviewers().byState(ReviewerStateInternal.REVIEWER);
for (Map.Entry<Account.Id, ReviewerStateInternal> reviewer : reviewers.entrySet()) {
Account.Id reviewerId = reviewer.getKey();
@@ -1049,10 +1067,9 @@ public class ChangeUpdate extends AbstractChangeUpdate {
if (plannedAttentionSetUpdates == null) {
plannedAttentionSetUpdates = new HashMap<>();
}
- Set<Account.Id> currentUsersInAttentionSet =
+ ImmutableMap<Account.Id, String> reasonsForCurrentUsersInAttentionSet =
AttentionSetUtil.additionsOnly(getNotes().getAttentionSet()).stream()
- .map(AttentionSetUpdate::account)
- .collect(Collectors.toSet());
+ .collect(toImmutableMap(AttentionSetUpdate::account, AttentionSetUpdate::reason));
// Current reviewers/ccs are the reviewers/ccs before the update + the new reviewers/ccs - the
// deleted reviewers/ccs.
@@ -1075,13 +1092,17 @@ public class ChangeUpdate extends AbstractChangeUpdate {
for (AttentionSetUpdate attentionSetUpdate : plannedAttentionSetUpdates.values()) {
if (attentionSetUpdate.operation() == AttentionSetUpdate.Operation.ADD
- && currentUsersInAttentionSet.contains(attentionSetUpdate.account())) {
- // Skip users that are already in the attention set: no need to re-add them.
+ && reasonsForCurrentUsersInAttentionSet.get(attentionSetUpdate.account()) != null
+ && reasonsForCurrentUsersInAttentionSet
+ .get(attentionSetUpdate.account())
+ .equals(attentionSetUpdate.reason())) {
+ // Skip users that are already in the attention set with the same reason: no need to re-add
+ // them.
continue;
}
if (attentionSetUpdate.operation() == AttentionSetUpdate.Operation.REMOVE
- && !currentUsersInAttentionSet.contains(attentionSetUpdate.account())) {
+ && !reasonsForCurrentUsersInAttentionSet.containsKey(attentionSetUpdate.account())) {
// Skip users that are not in the attention set: no need to remove them.
continue;
}
@@ -1114,7 +1135,7 @@ public class ChangeUpdate extends AbstractChangeUpdate {
}
private void removeInactiveUsersFromAttentionSet(Set<Account.Id> currentReviewers) {
- Set<Account.Id> inActiveUsersInTheAttentionSet =
+ ImmutableSet<Account.Id> inActiveUsersInTheAttentionSet =
// get the current attention set.
getNotes().getAttentionSet().stream()
.filter(a -> a.operation().equals(Operation.ADD))
@@ -1242,6 +1263,7 @@ public class ChangeUpdate extends AbstractChangeUpdate {
this.workInProgress = workInProgress;
}
+ @CanIgnoreReturnValue
private static StringBuilder addFooter(StringBuilder sb, FooterKey footer) {
return sb.append(footer.getName()).append(": ");
}
diff --git a/java/com/google/gerrit/server/notedb/CommitRewriter.java b/java/com/google/gerrit/server/notedb/CommitRewriter.java
index 270fc3262a..ff5d85f7f6 100644
--- a/java/com/google/gerrit/server/notedb/CommitRewriter.java
+++ b/java/com/google/gerrit/server/notedb/CommitRewriter.java
@@ -32,6 +32,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.Account;
@@ -1074,6 +1075,7 @@ public class CommitRewriter {
return Optional.of(fixedCommitBuilder.toString());
}
+ @CanIgnoreReturnValue
private static StringBuilder addFooter(StringBuilder sb, String footer, String value) {
if (value == null) {
return sb;
@@ -1184,7 +1186,7 @@ public class CommitRewriter {
.collect(
ImmutableMap.toImmutableMap(
Map.Entry::getKey, e -> Optional.ofNullable(e.getValue()))));
- Map<Account.Id, AccountState> possibleReplacements = ImmutableMap.of();
+ ImmutableMap<Account.Id, AccountState> possibleReplacements = ImmutableMap.of();
if (accountInfo.email().isPresent()) {
possibleReplacements =
changeFixProgress.parsedAccounts.entrySet().stream()
diff --git a/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java b/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
index 7d00d2ca2f..4c7e268839 100644
--- a/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
+++ b/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
@@ -19,6 +19,7 @@ import static com.google.gerrit.entities.RefNames.REFS_DRAFT_COMMENTS;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
import static org.eclipse.jgit.lib.Constants.EMPTY_TREE_ID;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
@@ -256,7 +257,7 @@ public class DeleteZombieCommentsRefs extends DeleteZombieComments<Ref> {
private void deleteZombieDraftsBatch(Collection<Ref> refsBatch) throws IOException {
try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
- List<ReceiveCommand> deleteCommands =
+ ImmutableList<ReceiveCommand> deleteCommands =
refsBatch.stream()
.map(
zombieRef ->
diff --git a/java/com/google/gerrit/server/notedb/DraftCommentsNotesReader.java b/java/com/google/gerrit/server/notedb/DraftCommentsNotesReader.java
index 27c59f968f..ea3dd0a33b 100644
--- a/java/com/google/gerrit/server/notedb/DraftCommentsNotesReader.java
+++ b/java/com/google/gerrit/server/notedb/DraftCommentsNotesReader.java
@@ -26,6 +26,7 @@ import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.query.change.ChangeNumberVirtualIdAlgorithm;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
@@ -43,15 +44,18 @@ public class DraftCommentsNotesReader implements DraftCommentsReader {
private final DraftCommentNotes.Factory draftCommentNotesFactory;
private final GitRepositoryManager repoManager;
private final AllUsersName allUsers;
+ private final ChangeNumberVirtualIdAlgorithm virtualIdAlgorithm;
@Inject
DraftCommentsNotesReader(
DraftCommentNotes.Factory draftCommentNotesFactory,
GitRepositoryManager repoManager,
- AllUsersName allUsers) {
+ AllUsersName allUsers,
+ ChangeNumberVirtualIdAlgorithm virtualIdAlgorithm) {
this.draftCommentNotesFactory = draftCommentNotesFactory;
this.repoManager = repoManager;
this.allUsers = allUsers;
+ this.virtualIdAlgorithm = virtualIdAlgorithm;
}
@Override
@@ -64,7 +68,7 @@ public class DraftCommentsNotesReader implements DraftCommentsReader {
@Override
public List<HumanComment> getDraftsByChangeAndDraftAuthor(ChangeNotes notes, Account.Id author) {
- return sort(new ArrayList<>(notes.getDraftComments(author)));
+ return sort(new ArrayList<>(notes.getDraftComments(author, getVirtualId(notes))));
}
@Override
@@ -77,7 +81,7 @@ public class DraftCommentsNotesReader implements DraftCommentsReader {
public List<HumanComment> getDraftsByPatchSetAndDraftAuthor(
ChangeNotes notes, PatchSet.Id psId, Account.Id author) {
return sort(
- notes.load().getDraftComments(author).stream()
+ notes.load().getDraftComments(author, getVirtualId(notes)).stream()
.filter(c -> c.key.patchSetId == psId.get())
.collect(Collectors.toList()));
}
@@ -136,7 +140,7 @@ public class DraftCommentsNotesReader implements DraftCommentsReader {
private List<Ref> getDraftRefs(ChangeNotes notes) {
try (Repository repo = repoManager.openRepository(allUsers)) {
return repo.getRefDatabase()
- .getRefsByPrefix(RefNames.refsDraftCommentsPrefix(notes.getChangeId()));
+ .getRefsByPrefix(RefNames.refsDraftCommentsPrefix(getVirtualId(notes)));
} catch (IOException e) {
throw new StorageException(e);
}
@@ -145,4 +149,10 @@ public class DraftCommentsNotesReader implements DraftCommentsReader {
private List<HumanComment> sort(List<HumanComment> comments) {
return CommentsUtil.sort(comments);
}
+
+ private Change.Id getVirtualId(ChangeNotes notes) {
+ return virtualIdAlgorithm == null
+ ? notes.getChangeId()
+ : virtualIdAlgorithm.apply(notes.getServerId(), notes.getChangeId());
+ }
}
diff --git a/java/com/google/gerrit/server/notedb/NoteDbDraftCommentsModule.java b/java/com/google/gerrit/server/notedb/NoteDbDraftCommentsModule.java
new file mode 100644
index 0000000000..783fce02a7
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/NoteDbDraftCommentsModule.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.notedb;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.server.ChangeDraftUpdate;
+import com.google.gerrit.server.ChangeDraftUpdateExecutor;
+import com.google.gerrit.server.DraftCommentsReader;
+import com.google.inject.Singleton;
+
+public class NoteDbDraftCommentsModule extends FactoryModule {
+ @Override
+ public void configure() {
+ factory(ChangeDraftNotesUpdate.Factory.class);
+ factory(ChangeDraftNotesUpdate.Executor.Factory.class);
+ factory(DraftCommentNotes.Factory.class);
+
+ bind(DraftCommentsReader.class).to(DraftCommentsNotesReader.class).in(Singleton.class);
+ bind(ChangeDraftUpdate.ChangeDraftUpdateFactory.class).to(ChangeDraftNotesUpdate.Factory.class);
+ bind(ChangeDraftUpdateExecutor.AbstractFactory.class)
+ .to(ChangeDraftNotesUpdate.Executor.Factory.class)
+ .in(Singleton.class);
+ }
+}
diff --git a/java/com/google/gerrit/server/notedb/NoteDbModule.java b/java/com/google/gerrit/server/notedb/NoteDbModule.java
index 31428cdf69..cf3fad6a90 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbModule.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbModule.java
@@ -17,11 +17,6 @@ package com.google.gerrit.server.notedb;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.server.DraftCommentsReader;
-import com.google.gerrit.server.StarredChangesReader;
-import com.google.gerrit.server.StarredChangesUtil;
-import com.google.gerrit.server.StarredChangesWriter;
-import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Names;
@@ -42,17 +37,11 @@ public class NoteDbModule extends FactoryModule {
@Override
public void configure() {
- factory(ChangeDraftNotesUpdate.Factory.class);
factory(ChangeUpdate.Factory.class);
factory(DeleteCommentRewriter.Factory.class);
- factory(DraftCommentNotes.Factory.class);
factory(NoteDbUpdateManager.Factory.class);
factory(RobotCommentNotes.Factory.class);
factory(RobotCommentUpdate.Factory.class);
- bind(StarredChangesReader.class).to(StarredChangesUtilNoteDbImpl.class).in(Singleton.class);
- bind(StarredChangesWriter.class).to(StarredChangesUtilNoteDbImpl.class).in(Singleton.class);
- bind(StarredChangesUtil.class).to(StarredChangesUtilNoteDbImpl.class).in(Singleton.class);
- bind(DraftCommentsReader.class).to(DraftCommentsNotesReader.class).in(Singleton.class);
if (!useTestBindings) {
install(ChangeNotesCache.module());
diff --git a/java/com/google/gerrit/server/notedb/NoteDbStarredChangesModule.java b/java/com/google/gerrit/server/notedb/NoteDbStarredChangesModule.java
new file mode 100644
index 0000000000..733c3cfa32
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/NoteDbStarredChangesModule.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.notedb;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.server.StarredChangesReader;
+import com.google.gerrit.server.StarredChangesWriter;
+import com.google.inject.Singleton;
+
+public class NoteDbStarredChangesModule extends FactoryModule {
+ @Override
+ public void configure() {
+ bind(StarredChangesReader.class).to(StarredChangesUtilNoteDbImpl.class).in(Singleton.class);
+ bind(StarredChangesWriter.class).to(StarredChangesUtilNoteDbImpl.class).in(Singleton.class);
+ }
+}
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUpdateExecutor.java b/java/com/google/gerrit/server/notedb/NoteDbUpdateExecutor.java
new file mode 100644
index 0000000000..c04e1848b5
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/NoteDbUpdateExecutor.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.notedb;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.git.RefUpdateUtil;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.update.BatchUpdateListener;
+import com.google.gerrit.server.update.ChainedReceiveCommands;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.util.Optional;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/** Utility class for executing commands on a given repository. */
+class NoteDbUpdateExecutor {
+ private final Provider<PersonIdent> serverIdent;
+
+ @Inject
+ NoteDbUpdateExecutor(@GerritPersonIdent Provider<PersonIdent> serverIdent) {
+ this.serverIdent = serverIdent;
+ }
+
+ Optional<BatchRefUpdate> execute(
+ OpenRepo or,
+ boolean dryrun,
+ boolean maybeAllowNonFastForwards,
+ ImmutableList<BatchUpdateListener> batchUpdateListeners,
+ @Nullable PushCertificate pushCert,
+ @Nullable PersonIdent refLogIdent,
+ @Nullable String refLogMessage)
+ throws IOException {
+ if (or == null || or.cmds.isEmpty()) {
+ return Optional.empty();
+ }
+ if (!dryrun) {
+ or.flush();
+ } else {
+ // OpenRepo buffers objects separately; caller may assume that objects are available in the
+ // inserter it previously passed via setChangeRepo.
+ or.flushToFinalInserter();
+ }
+
+ BatchRefUpdate bru = or.repo.getRefDatabase().newBatchUpdate();
+ bru.setPushCertificate(pushCert);
+ if (refLogMessage != null) {
+ bru.setRefLogMessage(refLogMessage, false);
+ } else {
+ bru.setRefLogMessage(
+ firstNonNull(NoteDbUtil.guessRestApiHandler(), "Update NoteDb refs"), false);
+ }
+ bru.setRefLogIdent(refLogIdent != null ? refLogIdent : serverIdent.get());
+ bru.setAtomic(true);
+ or.cmds.addTo(bru);
+ bru.setAllowNonFastForwards(maybeAllowNonFastForwards || allowNonFastForwards(or.cmds));
+ for (BatchUpdateListener listener : batchUpdateListeners) {
+ bru = listener.beforeUpdateRefs(bru);
+ }
+
+ if (!dryrun) {
+ RefUpdateUtil.executeChecked(bru, or.rw);
+ }
+ return Optional.of(bru);
+ }
+
+ /**
+ * Allow non-fast-forwards if any of the receive commands is of type {@link
+ * org.eclipse.jgit.transport.ReceiveCommand.Type#UPDATE_NONFASTFORWARD} (for example due to a
+ * force push).
+ */
+ private boolean allowNonFastForwards(ChainedReceiveCommands receiveCommands) {
+ return receiveCommands.getCommands().values().stream()
+ .anyMatch(cmd -> cmd.getType().equals(ReceiveCommand.Type.UPDATE_NONFASTFORWARD));
+ }
+}
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 7b2148aaee..4fce425270 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -14,7 +14,6 @@
package com.google.gerrit.server.notedb;
-import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
@@ -26,6 +25,7 @@ import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.Change;
@@ -33,10 +33,10 @@ import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.ProjectChangeKey;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.git.RefUpdateUtil;
import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.server.ChangeDraftUpdate;
+import com.google.gerrit.server.ChangeDraftUpdateExecutor;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.cancellation.RequestStateContext;
import com.google.gerrit.server.cancellation.RequestStateContext.NonCancellableOperationContext;
import com.google.gerrit.server.config.AllUsersName;
@@ -47,7 +47,6 @@ import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.update.BatchUpdateListener;
import com.google.gerrit.server.update.ChainedReceiveCommands;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.util.Collection;
@@ -61,7 +60,6 @@ import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.PushCertificate;
@@ -85,7 +83,6 @@ public class NoteDbUpdateManager implements AutoCloseable {
NoteDbUpdateManager create(Project.NameKey projectName, CurrentUser currentUser);
}
- private final Provider<PersonIdent> serverIdent;
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
private final NoteDbMetrics metrics;
@@ -94,36 +91,37 @@ public class NoteDbUpdateManager implements AutoCloseable {
private final int maxPatchSets;
private final CurrentUser currentUser;
private final ListMultimap<String, ChangeUpdate> changeUpdates;
- private final ListMultimap<String, ChangeDraftNotesUpdate> draftUpdates;
+ private final ListMultimap<String, ChangeDraftUpdate> draftUpdates;
+ private final NoteDbUpdateExecutor noteDbUpdateExecutor;
+ private final ChangeDraftUpdateExecutor.AbstractFactory draftUpdatesExecutorFactory;
private final ListMultimap<String, RobotCommentUpdate> robotCommentUpdates;
private final ListMultimap<String, NoteDbRewriter> rewriters;
private final Set<Change.Id> changesToDelete;
private OpenRepo changeRepo;
- private OpenRepo allUsersRepo;
- private AllUsersAsyncUpdate updateAllUsersAsync;
private boolean executed;
private String refLogMessage;
private PersonIdent refLogIdent;
private PushCertificate pushCert;
private ImmutableList<BatchUpdateListener> batchUpdateListeners;
+ private ChangeDraftUpdateExecutor draftUpdatesExecutor;
@Inject
NoteDbUpdateManager(
@GerritServerConfig Config cfg,
- @GerritPersonIdent Provider<PersonIdent> serverIdent,
GitRepositoryManager repoManager,
AllUsersName allUsersName,
NoteDbMetrics metrics,
- AllUsersAsyncUpdate updateAllUsersAsync,
@Assisted Project.NameKey projectName,
+ NoteDbUpdateExecutor noteDbUpdateExecutor,
+ ChangeDraftUpdateExecutor.AbstractFactory draftUpdatesExecutorFactory,
@Assisted CurrentUser currentUser) {
- this.serverIdent = serverIdent;
this.repoManager = repoManager;
this.allUsersName = allUsersName;
this.metrics = metrics;
- this.updateAllUsersAsync = updateAllUsersAsync;
this.projectName = projectName;
+ this.noteDbUpdateExecutor = noteDbUpdateExecutor;
+ this.draftUpdatesExecutorFactory = draftUpdatesExecutorFactory;
maxUpdates = cfg.getInt("change", null, "maxUpdates", MAX_UPDATES_DEFAULT);
maxPatchSets = cfg.getInt("change", null, "maxPatchSets", MAX_PATCH_SETS_DEFAULT);
this.currentUser = currentUser;
@@ -137,21 +135,14 @@ public class NoteDbUpdateManager implements AutoCloseable {
@Override
public void close() {
- try {
- if (allUsersRepo != null) {
- OpenRepo r = allUsersRepo;
- allUsersRepo = null;
- r.close();
- }
- } finally {
- if (changeRepo != null) {
- OpenRepo r = changeRepo;
- changeRepo = null;
- r.close();
- }
+ if (changeRepo != null) {
+ OpenRepo r = changeRepo;
+ changeRepo = null;
+ r.close();
}
}
+ @CanIgnoreReturnValue
public NoteDbUpdateManager setChangeRepo(
Repository repo, RevWalk rw, @Nullable ObjectInserter ins, ChainedReceiveCommands cmds) {
checkState(changeRepo == null, "change repo already initialized");
@@ -159,11 +150,13 @@ public class NoteDbUpdateManager implements AutoCloseable {
return this;
}
+ @CanIgnoreReturnValue
public NoteDbUpdateManager setRefLogMessage(String message) {
this.refLogMessage = message;
return this;
}
+ @CanIgnoreReturnValue
public NoteDbUpdateManager setRefLogIdent(PersonIdent ident) {
this.refLogIdent = ident;
return this;
@@ -183,11 +176,13 @@ public class NoteDbUpdateManager implements AutoCloseable {
* @param pushCert push certificate; may be null.
* @return this
*/
+ @CanIgnoreReturnValue
public NoteDbUpdateManager setPushCertificate(PushCertificate pushCert) {
this.pushCert = pushCert;
return this;
}
+ @CanIgnoreReturnValue
public NoteDbUpdateManager setBatchUpdateListeners(
ImmutableList<BatchUpdateListener> batchUpdateListeners) {
checkNotNull(batchUpdateListeners);
@@ -205,12 +200,6 @@ public class NoteDbUpdateManager implements AutoCloseable {
}
}
- private void initAllUsersRepo() throws IOException {
- if (allUsersRepo == null) {
- allUsersRepo = OpenRepo.open(repoManager, allUsersName);
- }
- }
-
private boolean isEmpty() {
return changeUpdates.isEmpty()
&& draftUpdates.isEmpty()
@@ -218,8 +207,7 @@ public class NoteDbUpdateManager implements AutoCloseable {
&& rewriters.isEmpty()
&& changesToDelete.isEmpty()
&& !hasCommands(changeRepo)
- && !hasCommands(allUsersRepo)
- && updateAllUsersAsync.isEmpty();
+ && (draftUpdatesExecutor == null || draftUpdatesExecutor.isEmpty());
}
private static boolean hasCommands(@Nullable OpenRepo or) {
@@ -246,10 +234,9 @@ public class NoteDbUpdateManager implements AutoCloseable {
"cannot update & rewrite ref %s in one BatchUpdate",
update.getRefName());
- Optional<ChangeDraftNotesUpdate> du =
- ChangeDraftNotesUpdate.asChangeDraftNotesUpdate(update.getDraftUpdate());
- if (du.isPresent()) {
- draftUpdates.put(du.get().getRefName(), du.get());
+ ChangeDraftUpdate du = update.getDraftUpdate();
+ if (du != null) {
+ draftUpdates.put(du.getStorageKey(), du);
}
RobotCommentUpdate rcu = update.getRobotCommentUpdate();
if (rcu != null) {
@@ -287,9 +274,9 @@ public class NoteDbUpdateManager implements AutoCloseable {
changeUpdates.put(update.getRefName(), update);
}
- public void add(ChangeDraftNotesUpdate draftUpdate) {
+ public void add(ChangeDraftUpdate draftUpdate) {
checkNotExecuted();
- draftUpdates.put(draftUpdate.getRefName(), draftUpdate);
+ draftUpdates.put(draftUpdate.getStorageKey(), draftUpdate);
}
public void deleteChange(Change.Id id) {
@@ -310,16 +297,18 @@ public class NoteDbUpdateManager implements AutoCloseable {
initChangeRepo();
if (!draftUpdates.isEmpty() || !changesToDelete.isEmpty()) {
- initAllUsersRepo();
+ draftUpdatesExecutor = draftUpdatesExecutorFactory.create(currentUser);
}
addCommands();
}
}
+ @CanIgnoreReturnValue
public ImmutableMultimap<Project.NameKey, BatchRefUpdate> execute() throws IOException {
return execute(false);
}
+ @CanIgnoreReturnValue
public ImmutableMultimap<Project.NameKey, BatchRefUpdate> execute(boolean dryrun)
throws IOException {
checkNotExecuted();
@@ -333,7 +322,7 @@ public class NoteDbUpdateManager implements AutoCloseable {
NonCancellableOperationContext nonCancellableOperationContext =
RequestStateContext.startNonCancellableOperation()) {
stage();
- // ChangeUpdates must execute before ChangeDraftNotesUpdates.
+ // ChangeUpdates must execute before ChangeDraftUpdates.
//
// ChangeUpdate will automatically delete draft comments for any published
// comments, but the updates to the two repos don't happen atomically.
@@ -345,16 +334,18 @@ public class NoteDbUpdateManager implements AutoCloseable {
newTimer("NoteDbUpdateManager#updateRepo", Metadata.empty())) {
execute(changeRepo, dryrun, pushCert).ifPresent(bru -> resultBuilder.put(projectName, bru));
}
- try (TraceContext.TraceTimer ignored =
- newTimer("NoteDbUpdateManager#updateAllUsersSync", Metadata.empty())) {
- execute(allUsersRepo, dryrun, null).ifPresent(bru -> resultBuilder.put(allUsersName, bru));
- }
- if (!dryrun) {
- // Only execute the asynchronous operation if we are not in dry-run mode: The dry run would
- // have to run synchronous to be of any value at all. For the removal of draft comments from
- // All-Users we don't care much of the operation succeeds, so we are skipping the dry run
- // altogether.
- updateAllUsersAsync.execute(refLogIdent, refLogMessage, pushCert, currentUser);
+
+ if (draftUpdatesExecutor != null) {
+ draftUpdatesExecutor
+ .executeAllSyncUpdates(dryrun, refLogIdent, refLogMessage)
+ .ifPresent(bru -> resultBuilder.put(allUsersName, bru));
+ if (!dryrun) {
+ // Only execute the asynchronous operation if we are not in dry-run mode: The dry run
+ // would have to run synchronous to be of any value at all. For the removal of draft
+ // comments from All-Users we don't care much of the operation succeeds, so we are
+ // skipping the dry run altogether.
+ draftUpdatesExecutor.executeAllAsyncUpdates(refLogIdent, refLogMessage, pushCert);
+ }
}
executed = true;
return resultBuilder.build();
@@ -373,49 +364,20 @@ public class NoteDbUpdateManager implements AutoCloseable {
private Optional<BatchRefUpdate> execute(
OpenRepo or, boolean dryrun, @Nullable PushCertificate pushCert) throws IOException {
- if (or == null || or.cmds.isEmpty()) {
- return Optional.empty();
- }
- if (!dryrun) {
- or.flush();
- } else {
- // OpenRepo buffers objects separately; caller may assume that objects are available in the
- // inserter it previously passed via setChangeRepo.
- or.flushToFinalInserter();
- }
-
- BatchRefUpdate bru = or.repo.getRefDatabase().newBatchUpdate();
- bru.setPushCertificate(pushCert);
- if (refLogMessage != null) {
- bru.setRefLogMessage(refLogMessage, false);
- } else {
- bru.setRefLogMessage(
- firstNonNull(NoteDbUtil.guessRestApiHandler(), "Update NoteDb refs"), false);
- }
- bru.setRefLogIdent(refLogIdent != null ? refLogIdent : serverIdent.get());
- bru.setAtomic(true);
- or.cmds.addTo(bru);
- bru.setAllowNonFastForwards(allowNonFastForwards(or.cmds));
- for (BatchUpdateListener listener : batchUpdateListeners) {
- bru = listener.beforeUpdateRefs(bru);
- }
-
- if (!dryrun) {
- RefUpdateUtil.executeChecked(bru, or.rw);
- }
- return Optional.of(bru);
+ return noteDbUpdateExecutor.execute(
+ or,
+ dryrun,
+ allowNonFastForwards(),
+ batchUpdateListeners,
+ pushCert,
+ refLogIdent,
+ refLogMessage);
}
private void addCommands() throws IOException {
changeRepo.addUpdates(changeUpdates, Optional.of(maxUpdates), Optional.of(maxPatchSets));
if (!draftUpdates.isEmpty()) {
- boolean publishOnly =
- draftUpdates.values().stream().allMatch(ChangeDraftNotesUpdate::canRunAsync);
- if (publishOnly) {
- updateAllUsersAsync.setDraftUpdates(draftUpdates);
- } else {
- allUsersRepo.addUpdatesNoLimits(draftUpdates);
- }
+ draftUpdatesExecutor.queueAllDraftUpdates(draftUpdates);
}
if (!robotCommentUpdates.isEmpty()) {
changeRepo.addUpdatesNoLimits(robotCommentUpdates);
@@ -435,14 +397,7 @@ public class NoteDbUpdateManager implements AutoCloseable {
old.ifPresent(
objectId -> changeRepo.cmds.add(new ReceiveCommand(objectId, ObjectId.zeroId(), metaRef)));
- // Just scan repo for ref names, but get "old" values from cmds.
- for (Ref r :
- allUsersRepo.repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftCommentsPrefix(id))) {
- old = allUsersRepo.cmds.get(r.getName());
- old.ifPresent(
- objectId ->
- allUsersRepo.cmds.add(new ReceiveCommand(objectId, ObjectId.zeroId(), r.getName())));
- }
+ draftUpdatesExecutor.queueDeletionForChangeDrafts(id);
}
private void checkNotExecuted() {
@@ -487,17 +442,10 @@ public class NoteDbUpdateManager implements AutoCloseable {
*
* <p>2. NoteDb rewriters.
*
- * <p>3. If any of the receive commands is of type {@link
- * org.eclipse.jgit.transport.ReceiveCommand.Type#UPDATE_NONFASTFORWARD} (for example due to a
- * force push).
- *
* <p>Note that we don't need to explicitly allow non fast-forward updates for DELETE commands
* since JGit forces the update implicitly in this case.
*/
- private boolean allowNonFastForwards(ChainedReceiveCommands receiveCommands) {
- return !draftUpdates.isEmpty()
- || !rewriters.isEmpty()
- || receiveCommands.getCommands().values().stream()
- .anyMatch(cmd -> cmd.getType().equals(ReceiveCommand.Type.UPDATE_NONFASTFORWARD));
+ private boolean allowNonFastForwards() {
+ return !draftUpdates.isEmpty() || !rewriters.isEmpty();
}
}
diff --git a/java/com/google/gerrit/server/notedb/OpenRepo.java b/java/com/google/gerrit/server/notedb/OpenRepo.java
index d02ec874d6..8bef1645ce 100644
--- a/java/com/google/gerrit/server/notedb/OpenRepo.java
+++ b/java/com/google/gerrit/server/notedb/OpenRepo.java
@@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ListMultimap;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
import com.google.gerrit.exceptions.StorageException;
@@ -31,6 +32,7 @@ import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
@@ -43,6 +45,8 @@ import org.eclipse.jgit.transport.ReceiveCommand;
* objects that are jointly closed when invoking {@link #close}.
*/
class OpenRepo implements AutoCloseable {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
final Repository repo;
final RevWalk rw;
final ChainedReceiveCommands cmds;
@@ -106,12 +110,16 @@ class OpenRepo implements AutoCloseable {
void flush() throws IOException {
flushToFinalInserter();
+ logger.atFine().log("flushing inserter %s", finalIns);
finalIns.flush();
}
void flushToFinalInserter() throws IOException {
checkState(finalIns != null);
for (InsertedObject obj : inMemIns.getInsertedObjects()) {
+ logger.atFine().log(
+ "copying %s object %s to final inserter %s",
+ Constants.typeString(obj.type()), obj.id().name(), finalIns);
finalIns.insert(obj.type(), obj.data().toByteArray());
}
inMemIns.clear();
diff --git a/java/com/google/gerrit/server/notedb/RepoSequence.java b/java/com/google/gerrit/server/notedb/RepoSequence.java
index bf2795d26e..033a53fb66 100644
--- a/java/com/google/gerrit/server/notedb/RepoSequence.java
+++ b/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -404,11 +404,6 @@ public class RepoSequence implements Sequence {
}
@Override
- public int getBatchSize() {
- return batchSize;
- }
-
- @Override
public int current() {
counterLock.lock();
try (Repository repo = repoManager.openRepository(projectName);
@@ -436,7 +431,8 @@ public class RepoSequence implements Sequence {
@Override
public int last() {
if (counter == 0) {
- next();
+ @SuppressWarnings("unused")
+ var unused = next();
}
return counter - 1;
}
diff --git a/java/com/google/gerrit/server/notedb/StarredChangesUtilNoteDbImpl.java b/java/com/google/gerrit/server/notedb/StarredChangesUtilNoteDbImpl.java
index d5258b1b4e..f13b832e51 100644
--- a/java/com/google/gerrit/server/notedb/StarredChangesUtilNoteDbImpl.java
+++ b/java/com/google/gerrit/server/notedb/StarredChangesUtilNoteDbImpl.java
@@ -19,7 +19,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toSet;
import com.google.common.base.Joiner;
-import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.primitives.Ints;
@@ -31,7 +31,8 @@ import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.git.GitUpdateFailureException;
import com.google.gerrit.git.LockFailureException;
import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.StarredChangesUtil;
+import com.google.gerrit.server.StarredChangesReader;
+import com.google.gerrit.server.StarredChangesWriter;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -62,7 +63,7 @@ import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
@Singleton
-public class StarredChangesUtilNoteDbImpl implements StarredChangesUtil {
+public class StarredChangesUtilNoteDbImpl implements StarredChangesReader, StarredChangesWriter {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final String DEFAULT_STAR_LABEL = "star";
@@ -176,13 +177,13 @@ public class StarredChangesUtilNoteDbImpl implements StarredChangesUtil {
}
@Override
- public ImmutableMap<Account.Id, Ref> byChange(Change.Id virtualId) {
+ public ImmutableList<Account.Id> byChange(Change.Id virtualId) {
try (Repository repo = repoManager.openRepository(allUsers)) {
- ImmutableMap.Builder<Account.Id, Ref> builder = ImmutableMap.builder();
+ ImmutableList.Builder<Account.Id> builder = ImmutableList.builder();
for (Account.Id accountId : getStars(repo, virtualId)) {
Optional<Ref> starRef = getStarRef(repo, RefNames.refsStarredChanges(virtualId, accountId));
if (starRef.isPresent()) {
- builder.put(accountId, starRef.get());
+ builder.add(accountId);
}
}
return builder.build();
diff --git a/java/com/google/gerrit/server/notedb/package-info.java b/java/com/google/gerrit/server/notedb/package-info.java
new file mode 100644
index 0000000000..b9ba73d2ef
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.notedb;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/package-info.java
index 0709b86ae4..bcb24a13cd 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/package-info.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -12,8 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+@CheckReturnValue
package com.google.gerrit.server;
-// TODO - delete this class. It cannot be deleted as part of the original refactoring since it's
-// used by a plugin.
-public interface StarredChangesUtil extends StarredChangesReader, StarredChangesWriter {}
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/patch/AutoMerger.java b/java/com/google/gerrit/server/patch/AutoMerger.java
index e27faf6df8..1dacde7027 100644
--- a/java/com/google/gerrit/server/patch/AutoMerger.java
+++ b/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -28,6 +28,7 @@ import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.InMemoryInserter;
import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.logging.CallerFinder;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.update.RepoView;
import com.google.inject.Inject;
@@ -42,7 +43,6 @@ import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.ResolveMerger;
import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
@@ -99,6 +99,7 @@ public class AutoMerger {
private final boolean save;
private final boolean useDiff3;
private final ThreeWayMergeStrategy configuredMergeStrategy;
+ private final CallerFinder callerFinder;
@Inject
AutoMerger(
@@ -125,6 +126,7 @@ public class AutoMerger {
this.useDiff3 = diff3ConflictView(cfg);
this.gerritIdentProvider = gerritIdentProvider;
this.configuredMergeStrategy = MergeUtil.getMergeStrategy(cfg);
+ this.callerFinder = CallerFinder.builder().addTarget(AutoMerger.class).build();
}
/**
@@ -137,15 +139,10 @@ public class AutoMerger {
* @return auto-merge commit. Headers of the returned RevCommit are parsed.
*/
public RevCommit lookupFromGitOrMergeInMemory(
- Repository repo,
- RevWalk rw,
- InMemoryInserter ins,
- RevCommit merge,
- ThreeWayMergeStrategy mergeStrategy)
- throws IOException {
+ Repository repo, RevWalk rw, InMemoryInserter ins, RevCommit merge) throws IOException {
checkArgument(rw.getObjectReader().getCreatedFromInserter() == ins);
Optional<RevCommit> existingCommit =
- lookupCommit(repo, rw, RefNames.refsCacheAutomerge(merge.name()));
+ lookupCommit(new RepoView(repo, rw, ins), RefNames.refsCacheAutomerge(merge.name()));
if (existingCommit.isPresent()) {
counter.increment(OperationType.CACHE_LOAD);
return existingCommit.get();
@@ -153,7 +150,8 @@ public class AutoMerger {
counter.increment(OperationType.IN_MEMORY_WRITE);
logger.atInfo().log("Computing in-memory AutoMerge for %s", merge.name());
try (Timer1.Context<OperationType> ignored = latency.start(OperationType.IN_MEMORY_WRITE)) {
- return rw.parseCommit(createAutoMergeCommit(repo.getConfig(), rw, ins, merge, mergeStrategy));
+ return rw.parseCommit(
+ createAutoMergeCommit(repo.getConfig(), rw, ins, merge, configuredMergeStrategy));
}
}
@@ -168,8 +166,7 @@ public class AutoMerger {
* auto merge commit.
*/
public Optional<ReceiveCommand> createAutoMergeCommitIfNecessary(
- RepoView repoView, RevWalk rw, ObjectInserter ins, RevCommit maybeMergeCommit)
- throws IOException {
+ RepoView repoView, ObjectInserter ins, RevCommit maybeMergeCommit) throws IOException {
if (maybeMergeCommit.getParentCount() != 2) {
logger.atFine().log("AutoMerge not required");
return Optional.empty();
@@ -182,14 +179,14 @@ public class AutoMerger {
String automergeRef = RefNames.refsCacheAutomerge(maybeMergeCommit.name());
logger.atFine().log("AutoMerge ref=%s, mergeCommit=%s", automergeRef, maybeMergeCommit.name());
if (repoView.getRef(automergeRef).isPresent()) {
- logger.atFine().log("AutoMerge alredy exists");
+ logger.atFine().log("AutoMerge already exists");
return Optional.empty();
}
return Optional.of(
new ReceiveCommand(
ObjectId.zeroId(),
- createAutoMergeCommit(repoView, rw, ins, maybeMergeCommit),
+ createAutoMergeCommit(repoView, ins, maybeMergeCommit),
automergeRef));
}
@@ -200,23 +197,26 @@ public class AutoMerger {
*
* @return An auto-merge commit. Headers of the returned RevCommit are parsed.
*/
- ObjectId createAutoMergeCommit(
- RepoView repoView, RevWalk rw, ObjectInserter ins, RevCommit mergeCommit) throws IOException {
+ ObjectId createAutoMergeCommit(RepoView repoView, ObjectInserter ins, RevCommit mergeCommit)
+ throws IOException {
ObjectId autoMerge;
try (Timer1.Context<OperationType> ignored = latency.start(OperationType.ON_DISK_WRITE)) {
autoMerge =
createAutoMergeCommit(
- repoView.getConfig(), rw, ins, mergeCommit, configuredMergeStrategy);
+ repoView.getConfig(),
+ repoView.getRevWalk(),
+ ins,
+ mergeCommit,
+ configuredMergeStrategy);
}
counter.increment(OperationType.ON_DISK_WRITE);
- logger.atFine().log("Added %s AutoMerge ref update for commit", autoMerge.name());
return autoMerge;
}
- Optional<RevCommit> lookupCommit(Repository repo, RevWalk rw, String refName) throws IOException {
- Ref ref = repo.getRefDatabase().exactRef(refName);
- if (ref != null && ref.getObjectId() != null) {
- RevObject obj = rw.parseAny(ref.getObjectId());
+ Optional<RevCommit> lookupCommit(RepoView repoView, String refName) throws IOException {
+ Optional<ObjectId> commit = repoView.getRef(refName);
+ if (commit.isPresent()) {
+ RevObject obj = repoView.getRevWalk().parseAny(commit.get());
if (obj instanceof RevCommit) {
return Optional.of((RevCommit) obj);
}
@@ -236,25 +236,51 @@ public class AutoMerger {
RevCommit merge,
ThreeWayMergeStrategy mergeStrategy)
throws IOException {
+ // Use a non-flushing inserter to do the merging and do the flushing explicitly when we are done
+ // with creating the AutoMerge commit.
+ ObjectInserter nonFlushingInserter =
+ ins instanceof InMemoryInserter ? ins : new NonFlushingWrapper(ins);
+
rw.parseHeaders(merge);
- ResolveMerger m = (ResolveMerger) mergeStrategy.newMerger(ins, repoConfig);
+ ResolveMerger m = (ResolveMerger) mergeStrategy.newMerger(nonFlushingInserter, repoConfig);
DirCache dc = DirCache.newInCore();
m.setDirCache(dc);
- // If we don't plan on saving results, use a fully in-memory inserter.
- // Using just a non-flushing wrapper is not sufficient, since in particular DfsInserter might
- // try to write to storage after exceeding an internal buffer size.
- m.setObjectInserter(ins instanceof InMemoryInserter ? new NonFlushingWrapper(ins) : ins);
boolean couldMerge = m.merge(merge.getParents());
ObjectId treeId;
if (couldMerge) {
treeId = m.getResultTreeId();
+ logger.atFine().log(
+ "AutoMerge treeId=%s (no conflicts, inserter: %s, caller: %s)",
+ treeId.name(), m.getObjectInserter(), callerFinder.findCallerLazy());
} else {
+ if (m.getResultTreeId() != null) {
+ // Merging with conflicts below uses the same DirCache instance that has been used by the
+ // Merger to attempt the merge without conflicts.
+ //
+ // The Merger uses the DirCache to do the updates, and in particular to write the result
+ // tree. DirCache caches a single DirCacheTree instance that is used to write the result
+ // tree, but it writes the result tree only if there were no conflicts.
+ //
+ // Merging with conflicts uses the same DirCache instance to write the tree with conflicts
+ // that has been used by the Merger. This means if the Merger unexpectedly wrote a result
+ // tree although there had been conflicts, then merging with conflicts uses the same
+ // DirCacheTree instance to write the tree with conflicts. However DirCacheTree#writeTree
+ // writes a tree only once and then that tree is cached. Further invocations of
+ // DirCacheTree#writeTree have no effect and return the previously created tree. This means
+ // merging with conflicts can only successfully create the tree with conflicts if the Merger
+ // didn't write a result tree yet. Hence this is checked here and we log a warning if the
+ // result tree was already written.
+ logger.atWarning().log(
+ "result tree has already been written: %s (merge: %s, conflicts: %s, failed: %s)",
+ m, m.getResultTreeId().name(), m.getUnmergedPaths(), m.getFailingPaths());
+ }
+
treeId =
MergeUtil.mergeWithConflicts(
rw,
- ins,
+ nonFlushingInserter,
dc,
"HEAD",
merge.getParent(0),
@@ -262,8 +288,10 @@ public class AutoMerger {
merge.getParent(1),
m.getMergeResults(),
useDiff3);
+ logger.atFine().log(
+ "AutoMerge treeId=%s (with conflicts, inserter: %s, caller: %s)",
+ treeId.name(), nonFlushingInserter, callerFinder.findCallerLazy());
}
- logger.atFine().log("AutoMerge treeId=%s", treeId.name());
rw.parseHeaders(merge);
// For maximum stability, choose a single ident using the committer time of
@@ -284,7 +312,6 @@ public class AutoMerger {
ObjectId commitId = ins.insert(cb);
logger.atFine().log("AutoMerge commitId=%s", commitId.name());
- ins.flush();
if (ins instanceof InMemoryInserter) {
// When using an InMemoryInserter we need to read back the values from that inserter because
@@ -295,6 +322,8 @@ public class AutoMerger {
}
}
+ logger.atFine().log("flushing inserter %s", ins);
+ ins.flush();
return rw.parseCommit(commitId);
}
@@ -315,5 +344,10 @@ public class AutoMerger {
@Override
public void close() {}
+
+ @Override
+ public String toString() {
+ return String.format("%s (wrapped inserter: %s)", super.toString(), ins.toString());
+ }
}
}
diff --git a/java/com/google/gerrit/server/patch/BaseCommitUtil.java b/java/com/google/gerrit/server/patch/BaseCommitUtil.java
index a264793756..f408038958 100644
--- a/java/com/google/gerrit/server/patch/BaseCommitUtil.java
+++ b/java/com/google/gerrit/server/patch/BaseCommitUtil.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.patch;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
@@ -36,6 +37,8 @@ import org.eclipse.jgit.revwalk.RevWalk;
/** A utility class for computing the base commit / parent for a specific patchset commit. */
@Singleton
class BaseCommitUtil {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
private final AutoMerger autoMerger;
private final GitRepositoryManager repoManager;
@@ -49,17 +52,6 @@ class BaseCommitUtil {
this.repoManager = repoManager;
}
- @Nullable
- RevCommit getBaseCommit(Project.NameKey project, ObjectId newCommit, @Nullable Integer parentNum)
- throws IOException {
- try (Repository repo = repoManager.openRepository(project);
- ObjectInserter ins = newInserter(repo);
- ObjectReader reader = ins.newReader();
- RevWalk rw = new RevWalk(reader)) {
- return getParentCommit(repo, ins, rw, parentNum, newCommit);
- }
- }
-
/**
* Returns the number of parent commits of the commit represented by the commitId parameter.
*
@@ -78,29 +70,24 @@ class BaseCommitUtil {
}
/**
- * Returns the parent commit Object of the commit represented by the commitId parameter.
+ * Returns the base commit for the provided commit.
*
- * @param repo a git repository.
+ * @param repoView repo view
* @param ins a git object inserter in the database.
- * @param rw a {@link RevWalk} object of the repository.
+ * @param commitId 20 bytes commitId SHA-1 hash.
* @param parentNum used to identify the parent number for merge commits. If parentNum is null and
* {@code commitId} has two parents, the auto-merge commit will be returned. If {@code
* commitId} has a single parent, it will be returned.
- * @param commitId 20 bytes commitId SHA-1 hash.
* @return Returns the parent commit of the commit represented by the commitId parameter. Note
* that auto-merge is not supported for commits having more than two parents. If the commit
* has no parents (initial commit) or more than 2 parents {@code null} is returned as the
* parent commit.
*/
@Nullable
- RevCommit getParentCommit(
- Repository repo,
- ObjectInserter ins,
- RevWalk rw,
- @Nullable Integer parentNum,
- ObjectId commitId)
+ RevCommit getBaseCommit(
+ RepoView repoView, ObjectInserter ins, ObjectId commitId, @Nullable Integer parentNum)
throws IOException {
- RevCommit current = rw.parseCommit(commitId);
+ RevCommit current = repoView.getRevWalk().parseCommit(commitId);
switch (current.getParentCount()) {
case 0:
return null;
@@ -109,7 +96,7 @@ class BaseCommitUtil {
default:
if (parentNum != null) {
RevCommit r = current.getParent(parentNum - 1);
- rw.parseBody(r);
+ repoView.getRevWalk().parseBody(r);
return r;
}
// Only support auto-merge for 2 parents, not octopus merges
@@ -119,7 +106,7 @@ class BaseCommitUtil {
"diff against auto-merge commits is only supported if 'change.cacheAutomerge' config is set to true.");
}
// TODO(ghareeb): Avoid persisting auto-merge commits.
- return getAutoMergeFromGitOrCreate(repo, ins, rw, current);
+ return getAutoMergeFromGitOrCreate(repoView, ins, current);
}
return null;
}
@@ -132,19 +119,18 @@ class BaseCommitUtil {
* @return the auto-merge {@link RevCommit}
*/
private RevCommit getAutoMergeFromGitOrCreate(
- Repository repo, ObjectInserter ins, RevWalk rw, RevCommit mergeCommit) throws IOException {
+ RepoView repoView, ObjectInserter ins, RevCommit mergeCommit) throws IOException {
String refName = RefNames.refsCacheAutomerge(mergeCommit.name());
- Optional<RevCommit> autoMergeCommit = autoMerger.lookupCommit(repo, rw, refName);
+ Optional<RevCommit> autoMergeCommit = autoMerger.lookupCommit(repoView, refName);
if (autoMergeCommit.isPresent()) {
return autoMergeCommit.get();
}
- ObjectId autoMergeId =
- autoMerger.createAutoMergeCommit(new RepoView(repo, rw, ins), rw, ins, mergeCommit);
+ if (!saveAutomerge && !(ins instanceof InMemoryInserter)) {
+ ins = new InMemoryInserter(repoView.getRevWalk().getObjectReader());
+ }
+ ObjectId autoMergeId = autoMerger.createAutoMergeCommit(repoView, ins, mergeCommit);
+ logger.atFine().log("flushing inserter %s", ins);
ins.flush();
- return rw.parseCommit(autoMergeId);
- }
-
- private ObjectInserter newInserter(Repository repo) {
- return saveAutomerge ? repo.newObjectInserter() : new InMemoryInserter(repo);
+ return repoView.getRevWalk().parseCommit(autoMergeId);
}
}
diff --git a/java/com/google/gerrit/server/patch/DiffOperations.java b/java/com/google/gerrit/server/patch/DiffOperations.java
index a53660a7d4..062994064f 100644
--- a/java/com/google/gerrit/server/patch/DiffOperations.java
+++ b/java/com/google/gerrit/server/patch/DiffOperations.java
@@ -20,11 +20,14 @@ import com.google.gerrit.entities.Patch.ChangeType;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.Project.NameKey;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.server.git.InMemoryInserter;
import com.google.gerrit.server.patch.filediff.FileDiffOutput;
import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
+import com.google.gerrit.server.update.RepoView;
import java.util.Map;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.revwalk.RevWalk;
/**
@@ -36,6 +39,12 @@ import org.eclipse.jgit.revwalk.RevWalk;
* <li>The detailed file diff for a single file path.
* <li>The Intra-line diffs for a single file path (TODO:ghareeb).
* </ul>
+ *
+ * <p>Do not use this class from commit validators (classes that implement {@link
+ * com.google.gerrit.server.git.validators.CommitValidationListener}), but use {@link
+ * DiffOperationsForCommitValidation} that is provided in {@link
+ * com.google.gerrit.server.events.CommitReceivedEvent#diffOperations} instead (see javadoc of
+ * {@link DiffOperationsForCommitValidation} for the context).
*/
public interface DiffOperations {
@@ -65,25 +74,27 @@ public interface DiffOperations {
/**
* This method is similar to {@link #listModifiedFilesAgainstParent(NameKey, ObjectId, int,
- * DiffOptions)} but loads the modified files directly instead of retrieving them from the diff
- * cache.
+ * DiffOptions)} but it loads the modified files directly if the modified files are not cached yet
+ * (instead of loading them via the diff cache).
*
- * <p>A RevWalk and repoConfig are also supplied and are used to look up the commit IDs. This is
- * useful in case one the commits is currently being created, that's why the {@code revWalk}
- * parameter is needed.
- *
- * <p>Note that rename detection is disabled for this method.
+ * <p>Commits are looked up from the provided {@link RepoView}. This way this method can also read
+ * new commits which are being created by the current request.
*
+ * @param repoView view to the repo from which commits IDs are looked up
+ * @param ins {@link ObjectInserter} to be used to create the auto-merge if the diff is done for a
+ * merge commit against the auto-merge and the auto-merge ref doesn't exist yet. This may be
+ * an {@link InMemoryInserter}.
+ * @param enableRenameDetection whether rename detection should be enabled
* @return a map of file paths to {@link ModifiedFile}. The {@link ModifiedFile} contains the
* old/new file paths and the change type (added, deleted, etc...).
*/
- Map<String, ModifiedFile> loadModifiedFilesAgainstParent(
+ Map<String, ModifiedFile> loadModifiedFilesAgainstParentIfNecessary(
Project.NameKey project,
ObjectId newCommit,
int parentNum,
- DiffOptions diffOptions,
- RevWalk revWalk,
- Config repoConfig)
+ RepoView repoView,
+ ObjectInserter ins,
+ boolean enableRenameDetection)
throws DiffNotAvailableException;
/**
@@ -105,25 +116,24 @@ public interface DiffOperations {
/**
* This method is similar to {@link #listModifiedFilesAgainstParent(NameKey, ObjectId, int,
- * DiffOptions)} but loads the modified files directly instead of retrieving them from the diff
- * cache.
+ * DiffOptions)} but it loads the modified files directly if the modified files are not cached yet
+ * (instead of loading them via the diff cache).
*
* <p>A RevWalk and repoConfig are also supplied and are used to look up the commit IDs. This is
* useful in case one the commits is currently being created, that's why the {@code revWalk}
* parameter is needed.
*
- * <p>Note that rename detection is disabled for this method.
- *
+ * @param enableRenameDetection whether rename detection should be enabled
* @return a map of file paths to {@link ModifiedFile}. The {@link ModifiedFile} contains the
* old/new file paths and the change type (added, deleted, etc...).
*/
- Map<String, ModifiedFile> loadModifiedFiles(
+ Map<String, ModifiedFile> loadModifiedFilesIfNecessary(
Project.NameKey project,
ObjectId oldCommit,
ObjectId newCommit,
- DiffOptions diffOptions,
RevWalk revWalk,
- Config repoConfig)
+ Config repoConfig,
+ boolean enableRenameDetection)
throws DiffNotAvailableException;
/**
diff --git a/java/com/google/gerrit/server/patch/DiffOperationsForCommitValidation.java b/java/com/google/gerrit/server/patch/DiffOperationsForCommitValidation.java
new file mode 100644
index 0000000000..3bb9ed0107
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/DiffOperationsForCommitValidation.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.patch;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
+import com.google.gerrit.server.update.RepoView;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.util.Map;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+
+/**
+ * Class to get modified files in {@link
+ * com.google.gerrit.server.git.validators.CommitValidationListener}s.
+ *
+ * <p>Computing the modified files for a merge commit may require the creation of the auto-merge
+ * commit (usually the auto-merge commit is not created yet when the commit validators are invoked).
+ * However commit validators should not write any commits (as the name {@code
+ * CommitValidationListener} suggests they are only intended to validate and listen). In particular
+ * commit validators must not write the auto-merge commit with a new {@link ObjectInserter} instance
+ * that competes with the main {@link ObjectInserter} instance that is being used to create changes,
+ * patch sets and auto-merge commits. This class wraps the computation of modified files and takes
+ * care of creating any missing auto-merge commit with the main {@link ObjectInserter} instance, so
+ * that the auto-merge commit is only created by this {@link ObjectInserter} instance and there is
+ * no competing {@link ObjectInserter} instance that creates the same auto-merge commit. Creating
+ * the same auto-merge commit with competing {@link ObjectInserter} instances must be avoided as it
+ * can result issues during object quorum.
+ */
+public class DiffOperationsForCommitValidation {
+ public interface Factory {
+ DiffOperationsForCommitValidation create(RepoView repoView, ObjectInserter inserter);
+ }
+
+ private final DiffOperations diffOperations;
+ private final RepoView repoView;
+ private final ObjectInserter inserter;
+
+ @Inject
+ DiffOperationsForCommitValidation(
+ DiffOperations diffOperations,
+ @Assisted RepoView repoView,
+ @Assisted ObjectInserter inserter) {
+ this.diffOperations = diffOperations;
+ this.repoView = repoView;
+ this.inserter = inserter;
+ }
+
+ /**
+ * Retrieves the modified files from the {@link
+ * com.google.gerrit.server.patch.diff.ModifiedFilesCache} if they are already cached. If not, the
+ * modified files are loaded directly (using the main {@link org.eclipse.jgit.revwalk.RevWalk}
+ * instance that can see newly inserted objects) rather than loading them via the {@link
+ * com.google.gerrit.server.patch.diff.ModifiedFilesCache} (that would open a new {@link
+ * org.eclipse.jgit.revwalk.RevWalk} instance).
+ *
+ * <p>If the loading requires the creation of the auto-merge commit it is created with the main
+ * {@link ObjectInserter} instance (also see the class javadoc).
+ *
+ * <p>The results will be stored in the {@link
+ * com.google.gerrit.server.patch.diff.ModifiedFilesCache} so that calling this method multiple
+ * times loads the modified files only once (for the first call, for further calls the cached
+ * modified files are returned).
+ */
+ public Map<String, ModifiedFile> loadModifiedFilesAgainstParentIfNecessary(
+ Project.NameKey project, ObjectId newCommit, int parentNum, boolean enableRenameDetection)
+ throws DiffNotAvailableException {
+ return diffOperations.loadModifiedFilesAgainstParentIfNecessary(
+ project, newCommit, parentNum, repoView, inserter, enableRenameDetection);
+ }
+}
diff --git a/java/com/google/gerrit/server/patch/DiffOperationsImpl.java b/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
index 4d0bcc8358..27c6ca67de 100644
--- a/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
+++ b/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
@@ -14,10 +14,13 @@
package com.google.gerrit.server.patch;
+import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap;
import static com.google.gerrit.entities.Patch.COMMIT_MSG;
import static com.google.gerrit.entities.Patch.MERGE_LIST;
+import static java.util.Comparator.naturalOrder;
import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -29,9 +32,12 @@ import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.logging.CallerFinder;
import com.google.gerrit.server.patch.diff.ModifiedFilesCache;
import com.google.gerrit.server.patch.diff.ModifiedFilesCacheImpl;
import com.google.gerrit.server.patch.diff.ModifiedFilesCacheKey;
+import com.google.gerrit.server.patch.diff.ModifiedFilesLoader;
import com.google.gerrit.server.patch.filediff.FileDiffCache;
import com.google.gerrit.server.patch.filediff.FileDiffCacheImpl;
import com.google.gerrit.server.patch.filediff.FileDiffCacheKey;
@@ -40,6 +46,7 @@ import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesCacheImpl;
import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
import com.google.gerrit.server.patch.gitfilediff.GitFileDiffCacheImpl;
import com.google.gerrit.server.patch.gitfilediff.GitFileDiffCacheImpl.DiffAlgorithm;
+import com.google.gerrit.server.update.RepoView;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
@@ -49,15 +56,13 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
-import java.util.stream.Collectors;
-import org.eclipse.jgit.diff.DiffEntry;
-import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.util.io.DisabledOutputStream;
/**
* Provides different file diff operations. Uses the underlying Git/Gerrit caches to speed up the
@@ -67,27 +72,19 @@ import org.eclipse.jgit.util.io.DisabledOutputStream;
public class DiffOperationsImpl implements DiffOperations {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- private static final ImmutableMap<DiffEntry.ChangeType, Patch.ChangeType> changeTypeMap =
- ImmutableMap.of(
- DiffEntry.ChangeType.ADD,
- Patch.ChangeType.ADDED,
- DiffEntry.ChangeType.MODIFY,
- Patch.ChangeType.MODIFIED,
- DiffEntry.ChangeType.DELETE,
- Patch.ChangeType.DELETED,
- DiffEntry.ChangeType.RENAME,
- Patch.ChangeType.RENAMED,
- DiffEntry.ChangeType.COPY,
- Patch.ChangeType.COPIED);
-
- private static final int RENAME_SCORE = 60;
+ @VisibleForTesting static final int RENAME_SCORE = 60;
+
private static final DiffAlgorithm DEFAULT_DIFF_ALGORITHM =
DiffAlgorithm.HISTOGRAM_WITH_FALLBACK_MYERS;
private static final Whitespace DEFAULT_WHITESPACE = Whitespace.IGNORE_NONE;
+ private final GitRepositoryManager repoManager;
private final ModifiedFilesCache modifiedFilesCache;
+ private final ModifiedFilesCacheImpl modifiedFilesCacheImpl;
+ private final ModifiedFilesLoader.Factory modifiedFilesLoaderFactory;
private final FileDiffCache fileDiffCache;
private final BaseCommitUtil baseCommitUtil;
+ private final CallerFinder callerFinder;
public static Module module() {
return new CacheModule() {
@@ -104,20 +101,38 @@ public class DiffOperationsImpl implements DiffOperations {
@Inject
public DiffOperationsImpl(
+ GitRepositoryManager repoManager,
ModifiedFilesCache modifiedFilesCache,
+ ModifiedFilesCacheImpl modifiedFilesCacheImpl,
+ ModifiedFilesLoader.Factory modifiedFilesLoaderFactory,
FileDiffCache fileDiffCache,
BaseCommitUtil baseCommit) {
+ this.repoManager = repoManager;
this.modifiedFilesCache = modifiedFilesCache;
+ this.modifiedFilesCacheImpl = modifiedFilesCacheImpl;
+ this.modifiedFilesLoaderFactory = modifiedFilesLoaderFactory;
this.fileDiffCache = fileDiffCache;
this.baseCommitUtil = baseCommit;
+ this.callerFinder =
+ CallerFinder.builder()
+ .addTarget(DiffOperations.class)
+ .addTarget(DiffOperationsImpl.class)
+ .build();
}
@Override
public Map<String, FileDiffOutput> listModifiedFilesAgainstParent(
Project.NameKey project, ObjectId newCommit, int parent, DiffOptions diffOptions)
throws DiffNotAvailableException {
- try {
- DiffParameters diffParams = computeDiffParameters(project, newCommit, parent);
+ try (Repository repo = repoManager.openRepository(project);
+ ObjectInserter ins = repo.newObjectInserter();
+ ObjectReader reader = ins.newReader();
+ RevWalk revWalk = new RevWalk(reader)) {
+ logger.atFine().log(
+ "Opened repo %s to list modified files against parent for %s (inserter: %s, caller: %s)",
+ project, newCommit.name(), ins, callerFinder.findCallerLazy());
+ DiffParameters diffParams =
+ computeDiffParameters(project, newCommit, parent, new RepoView(repo, revWalk, ins), ins);
return getModifiedFiles(diffParams, diffOptions);
} catch (IOException e) {
throw new DiffNotAvailableException(
@@ -126,17 +141,19 @@ public class DiffOperationsImpl implements DiffOperations {
}
@Override
- public Map<String, ModifiedFile> loadModifiedFilesAgainstParent(
+ public Map<String, ModifiedFile> loadModifiedFilesAgainstParentIfNecessary(
Project.NameKey project,
ObjectId newCommit,
int parentNum,
- DiffOptions diffOptions,
- RevWalk revWalk,
- Config repoConfig)
+ RepoView repoView,
+ ObjectInserter ins,
+ boolean enableRenameDetection)
throws DiffNotAvailableException {
try {
- DiffParameters diffParams = computeDiffParameters(project, newCommit, parentNum);
- return loadModifiedFilesWithoutCache(project, diffParams, revWalk, repoConfig);
+ DiffParameters diffParams =
+ computeDiffParameters(project, newCommit, parentNum, repoView, ins);
+ return loadModifiedFilesWithoutCacheIfNecessary(
+ project, diffParams, repoView.getRevWalk(), repoView.getConfig(), enableRenameDetection);
} catch (IOException e) {
throw new DiffNotAvailableException(
String.format(
@@ -161,13 +178,13 @@ public class DiffOperationsImpl implements DiffOperations {
}
@Override
- public Map<String, ModifiedFile> loadModifiedFiles(
+ public Map<String, ModifiedFile> loadModifiedFilesIfNecessary(
Project.NameKey project,
ObjectId oldCommit,
ObjectId newCommit,
- DiffOptions diffOptions,
RevWalk revWalk,
- Config repoConfig)
+ Config repoConfig,
+ boolean enableRenameDetection)
throws DiffNotAvailableException {
DiffParameters params =
DiffParameters.builder()
@@ -176,7 +193,8 @@ public class DiffOperationsImpl implements DiffOperations {
.baseCommit(oldCommit)
.comparisonType(ComparisonType.againstOtherPatchSet())
.build();
- return loadModifiedFilesWithoutCache(project, params, revWalk, repoConfig);
+ return loadModifiedFilesWithoutCacheIfNecessary(
+ project, params, revWalk, repoConfig, enableRenameDetection);
}
@Override
@@ -187,8 +205,15 @@ public class DiffOperationsImpl implements DiffOperations {
String fileName,
@Nullable DiffPreferencesInfo.Whitespace whitespace)
throws DiffNotAvailableException {
- try {
- DiffParameters diffParams = computeDiffParameters(project, newCommit, parent);
+ try (Repository repo = repoManager.openRepository(project);
+ ObjectInserter ins = repo.newObjectInserter();
+ ObjectReader reader = ins.newReader();
+ RevWalk revWalk = new RevWalk(reader)) {
+ logger.atFine().log(
+ "Opened repo %s to get modified file against parent for %s (inserter: %s, caller: %s)",
+ project, newCommit.name(), ins, callerFinder.findCallerLazy());
+ DiffParameters diffParams =
+ computeDiffParameters(project, newCommit, parent, new RepoView(repo, revWalk, ins), ins);
FileDiffCacheKey key =
createFileDiffCacheKey(
project,
@@ -283,7 +308,7 @@ public class DiffOperationsImpl implements DiffOperations {
private FileDiffOutput getModifiedFileForKey(FileDiffCacheKey key)
throws DiffNotAvailableException {
- Map<String, FileDiffOutput> diffList =
+ ImmutableMap<String, FileDiffOutput> diffList =
getModifiedFilesForKeys(ImmutableList.of(key), DiffOptions.DEFAULTS);
return diffList.containsKey(key.newFilePath())
? diffList.get(key.newFilePath())
@@ -392,51 +417,61 @@ public class DiffOperationsImpl implements DiffOperations {
.build();
}
- /** Loads the modified file paths between two commits without inspecting the diff cache. */
- private static Map<String, ModifiedFile> loadModifiedFilesWithoutCache(
- Project.NameKey project, DiffParameters diffParams, RevWalk revWalk, Config repoConfig)
+ /**
+ * Retrieves the modified files from the {@link ModifiedFilesCache} if they are already cached. If
+ * not, the modified files are loaded directly (using the provided {@link RevWalk}) rather than
+ * loading them via the {@link ModifiedFilesCache} (that would open a new {@link RevWalk}
+ * instance).
+ *
+ * <p>The results will be stored in the {@link ModifiedFilesCache} so that calling this method
+ * multiple times loads the modified files only once (for the first call, for further calls the
+ * cached modified files are returned).
+ */
+ private ImmutableMap<String, ModifiedFile> loadModifiedFilesWithoutCacheIfNecessary(
+ Project.NameKey project,
+ DiffParameters diffParams,
+ RevWalk revWalk,
+ Config repoConfig,
+ boolean enableRenameDetection)
throws DiffNotAvailableException {
- ObjectId newCommit = diffParams.newCommit();
- ObjectId oldCommit = diffParams.baseCommit();
- try {
- ObjectReader reader = revWalk.getObjectReader();
- List<DiffEntry> diffEntries;
- try (DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
- df.setReader(reader, repoConfig);
- df.setDetectRenames(false);
- diffEntries = df.scan(oldCommit.equals(ObjectId.zeroId()) ? null : oldCommit, newCommit);
- }
- List<ModifiedFile> modifiedFiles =
- diffEntries.stream()
- .map(
- entry ->
- ModifiedFile.builder()
- .changeType(toChangeType(entry.getChangeType()))
- .oldPath(getGitPath(entry.getOldPath()))
- .newPath(getGitPath(entry.getNewPath()))
- .build())
- .collect(Collectors.toList());
- return DiffUtil.mergeRewrittenModifiedFiles(modifiedFiles).stream()
- .collect(ImmutableMap.toImmutableMap(ModifiedFile::getDefaultPath, Function.identity()));
- } catch (IOException e) {
- throw new DiffNotAvailableException(
- String.format(
- "Failed to compute the modified files for project '%s',"
- + " old commit '%s', new commit '%s'.",
- project, oldCommit.name(), newCommit.name()),
- e);
+ ModifiedFilesCacheKey.Builder cacheKeyBuilder =
+ ModifiedFilesCacheKey.builder()
+ .project(project)
+ .aCommit(diffParams.baseCommit())
+ .bCommit(diffParams.newCommit());
+ if (enableRenameDetection) {
+ cacheKeyBuilder.renameScore(RENAME_SCORE);
+ } else {
+ cacheKeyBuilder.disableRenameDetection();
}
- }
+ ModifiedFilesCacheKey cacheKey = cacheKeyBuilder.build();
- private static Optional<String> getGitPath(String path) {
- return path.equals(DiffEntry.DEV_NULL) ? Optional.empty() : Optional.of(path);
- }
+ Optional<ImmutableList<ModifiedFile>> cachedModifiedFiles =
+ modifiedFilesCacheImpl.getIfPresent(cacheKey);
+ if (cachedModifiedFiles.isPresent()) {
+ return toMap(cachedModifiedFiles.get());
+ }
- private static Patch.ChangeType toChangeType(DiffEntry.ChangeType changeType) {
- if (!changeTypeMap.containsKey(changeType)) {
- throw new IllegalArgumentException("Unsupported type " + changeType);
+ ModifiedFilesLoader modifiedFilesLoader = modifiedFilesLoaderFactory.create();
+ if (enableRenameDetection) {
+ modifiedFilesLoader.withRenameDetection(RENAME_SCORE);
}
- return changeTypeMap.get(changeType);
+ ImmutableMap<String, ModifiedFile> modifiedFiles =
+ toMap(
+ modifiedFilesLoader.load(
+ project, repoConfig, revWalk, diffParams.baseCommit(), diffParams.newCommit()));
+
+ // Store the result in the cache.
+ modifiedFilesCacheImpl.put(cacheKey, ImmutableList.copyOf(modifiedFiles.values()));
+ return modifiedFiles;
+ }
+
+ private static ImmutableMap<String, ModifiedFile> toMap(
+ ImmutableList<ModifiedFile> modifiedFiles) {
+ return modifiedFiles.stream()
+ .collect(
+ toImmutableSortedMap(
+ naturalOrder(), ModifiedFile::getDefaultPath, Function.identity()));
}
@AutoValue
@@ -485,11 +520,16 @@ public class DiffOperationsImpl implements DiffOperations {
/** Compute Diff parameters - the base commit and the comparison type - using the input args. */
private DiffParameters computeDiffParameters(
- Project.NameKey project, ObjectId newCommit, Integer parent) throws IOException {
+ Project.NameKey project,
+ ObjectId newCommit,
+ int parent,
+ RepoView repoView,
+ ObjectInserter ins)
+ throws IOException {
DiffParameters.Builder result =
DiffParameters.builder().project(project).newCommit(newCommit).parent(parent);
if (parent > 0) {
- RevCommit baseCommit = baseCommitUtil.getBaseCommit(project, newCommit, parent);
+ RevCommit baseCommit = baseCommitUtil.getBaseCommit(repoView, ins, newCommit, parent);
if (baseCommit == null) {
// The specified parent doesn't exist or is not supported, fall back to comparing against
// the root.
@@ -509,7 +549,7 @@ public class DiffOperationsImpl implements DiffOperations {
return result.build();
}
if (numParents == 1) {
- result.baseCommit(baseCommitUtil.getBaseCommit(project, newCommit, parent));
+ result.baseCommit(baseCommitUtil.getBaseCommit(repoView, ins, newCommit, parent));
result.comparisonType(ComparisonType.againstParent(1));
return result.build();
}
@@ -519,11 +559,12 @@ public class DiffOperationsImpl implements DiffOperations {
+ "with more than two parents is not supported. Commit %s has %d parents."
+ " Falling back to the diff against the first parent.",
newCommit, numParents);
- result.baseCommit(baseCommitUtil.getBaseCommit(project, newCommit, 1).getId());
+ result.baseCommit(baseCommitUtil.getBaseCommit(repoView, ins, newCommit, 1).getId());
result.comparisonType(ComparisonType.againstParent(1));
result.skipFiles(true);
} else {
- result.baseCommit(baseCommitUtil.getBaseCommit(project, newCommit, null));
+ result.baseCommit(
+ baseCommitUtil.getBaseCommit(repoView, ins, newCommit, /* parentNum= */ null));
result.comparisonType(ComparisonType.againstAutoMerge());
}
return result.build();
diff --git a/java/com/google/gerrit/server/patch/GitPositionTransformer.java b/java/com/google/gerrit/server/patch/GitPositionTransformer.java
index b2c02e719f..aca918b6c4 100644
--- a/java/com/google/gerrit/server/patch/GitPositionTransformer.java
+++ b/java/com/google/gerrit/server/patch/GitPositionTransformer.java
@@ -94,7 +94,8 @@ public class GitPositionTransformer {
Collection<PositionedEntity<T>> entities, Set<Mapping> mappings) {
// Update the file paths first as copied files might exist. For copied files, this operation
// will duplicate the PositionedEntity instances of the original file.
- List<PositionedEntity<T>> filePathUpdatedEntities = updateFilePaths(entities, mappings);
+ ImmutableList<PositionedEntity<T>> filePathUpdatedEntities =
+ updateFilePaths(entities, mappings);
return shiftRanges(filePathUpdatedEntities, mappings);
}
diff --git a/java/com/google/gerrit/server/patch/PatchList.java b/java/com/google/gerrit/server/patch/PatchList.java
index 4efbc69658..3c5be17e66 100644
--- a/java/com/google/gerrit/server/patch/PatchList.java
+++ b/java/com/google/gerrit/server/patch/PatchList.java
@@ -59,7 +59,7 @@ public class PatchList implements Serializable {
*/
@VisibleForTesting
static class ChangeTypeCmp implements Comparator<ChangeType> {
- static final List<ChangeType> order =
+ static final ImmutableList<ChangeType> order =
ImmutableList.of(
ChangeType.ADDED,
ChangeType.RENAMED,
diff --git a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
index 41f2fedab7..bdcc424454 100644
--- a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
@@ -14,19 +14,12 @@
package com.google.gerrit.server.patch.diff;
-import static com.google.common.collect.ImmutableList.toImmutableList;
-
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
-import com.google.common.collect.Streams;
-import com.google.common.flogger.FluentLogger;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.DiffNotAvailableException;
-import com.google.gerrit.server.patch.DiffUtil;
import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesCache;
import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesCacheImpl;
import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesCacheKey;
@@ -37,12 +30,9 @@ import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import java.io.IOException;
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Stream;
+import java.util.Optional;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
/**
@@ -57,8 +47,6 @@ import org.eclipse.jgit.revwalk.RevWalk;
*/
@Singleton
public class ModifiedFilesCacheImpl implements ModifiedFilesCache {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
private static final String MODIFIED_FILES = "modified_files";
private final LoadingCache<ModifiedFilesCacheKey, ImmutableList<ModifiedFile>> cache;
@@ -83,7 +71,7 @@ public class ModifiedFilesCacheImpl implements ModifiedFilesCache {
.maximumWeight(10 << 20)
.weigher(ModifiedFilesWeigher.class)
.version(4)
- .loader(ModifiedFilesLoader.class);
+ .loader(Loader.class);
}
};
}
@@ -105,14 +93,22 @@ public class ModifiedFilesCacheImpl implements ModifiedFilesCache {
}
}
- static class ModifiedFilesLoader
- extends CacheLoader<ModifiedFilesCacheKey, ImmutableList<ModifiedFile>> {
- private final GitModifiedFilesCache gitCache;
+ public Optional<ImmutableList<ModifiedFile>> getIfPresent(ModifiedFilesCacheKey key) {
+ return Optional.ofNullable(cache.getIfPresent(key));
+ }
+
+ public void put(ModifiedFilesCacheKey key, ImmutableList<ModifiedFile> modifiedFiles) {
+ cache.put(key, modifiedFiles);
+ }
+
+ static class Loader extends CacheLoader<ModifiedFilesCacheKey, ImmutableList<ModifiedFile>> {
private final GitRepositoryManager repoManager;
+ private final ModifiedFilesLoader.Factory modifiedFilesLoaderFactory;
@Inject
- ModifiedFilesLoader(GitModifiedFilesCache gitCache, GitRepositoryManager repoManager) {
- this.gitCache = gitCache;
+ Loader(
+ GitRepositoryManager repoManager, ModifiedFilesLoader.Factory modifiedFilesLoaderFactory) {
+ this.modifiedFilesLoaderFactory = modifiedFilesLoaderFactory;
this.repoManager = repoManager;
}
@@ -120,88 +116,15 @@ public class ModifiedFilesCacheImpl implements ModifiedFilesCache {
public ImmutableList<ModifiedFile> load(ModifiedFilesCacheKey key)
throws IOException, DiffNotAvailableException {
try (Repository repo = repoManager.openRepository(key.project());
- RevWalk rw = new RevWalk(repo.newObjectReader())) {
- return loadModifiedFiles(key, rw);
- }
- }
-
- private ImmutableList<ModifiedFile> loadModifiedFiles(ModifiedFilesCacheKey key, RevWalk rw)
- throws IOException, DiffNotAvailableException {
- ObjectId aTree =
- key.aCommit().equals(ObjectId.zeroId())
- ? key.aCommit()
- : DiffUtil.getTreeId(rw, key.aCommit());
- ObjectId bTree = DiffUtil.getTreeId(rw, key.bCommit());
- GitModifiedFilesCacheKey gitKey =
- GitModifiedFilesCacheKey.builder()
- .project(key.project())
- .aTree(aTree)
- .bTree(bTree)
- .renameScore(key.renameScore())
- .build();
- ImmutableList<ModifiedFile> modifiedFiles =
- DiffUtil.mergeRewrittenModifiedFiles(gitCache.get(gitKey));
- if (key.aCommit().equals(ObjectId.zeroId())) {
- return modifiedFiles;
+ RevWalk revWalk = new RevWalk(repo.newObjectReader())) {
+ ModifiedFilesLoader loader =
+ modifiedFilesLoaderFactory
+ .createWithRetrievingModifiedFilesForTreesFromGitModifiedFilesCache();
+ if (key.renameDetectionEnabled()) {
+ loader.withRenameDetection(key.renameScore());
+ }
+ return loader.load(key.project(), repo.getConfig(), revWalk, key.aCommit(), key.bCommit());
}
- RevCommit revCommitA = DiffUtil.getRevCommit(rw, key.aCommit());
- RevCommit revCommitB = DiffUtil.getRevCommit(rw, key.bCommit());
- if (DiffUtil.areRelated(revCommitA, revCommitB)) {
- return modifiedFiles;
- }
- Set<String> touchedFiles =
- getTouchedFilesWithParents(
- key, revCommitA.getParent(0).getId(), revCommitB.getParent(0).getId(), rw);
- return modifiedFiles.stream()
- .filter(f -> isTouched(touchedFiles, f))
- .collect(toImmutableList());
- }
-
- /**
- * Returns the paths of files that were modified between the old and new commits versus their
- * parents (i.e. old commit vs. its parent, and new commit vs. its parent).
- *
- * @param key the {@link ModifiedFilesCacheKey} representing the commits we are diffing
- * @param rw a {@link RevWalk} for the repository
- * @return The list of modified files between the old/new commits and their parents
- */
- private Set<String> getTouchedFilesWithParents(
- ModifiedFilesCacheKey key, ObjectId parentOfA, ObjectId parentOfB, RevWalk rw)
- throws IOException {
- try {
- // TODO(ghareeb): as an enhancement: the 3 calls of the underlying git cache can be combined
- GitModifiedFilesCacheKey oldVsBaseKey =
- GitModifiedFilesCacheKey.create(
- key.project(), parentOfA, key.aCommit(), key.renameScore(), rw);
- List<ModifiedFile> oldVsBase = gitCache.get(oldVsBaseKey);
-
- GitModifiedFilesCacheKey newVsBaseKey =
- GitModifiedFilesCacheKey.create(
- key.project(), parentOfB, key.bCommit(), key.renameScore(), rw);
- List<ModifiedFile> newVsBase = gitCache.get(newVsBaseKey);
-
- return Sets.union(getOldAndNewPaths(oldVsBase), getOldAndNewPaths(newVsBase));
- } catch (DiffNotAvailableException e) {
- logger.atWarning().log(
- "Failed to retrieve the touched files' commits (%s, %s) and parents (%s, %s): %s",
- key.aCommit(), key.bCommit(), parentOfA, parentOfB, e.getMessage());
- return ImmutableSet.of();
- }
- }
-
- private ImmutableSet<String> getOldAndNewPaths(List<ModifiedFile> files) {
- return files.stream()
- .flatMap(
- file -> Stream.concat(Streams.stream(file.oldPath()), Streams.stream(file.newPath())))
- .collect(ImmutableSet.toImmutableSet());
- }
-
- private static boolean isTouched(Set<String> touchedFilePaths, ModifiedFile modifiedFile) {
- String oldFilePath = modifiedFile.oldPath().orElse(null);
- String newFilePath = modifiedFile.newPath().orElse(null);
- // One of the above file paths could be /dev/null but we need not explicitly check for this
- // value as the set of file paths shouldn't contain it.
- return touchedFilePaths.contains(oldFilePath) || touchedFilePaths.contains(newFilePath);
}
}
}
diff --git a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheKey.java b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheKey.java
index 4a406c88c9..17829f819a 100644
--- a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheKey.java
+++ b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheKey.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.patch.diff;
import static com.google.gerrit.server.patch.DiffUtil.stringSize;
import com.google.auto.value.AutoValue;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.Project.NameKey;
import com.google.gerrit.proto.Protos;
@@ -68,6 +69,7 @@ public abstract class ModifiedFilesCacheKey {
public abstract ModifiedFilesCacheKey.Builder bCommit(ObjectId value);
+ @CanIgnoreReturnValue
public ModifiedFilesCacheKey.Builder disableRenameDetection() {
renameScore(-1);
return this;
diff --git a/java/com/google/gerrit/server/patch/diff/ModifiedFilesLoader.java b/java/com/google/gerrit/server/patch/diff/ModifiedFilesLoader.java
new file mode 100644
index 0000000000..0d03a585e0
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/diff/ModifiedFilesLoader.java
@@ -0,0 +1,268 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.patch.diff;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.util.Comparator.comparing;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
+import com.google.gerrit.server.patch.DiffUtil;
+import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesCache;
+import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesCacheKey;
+import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesLoader;
+import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/**
+ * Class to load the files that have been modified between two commits.
+ *
+ * <p>Rename detection is off unless {@link #withRenameDetection(int)} is called.
+ *
+ * <p>The commits and their trees are looked up via the {@link RevWalk} instance that is provided to
+ * the {@link #load(com.google.gerrit.entities.Project.NameKey, Config, RevWalk, ObjectId,
+ * ObjectId)} method, unless the modified files for the trees of the commits should be retrieved
+ * from the {@link GitModifiedFilesCache} (see {@link
+ * Factory#createWithRetrievingModifiedFilesForTreesFromGitModifiedFilesCache()} in which case the
+ * trees are looked up via a new {@link RevWalk} instance that is created by {@code
+ * GitModifiedFilesCacheImpl.Loader}. Looking up the trees from a new {@link RevWalk} instance only
+ * succeeds if they were already fully persisted in the repository, i.e., if these are not newly
+ * created trees or tree which have been created in memory. This means using the {@link
+ * GitModifiedFilesCache} is expected to cause {@link MissingObjectException}s for the commit trees
+ * that are newly created or that were created in memory only.
+ */
+public class ModifiedFilesLoader {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ @Singleton
+ public static class Factory {
+ private final GitModifiedFilesCache gitModifiedFilesCache;
+
+ @Inject
+ Factory(GitModifiedFilesCache gitModifiedFilesCache) {
+ this.gitModifiedFilesCache = gitModifiedFilesCache;
+ }
+
+ /**
+ * Creates a {@link ModifiedFilesLoader} instance that looks up the commits and their trees via
+ * the {@link RevWalk} instance that is provided to the {@link
+ * #load(com.google.gerrit.entities.Project.NameKey, Config, RevWalk, ObjectId, ObjectId)}
+ * method.
+ */
+ public ModifiedFilesLoader create() {
+ return new ModifiedFilesLoader(/* gitModifiedFilesCache= */ null);
+ }
+
+ /**
+ * Creates a {@link ModifiedFilesLoader} instance that retrieves the modified files for the
+ * trees of the commits from the {@link GitModifiedFilesCache}.
+ *
+ * <p>Retrieving modified files for the trees from the {@link GitModifiedFilesCache} means that
+ * the trees are loaded via a new {@link RevWalk} instance (that is created by {@code
+ * GitModifiedFilesCacheImpl.Loader}), and not by the {@link RevWalk} instance that is given to
+ * the {@link #load(com.google.gerrit.entities.Project.NameKey, Config, RevWalk, ObjectId,
+ * ObjectId)} method. Looking up the trees from a new {@link RevWalk} instance only succeeds if
+ * they were already fully persisted in the repository, i.e., if these are not newly created
+ * trees or tree which have been created in memory. This means using the {@link
+ * GitModifiedFilesCache} is expected to cause {@link MissingObjectException}s for the commit
+ * trees that are newly created or that were created in memory only. Also see the javadoc on
+ * this class.
+ */
+ ModifiedFilesLoader createWithRetrievingModifiedFilesForTreesFromGitModifiedFilesCache() {
+ return new ModifiedFilesLoader(gitModifiedFilesCache);
+ }
+ }
+
+ @Nullable private final GitModifiedFilesCache gitModifiedFilesCache;
+
+ @Nullable private Integer renameScore = null;
+
+ ModifiedFilesLoader(@Nullable GitModifiedFilesCache gitModifiedFilesCache) {
+ this.gitModifiedFilesCache = gitModifiedFilesCache;
+ }
+
+ /**
+ * Enables rename detection
+ *
+ * @param renameScore the score that should be used for the rename detection.
+ */
+ @CanIgnoreReturnValue
+ public ModifiedFilesLoader withRenameDetection(int renameScore) {
+ checkState(renameScore >= 0);
+ this.renameScore = renameScore;
+ return this;
+ }
+
+ /**
+ * Loads the files that have been modified between {@code baseCommit} and {@code newCommit}.
+ *
+ * <p>The commits and the commit trees are looked up via the given {@code revWalk} instance,
+ * unless the modified files for the trees of the commits should be retrieved from the {@link
+ * GitModifiedFilesCache} (see {@link
+ * Factory#createWithRetrievingModifiedFilesForTreesFromGitModifiedFilesCache()} in which case the
+ * trees are looked up via a new {@link RevWalk} instance that is created by {@code
+ * GitModifiedFilesCacheImpl.Loader}. Also see the javadoc on this class.
+ */
+ public ImmutableList<ModifiedFile> load(
+ Project.NameKey project,
+ Config repoConfig,
+ RevWalk revWalk,
+ ObjectId baseCommit,
+ ObjectId newCommit)
+ throws DiffNotAvailableException {
+ try {
+ ObjectId baseTree =
+ baseCommit.equals(ObjectId.zeroId())
+ ? ObjectId.zeroId()
+ : DiffUtil.getTreeId(revWalk, baseCommit);
+ ObjectId newTree = DiffUtil.getTreeId(revWalk, newCommit);
+ ImmutableList<ModifiedFile> modifiedFiles =
+ ImmutableList.sortedCopyOf(
+ comparing(f -> f.getDefaultPath()),
+ DiffUtil.mergeRewrittenModifiedFiles(
+ getModifiedFiles(
+ project, repoConfig, revWalk.getObjectReader(), baseTree, newTree)));
+ if (baseCommit.equals(ObjectId.zeroId())) {
+ return modifiedFiles;
+ }
+ RevCommit revCommitBase = DiffUtil.getRevCommit(revWalk, baseCommit);
+ RevCommit revCommitNew = DiffUtil.getRevCommit(revWalk, newCommit);
+ if (DiffUtil.areRelated(revCommitBase, revCommitNew)) {
+ return modifiedFiles;
+ }
+ Set<String> touchedFiles =
+ getTouchedFilesWithParents(
+ project,
+ repoConfig,
+ revWalk,
+ baseCommit,
+ revCommitBase.getParent(0).getId(),
+ newCommit,
+ revCommitNew.getParent(0).getId());
+ return modifiedFiles.stream()
+ .filter(f -> isTouched(touchedFiles, f))
+ .collect(toImmutableList());
+ } catch (IOException e) {
+ throw new DiffNotAvailableException(
+ String.format(
+ "Failed to get files that have been modified between commit %s and commit %s in project %s",
+ baseCommit.name(), newCommit.name(), project),
+ e);
+ }
+ }
+
+ /**
+ * Returns the paths of files that were modified between the base and new commits versus their
+ * parents (i.e. base commit vs. its parent, and new commit vs. its parent).
+ *
+ * @return The list of modified files between the base/new commits and their parents
+ */
+ private Set<String> getTouchedFilesWithParents(
+ Project.NameKey project,
+ Config repoConfig,
+ RevWalk revWalk,
+ ObjectId baseCommit,
+ ObjectId parentOfBase,
+ ObjectId newCommit,
+ ObjectId parentOfNew)
+ throws IOException {
+ try {
+ ImmutableList<ModifiedFile> oldVsBase =
+ getModifiedFiles(
+ project,
+ repoConfig,
+ revWalk.getObjectReader(),
+ DiffUtil.getTreeId(revWalk, parentOfBase),
+ DiffUtil.getTreeId(revWalk, baseCommit));
+ ImmutableList<ModifiedFile> newVsBase =
+ getModifiedFiles(
+ project,
+ repoConfig,
+ revWalk.getObjectReader(),
+ DiffUtil.getTreeId(revWalk, parentOfNew),
+ DiffUtil.getTreeId(revWalk, newCommit));
+ return Sets.union(getOldAndNewPaths(oldVsBase), getOldAndNewPaths(newVsBase));
+ } catch (DiffNotAvailableException e) {
+ logger.atWarning().log(
+ "Failed to retrieve the touched files' commits (%s, %s) and parents (%s, %s): %s",
+ baseCommit, newCommit, parentOfBase, parentOfNew, e.getMessage());
+ return ImmutableSet.of();
+ }
+ }
+
+ /**
+ * Get the files that have been modified between {@code baseTree} and {@code newTree}.
+ *
+ * <p>The modified files are loaded through {@link GitModifiedFilesLoader} unless it was requested
+ * to retrieve them from {@link GitModifiedFilesCache} (see {@link
+ * Factory#createWithRetrievingModifiedFilesForTreesFromGitModifiedFilesCache()})
+ */
+ private ImmutableList<ModifiedFile> getModifiedFiles(
+ Project.NameKey project,
+ Config repoConfig,
+ ObjectReader reader,
+ ObjectId baseTree,
+ ObjectId newTree)
+ throws IOException, DiffNotAvailableException {
+ if (gitModifiedFilesCache != null) {
+ GitModifiedFilesCacheKey.Builder cacheKeyBuilder =
+ GitModifiedFilesCacheKey.builder().project(project).aTree(baseTree).bTree(newTree);
+ if (renameScore != null) {
+ cacheKeyBuilder.renameScore(renameScore);
+ } else {
+ cacheKeyBuilder.disableRenameDetection();
+ }
+ return gitModifiedFilesCache.get(cacheKeyBuilder.build());
+ }
+
+ GitModifiedFilesLoader gitModifiedFilesLoader = new GitModifiedFilesLoader();
+ if (renameScore != null) {
+ gitModifiedFilesLoader.withRenameDetection(renameScore);
+ }
+ return gitModifiedFilesLoader.load(repoConfig, reader, baseTree, newTree);
+ }
+
+ private ImmutableSet<String> getOldAndNewPaths(List<ModifiedFile> files) {
+ return files.stream()
+ .flatMap(file -> Stream.concat(file.oldPath().stream(), file.newPath().stream()))
+ .collect(ImmutableSet.toImmutableSet());
+ }
+
+ private static boolean isTouched(Set<String> touchedFilePaths, ModifiedFile modifiedFile) {
+ String oldFilePath = modifiedFile.oldPath().orElse(null);
+ String newFilePath = modifiedFile.newPath().orElse(null);
+ // One of the above file paths could be /dev/null but we need not explicitly check for this
+ // value as the set of file paths shouldn't contain it.
+ return touchedFilePaths.contains(oldFilePath) || touchedFilePaths.contains(newFilePath);
+ }
+}
diff --git a/java/com/google/gerrit/server/patch/diff/package-info.java b/java/com/google/gerrit/server/patch/diff/package-info.java
new file mode 100644
index 0000000000..e45b57bf0e
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/diff/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.patch.diff;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/patch/filediff/AllDiffsEvaluator.java b/java/com/google/gerrit/server/patch/filediff/AllDiffsEvaluator.java
index 0923252120..fb13920a2e 100644
--- a/java/com/google/gerrit/server/patch/filediff/AllDiffsEvaluator.java
+++ b/java/com/google/gerrit/server/patch/filediff/AllDiffsEvaluator.java
@@ -75,7 +75,7 @@ class AllDiffsEvaluator {
// First batch: "old commit vs. new commit" and "new parent vs. new commit"
// Second batch: "old parent vs. old commit" and "old parent vs. new parent"
- Map<FileDiffCacheKey, GitDiffEntity> mainDiffs =
+ ImmutableMap<FileDiffCacheKey, GitDiffEntity> mainDiffs =
computeGitFileDiffs(
createGitKeys(
augmentedKeys,
@@ -83,7 +83,7 @@ class AllDiffsEvaluator {
k -> k.key().newCommit(),
k -> k.key().newFilePath()));
- Map<FileDiffCacheKey, GitDiffEntity> oldVsParentDiffs =
+ ImmutableMap<FileDiffCacheKey, GitDiffEntity> oldVsParentDiffs =
computeGitFileDiffs(
createGitKeys(
keysWithRebaseEdits,
@@ -91,7 +91,7 @@ class AllDiffsEvaluator {
k -> k.key().oldCommit(),
k -> mainDiffs.get(k.key()).gitDiff().oldPath().orElse(null)));
- Map<FileDiffCacheKey, GitDiffEntity> newVsParentDiffs =
+ ImmutableMap<FileDiffCacheKey, GitDiffEntity> newVsParentDiffs =
computeGitFileDiffs(
createGitKeys(
keysWithRebaseEdits,
@@ -99,7 +99,7 @@ class AllDiffsEvaluator {
k -> k.key().newCommit(),
k -> k.key().newFilePath()));
- Map<FileDiffCacheKey, GitDiffEntity> parentsDiffs =
+ ImmutableMap<FileDiffCacheKey, GitDiffEntity> parentsDiffs =
computeGitFileDiffs(
createGitKeys(
keysWithRebaseEdits,
@@ -149,7 +149,7 @@ class AllDiffsEvaluator {
* Computes the git diff for the git keys of the input map {@code keys} parameter. The computation
* uses the underlying {@link GitFileDiffCache}.
*/
- private Map<FileDiffCacheKey, GitDiffEntity> computeGitFileDiffs(
+ private ImmutableMap<FileDiffCacheKey, GitDiffEntity> computeGitFileDiffs(
Map<FileDiffCacheKey, GitFileDiffCacheKey> keys) throws DiffNotAvailableException {
ImmutableMap.Builder<FileDiffCacheKey, GitDiffEntity> result =
ImmutableMap.builderWithExpectedSize(keys.size());
diff --git a/java/com/google/gerrit/server/patch/filediff/EditTransformer.java b/java/com/google/gerrit/server/patch/filediff/EditTransformer.java
index 55568e4b0c..708374409c 100644
--- a/java/com/google/gerrit/server/patch/filediff/EditTransformer.java
+++ b/java/com/google/gerrit/server/patch/filediff/EditTransformer.java
@@ -50,7 +50,7 @@ class EditTransformer {
private final GitPositionTransformer positionTransformer =
new GitPositionTransformer(OmitPositionOnConflict.INSTANCE);
- private List<ContextAwareEdit> edits;
+ private ImmutableList<ContextAwareEdit> edits;
/**
* Creates a new {@code EditTransformer} for the edits contained in the specified {@code
@@ -113,7 +113,7 @@ class EditTransformer {
}
public static Stream<ContextAwareEdit> toEdits(FileEdits in) {
- List<Edit> edits = in.edits();
+ ImmutableList<Edit> edits = in.edits();
if (edits.isEmpty()) {
return Stream.of(ContextAwareEdit.createForNoContentEdit(in.oldPath(), in.newPath()));
}
diff --git a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
index d1bda5cb6d..82d7e93740 100644
--- a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
@@ -408,7 +408,7 @@ public class FileDiffCacheImpl implements FileDiffCache {
if (!augmentedKey.ignoreRebase()) {
rebaseFileEdits = computeRebaseEdits(allDiffs);
}
- List<Edit> rebaseEdits = rebaseFileEdits.edits();
+ ImmutableList<Edit> rebaseEdits = rebaseFileEdits.edits();
ObjectId oldTreeId = allDiffs.mainDiff().gitKey().oldTree();
diff --git a/java/com/google/gerrit/server/patch/filediff/package-info.java b/java/com/google/gerrit/server/patch/filediff/package-info.java
new file mode 100644
index 0000000000..1e3ad52887
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/filediff/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.patch.filediff;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheImpl.java b/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheImpl.java
index b70f6e1bf8..7cfc066f4b 100644
--- a/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheImpl.java
@@ -14,13 +14,9 @@
package com.google.gerrit.server.patch.gitdiff;
-import static com.google.common.collect.ImmutableList.toImmutableList;
-
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.entities.Patch;
import com.google.gerrit.proto.Protos;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.cache.proto.Cache.ModifiedFilesProto;
@@ -33,33 +29,14 @@ import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import java.io.IOException;
-import java.util.List;
-import java.util.Optional;
import java.util.concurrent.ExecutionException;
-import org.eclipse.jgit.diff.DiffEntry;
-import org.eclipse.jgit.diff.DiffEntry.ChangeType;
-import org.eclipse.jgit.diff.DiffFormatter;
-import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.util.io.DisabledOutputStream;
/** Implementation of the {@link GitModifiedFilesCache} */
@Singleton
public class GitModifiedFilesCacheImpl implements GitModifiedFilesCache {
private static final String GIT_MODIFIED_FILES = "git_modified_files";
- private static final ImmutableMap<ChangeType, Patch.ChangeType> changeTypeMap =
- ImmutableMap.of(
- DiffEntry.ChangeType.ADD,
- Patch.ChangeType.ADDED,
- DiffEntry.ChangeType.MODIFY,
- Patch.ChangeType.MODIFIED,
- DiffEntry.ChangeType.DELETE,
- Patch.ChangeType.DELETED,
- DiffEntry.ChangeType.RENAME,
- Patch.ChangeType.RENAMED,
- DiffEntry.ChangeType.COPY,
- Patch.ChangeType.COPIED);
private LoadingCache<GitModifiedFilesCacheKey, ImmutableList<ModifiedFile>> cache;
@@ -117,43 +94,12 @@ public class GitModifiedFilesCacheImpl implements GitModifiedFilesCache {
public ImmutableList<ModifiedFile> load(GitModifiedFilesCacheKey key) throws IOException {
try (Repository repo = repoManager.openRepository(key.project());
ObjectReader reader = repo.newObjectReader()) {
- List<DiffEntry> entries = getGitTreeDiff(repo, reader, key);
-
- return entries.stream().map(Loader::toModifiedFile).collect(toImmutableList());
- }
- }
-
- private List<DiffEntry> getGitTreeDiff(
- Repository repo, ObjectReader reader, GitModifiedFilesCacheKey key) throws IOException {
- try (DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
- df.setReader(reader, repo.getConfig());
+ GitModifiedFilesLoader loader = new GitModifiedFilesLoader();
if (key.renameDetection()) {
- df.setDetectRenames(true);
- df.getRenameDetector().setRenameScore(key.renameScore());
+ loader.withRenameDetection(key.renameScore());
}
- // Skip detecting content renames for binary files.
- df.getRenameDetector().setSkipContentRenamesForBinaryFiles(true);
- // The scan method only returns the file paths that are different. Callers may choose to
- // format these paths themselves.
- return df.scan(key.aTree().equals(ObjectId.zeroId()) ? null : key.aTree(), key.bTree());
- }
- }
-
- private static ModifiedFile toModifiedFile(DiffEntry entry) {
- String oldPath = entry.getOldPath();
- String newPath = entry.getNewPath();
- return ModifiedFile.builder()
- .changeType(toChangeType(entry.getChangeType()))
- .oldPath(oldPath.equals(DiffEntry.DEV_NULL) ? Optional.empty() : Optional.of(oldPath))
- .newPath(newPath.equals(DiffEntry.DEV_NULL) ? Optional.empty() : Optional.of(newPath))
- .build();
- }
-
- private static Patch.ChangeType toChangeType(DiffEntry.ChangeType changeType) {
- if (!changeTypeMap.containsKey(changeType)) {
- throw new IllegalArgumentException("Unsupported type " + changeType);
+ return loader.load(repo.getConfig(), reader, key.aTree(), key.bTree());
}
- return changeTypeMap.get(changeType);
}
}
diff --git a/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheKey.java b/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheKey.java
index 16b0e6580c..52b274adad 100644
--- a/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheKey.java
+++ b/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheKey.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.patch.gitdiff;
import static com.google.gerrit.server.patch.DiffUtil.stringSize;
import com.google.auto.value.AutoValue;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.Project.NameKey;
import com.google.gerrit.proto.Protos;
@@ -92,6 +93,7 @@ public abstract class GitModifiedFilesCacheKey {
public abstract Builder renameScore(int value);
+ @CanIgnoreReturnValue
public Builder disableRenameDetection() {
renameScore(-1);
return this;
diff --git a/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesLoader.java b/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesLoader.java
new file mode 100644
index 0000000000..69a1e9d865
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesLoader.java
@@ -0,0 +1,117 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.patch.gitdiff;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Patch;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffEntry.ChangeType;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.io.DisabledOutputStream;
+
+/**
+ * /** Class to load the files that have been modified between two Git trees.
+ *
+ * <p>Rename detection is off unless {@link #withRenameDetection(int)} is called.
+ *
+ * <p>The commits and the commit trees are looked up via the {@link RevWalk} instance that is
+ * provided to the {@link #load(Config, ObjectReader, ObjectId, ObjectId)} method.
+ */
+public class GitModifiedFilesLoader {
+ private static final ImmutableMap<ChangeType, Patch.ChangeType> changeTypeMap =
+ ImmutableMap.of(
+ DiffEntry.ChangeType.ADD,
+ Patch.ChangeType.ADDED,
+ DiffEntry.ChangeType.MODIFY,
+ Patch.ChangeType.MODIFIED,
+ DiffEntry.ChangeType.DELETE,
+ Patch.ChangeType.DELETED,
+ DiffEntry.ChangeType.RENAME,
+ Patch.ChangeType.RENAMED,
+ DiffEntry.ChangeType.COPY,
+ Patch.ChangeType.COPIED);
+
+ @Nullable private Integer renameScore = null;
+
+ /**
+ * Enables rename detection
+ *
+ * @param renameScore the score that should be used for the rename detection.
+ */
+ @CanIgnoreReturnValue
+ public GitModifiedFilesLoader withRenameDetection(int renameScore) {
+ checkState(renameScore >= 0);
+ this.renameScore = renameScore;
+ return this;
+ }
+
+ /**
+ * Loads the files that have been modified between {@code aTree} and {@code bTree}.
+ *
+ * <p>The trees are looked up via the given {@code revWalk} instance,
+ */
+ public ImmutableList<ModifiedFile> load(
+ Config repoConfig, ObjectReader reader, ObjectId aTree, ObjectId bTree) throws IOException {
+ List<DiffEntry> entries = getGitTreeDiff(repoConfig, reader, aTree, bTree);
+ return entries.stream().map(GitModifiedFilesLoader::toModifiedFile).collect(toImmutableList());
+ }
+
+ private List<DiffEntry> getGitTreeDiff(
+ Config repoConfig, ObjectReader reader, ObjectId aTree, ObjectId bTree) throws IOException {
+ try (DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
+ df.setReader(reader, repoConfig);
+ if (renameScore != null) {
+ df.setDetectRenames(true);
+ df.getRenameDetector().setRenameScore(renameScore);
+
+ // Skip detecting content renames for binary files.
+ df.getRenameDetector().setSkipContentRenamesForBinaryFiles(true);
+ }
+ // The scan method only returns the file paths that are different. Callers may choose to
+ // format these paths themselves.
+ return df.scan(aTree.equals(ObjectId.zeroId()) ? null : aTree, bTree);
+ }
+ }
+
+ private static ModifiedFile toModifiedFile(DiffEntry entry) {
+ String oldPath = entry.getOldPath();
+ String newPath = entry.getNewPath();
+ return ModifiedFile.builder()
+ .changeType(toChangeType(entry.getChangeType()))
+ .oldPath(oldPath.equals(DiffEntry.DEV_NULL) ? Optional.empty() : Optional.of(oldPath))
+ .newPath(newPath.equals(DiffEntry.DEV_NULL) ? Optional.empty() : Optional.of(newPath))
+ .build();
+ }
+
+ private static Patch.ChangeType toChangeType(DiffEntry.ChangeType changeType) {
+ if (!changeTypeMap.containsKey(changeType)) {
+ throw new IllegalArgumentException("Unsupported type " + changeType);
+ }
+ return changeTypeMap.get(changeType);
+ }
+}
diff --git a/java/com/google/gerrit/server/patch/gitdiff/package-info.java b/java/com/google/gerrit/server/patch/gitdiff/package-info.java
new file mode 100644
index 0000000000..e78c6e3b0e
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/gitdiff/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.patch.gitdiff;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java
index d0d024c20c..580aef59c7 100644
--- a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java
+++ b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java
@@ -31,7 +31,6 @@ import com.google.gerrit.server.cache.serialize.CacheSerializer;
import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
import com.google.gerrit.server.patch.filediff.Edit;
import com.google.protobuf.Descriptors.FieldDescriptor;
-import java.util.Map;
import java.util.Optional;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
@@ -45,7 +44,7 @@ import org.eclipse.jgit.patch.FileHeader;
*/
@AutoValue
public abstract class GitFileDiff {
- private static final Map<FileMode, Patch.FileMode> fileModeMap =
+ private static final ImmutableMap<FileMode, Patch.FileMode> fileModeMap =
ImmutableMap.<FileMode, Patch.FileMode>builder()
.put(FileMode.TREE, Patch.FileMode.TREE)
.put(FileMode.SYMLINK, Patch.FileMode.SYMLINK)
diff --git a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
index 72dc4348c5..84eda5178b 100644
--- a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
@@ -55,7 +55,6 @@ import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
@@ -233,7 +232,7 @@ public class GitFileDiffCacheImpl implements GitFileDiffCache {
*
* @return The git file diffs for all input keys.
*/
- private Map<GitFileDiffCacheKey, GitFileDiff> loadAllImpl(
+ private ImmutableMap<GitFileDiffCacheKey, GitFileDiff> loadAllImpl(
Repository repo, DiffOptions options, List<GitFileDiffCacheKey> keys)
throws IOException, DiffNotAvailableException {
ImmutableMap.Builder<GitFileDiffCacheKey, GitFileDiff> result =
@@ -279,7 +278,7 @@ public class GitFileDiffCacheImpl implements GitFileDiffCache {
private static ListMultimap<String, DiffEntry> loadDiffEntries(
DiffFormatter diffFormatter, DiffOptions diffOptions, Collection<String> filePaths)
throws IOException {
- Set<String> filePathsSet = ImmutableSet.copyOf(filePaths);
+ ImmutableSet<String> filePathsSet = ImmutableSet.copyOf(filePaths);
List<DiffEntry> diffEntries =
diffFormatter.scan(
diffOptions.oldTree().equals(ObjectId.zeroId()) ? null : diffOptions.oldTree(),
diff --git a/java/com/google/gerrit/server/patch/gitfilediff/package-info.java b/java/com/google/gerrit/server/patch/gitfilediff/package-info.java
new file mode 100644
index 0000000000..786011f7bb
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/gitfilediff/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.patch.gitfilediff;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/patch/package-info.java b/java/com/google/gerrit/server/patch/package-info.java
new file mode 100644
index 0000000000..fc85a1a9e8
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.patch;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
index a4ee052440..677ee18587 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
@@ -19,6 +19,7 @@ import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toSet;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
@@ -41,7 +42,6 @@ import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.util.Collection;
-import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -238,7 +238,7 @@ public class DefaultPermissionBackend extends PermissionBackend {
}
private boolean canEmailReviewers() {
- List<PermissionRule> email = capabilities().emailReviewers;
+ ImmutableList<PermissionRule> email = capabilities().emailReviewers;
if (allow(email)) {
logger.atFinest().log(
"user %s can email reviewers (allowed by %s)", user.getLoggableName(), email);
diff --git a/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java b/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java
index 640ea9a6b8..d8109af324 100644
--- a/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java
+++ b/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java
@@ -107,9 +107,13 @@ public class GitVisibleChangeFilter {
id -> {
try {
ChangeData cd = changeDataFactory.create(projectName, id);
- cd.notes(); // Make sure notes are available. This will trigger loading notes and
- // throw an exception in case the change is corrupt and can't be loaded. It will
- // then be omitted from the result.
+
+ // Make sure notes are available. This will trigger loading notes and throw an
+ // exception in case the change is corrupt and can't be loaded. It will then be
+ // omitted from the result.
+ @SuppressWarnings("unused")
+ var unused = cd.notes();
+
return cd;
} catch (Exception e) {
// We drop changes that we can't load. The repositories contain 'dead' change refs
diff --git a/java/com/google/gerrit/server/permissions/package-info.java b/java/com/google/gerrit/server/permissions/package-info.java
new file mode 100644
index 0000000000..df7a36af83
--- /dev/null
+++ b/java/com/google/gerrit/server/permissions/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.permissions;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/plugincontext/package-info.java b/java/com/google/gerrit/server/plugincontext/package-info.java
new file mode 100644
index 0000000000..487b39ca2f
--- /dev/null
+++ b/java/com/google/gerrit/server/plugincontext/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.plugincontext;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/plugins/AutoRegisterModules.java b/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
index 9d93ed2fda..1942193eab 100644
--- a/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
+++ b/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
@@ -21,6 +21,7 @@ import static com.google.gerrit.server.plugins.PluginGuiceEnvironment.is;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.annotations.Export;
import com.google.gerrit.extensions.annotations.ExtensionPoint;
import com.google.gerrit.extensions.annotations.Listen;
@@ -71,6 +72,7 @@ class AutoRegisterModules {
this.httpGen = env.hasHttpModule() ? env.newHttpModuleGenerator() : new ModuleGenerator.NOP();
}
+ @CanIgnoreReturnValue
AutoRegisterModules discover() throws InvalidPluginException {
sysSingletons = new HashSet<>();
sysListen = LinkedListMultimap.create();
diff --git a/java/com/google/gerrit/server/plugins/CopyConfigModule.java b/java/com/google/gerrit/server/plugins/CopyConfigModule.java
index c495721584..2764675730 100644
--- a/java/com/google/gerrit/server/plugins/CopyConfigModule.java
+++ b/java/com/google/gerrit/server/plugins/CopyConfigModule.java
@@ -44,7 +44,37 @@ import org.eclipse.jgit.lib.PersonIdent;
@SuppressWarnings("ProvidesMethodOutsideOfModule")
@Singleton
class CopyConfigModule extends AbstractModule {
- @Inject @SitePath private Path sitePath;
+ private final Path sitePath;
+ private final SitePaths sitePaths;
+ private final TrackingFooters trackingFooters;
+ private final Config gerritServerConfig;
+ private final GitRepositoryManager gitRepositoryManager;
+ private final String anonymousCowardName;
+ private final GerritPersonIdentProvider serverIdentProvider;
+ private final SecureStore secureStore;
+ private final GerritIsReplicaProvider isReplicaProvider;
+
+ @Inject
+ CopyConfigModule(
+ @SitePath Path sitePath,
+ SitePaths sitePaths,
+ TrackingFooters trackingFooters,
+ @GerritServerConfig Config gerritServerConfig,
+ GitRepositoryManager gitRepositoryManager,
+ @AnonymousCowardName String anonymousCowardName,
+ GerritPersonIdentProvider serverIdentProvider,
+ SecureStore secureStore,
+ GerritIsReplicaProvider isReplicaProvider) {
+ this.sitePath = sitePath;
+ this.sitePaths = sitePaths;
+ this.trackingFooters = trackingFooters;
+ this.gerritServerConfig = gerritServerConfig;
+ this.gitRepositoryManager = gitRepositoryManager;
+ this.anonymousCowardName = anonymousCowardName;
+ this.serverIdentProvider = serverIdentProvider;
+ this.secureStore = secureStore;
+ this.isReplicaProvider = isReplicaProvider;
+ }
@Provides
@SitePath
@@ -52,69 +82,50 @@ class CopyConfigModule extends AbstractModule {
return sitePath;
}
- @Inject private SitePaths sitePaths;
-
@Provides
SitePaths getSitePaths() {
return sitePaths;
}
- @Inject private TrackingFooters trackingFooters;
-
@Provides
TrackingFooters getTrackingFooters() {
return trackingFooters;
}
- @Inject @GerritServerConfig private Config gerritServerConfig;
-
@Provides
@GerritServerConfig
Config getGerritServerConfig() {
return gerritServerConfig;
}
- @Inject private GitRepositoryManager gitRepositoryManager;
-
@Provides
GitRepositoryManager getGitRepositoryManager() {
return gitRepositoryManager;
}
- @Inject @AnonymousCowardName private String anonymousCowardName;
-
@Provides
@AnonymousCowardName
String getAnonymousCowardName() {
return anonymousCowardName;
}
- @Inject private GerritPersonIdentProvider serverIdentProvider;
-
@Provides
@GerritPersonIdent
PersonIdent getServerIdent() {
return serverIdentProvider.get();
}
- @Inject private SecureStore secureStore;
-
@Provides
SecureStore getSecureStore() {
return secureStore;
}
- @Inject private GerritIsReplicaProvider isReplicaProvider;
-
@Provides
@GerritIsReplica
boolean getIsReplica() {
return isReplicaProvider.get();
}
- @Inject
- CopyConfigModule() {}
-
@Override
protected void configure() {}
}
diff --git a/java/com/google/gerrit/server/plugins/JarPluginProvider.java b/java/com/google/gerrit/server/plugins/JarPluginProvider.java
index 760631d4f1..e64651dfb2 100644
--- a/java/com/google/gerrit/server/plugins/JarPluginProvider.java
+++ b/java/com/google/gerrit/server/plugins/JarPluginProvider.java
@@ -30,7 +30,6 @@ import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
@@ -131,7 +130,7 @@ public class JarPluginProvider implements ServerPluginProvider {
List<URL> urls = new ArrayList<>(2);
String overlay = System.getProperty("gerrit.plugin-classes");
if (overlay != null) {
- Path classes = Paths.get(overlay).resolve(name).resolve("main");
+ Path classes = Path.of(overlay).resolve(name).resolve("main");
if (Files.isDirectory(classes)) {
logger.atInfo().log("plugin %s: including %s", name, classes);
urls.add(classes.toUri().toURL());
diff --git a/java/com/google/gerrit/server/plugins/JarScanner.java b/java/com/google/gerrit/server/plugins/JarScanner.java
index 122e3f4e2f..b92a9b28ea 100644
--- a/java/com/google/gerrit/server/plugins/JarScanner.java
+++ b/java/com/google/gerrit/server/plugins/JarScanner.java
@@ -19,6 +19,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Iterables.transform;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Maps;
@@ -112,7 +113,7 @@ public class JarScanner implements PluginContentScanner, AutoCloseable {
for (Class<? extends Annotation> annotoation : annotations) {
String descr = classObjToClassDescr.get(annotoation);
- Collection<ClassData> discoverdData = rawMap.get(descr);
+ List<ClassData> discoverdData = rawMap.get(descr);
Collection<ClassData> values = firstNonNull(discoverdData, Collections.emptySet());
result.put(
@@ -331,7 +332,7 @@ public class JarScanner implements PluginContentScanner, AutoCloseable {
return Maps.transformEntries(attributes, (key, value) -> (String) value);
}
- private static Iterable<JarEntry> entriesOf(JarFile jarFile) {
+ private static ImmutableList<JarEntry> entriesOf(JarFile jarFile) {
return jarFile.stream().collect(toImmutableList());
}
}
diff --git a/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
index 89d6f8f1b7..7ecb6c64f2 100644
--- a/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
+++ b/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -251,7 +251,8 @@ public class PluginGuiceEnvironment {
}
public void exit(RequestContext old) {
- local.setContext(old);
+ @SuppressWarnings("unused")
+ var unused = local.setContext(old);
}
public void onStartPlugin(Plugin plugin) {
@@ -276,7 +277,7 @@ public class PluginGuiceEnvironment {
apiSets.putAll(dynamicSetsOf(apiInjector));
apiMaps.putAll(dynamicMapsOf(apiInjector));
- List<Injector> allPluginInjectors =
+ ImmutableList<Injector> allPluginInjectors =
listOfInjectors(
plugin.getSysInjector(), plugin.getSshInjector(), plugin.getHttpInjector());
allPluginInjectors.forEach(i -> attachItem(apiItems, i, plugin));
@@ -292,7 +293,7 @@ public class PluginGuiceEnvironment {
}
}
- private List<Injector> listOfInjectors(Injector... injectors) {
+ private ImmutableList<Injector> listOfInjectors(Injector... injectors) {
ImmutableList.Builder<Injector> injectorsListBuilder = ImmutableList.builder();
for (Injector injector : injectors) {
diff --git a/java/com/google/gerrit/server/plugins/PluginLoader.java b/java/com/google/gerrit/server/plugins/PluginLoader.java
index 8260104018..e122d8de32 100644
--- a/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -27,6 +27,7 @@ import com.google.common.collect.Maps;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -47,7 +48,6 @@ import java.io.InputStream;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.AbstractMap;
import java.util.ArrayDeque;
import java.util.ArrayList;
@@ -164,6 +164,7 @@ public class PluginLoader implements LifecycleListener {
return plugins;
}
+ @CanIgnoreReturnValue
public String installPluginFromStream(String originalName, InputStream in)
throws IOException, PluginInstallException {
checkRemoteInstall();
@@ -519,6 +520,7 @@ public class PluginLoader implements LifecycleListener {
dropRemovedDisabledPlugins(jars);
}
+ @CanIgnoreReturnValue
private Plugin runPlugin(String name, Path plugin, Plugin oldPlugin)
throws PluginInstallException {
FileSnapshot snapshot = FileSnapshot.save(plugin.toFile());
@@ -611,6 +613,7 @@ public class PluginLoader implements LifecycleListener {
}
}
+ @CanIgnoreReturnValue
synchronized int processPendingCleanups() {
Iterator<Plugin> iterator = toCleanup.iterator();
while (iterator.hasNext()) {
@@ -726,15 +729,15 @@ public class PluginLoader implements LifecycleListener {
assert winner != null;
// Disable all loser plugins by renaming their file names to
// "file.disabled" and replace the disabled files in the multimap.
- Collection<Path> elementsToRemove = new ArrayList<>();
- Collection<Path> elementsToAdd = new ArrayList<>();
+ List<Path> elementsToRemove = new ArrayList<>();
+ List<Path> elementsToAdd = new ArrayList<>();
for (Path loser : Iterables.skip(enabled, 1)) {
logger.atWarning().log(
"Plugin <%s> was disabled, because"
+ " another plugin <%s>"
+ " with the same name <%s> already exists",
loser, winner, plugin);
- Path disabledPlugin = Paths.get(loser + ".disabled");
+ Path disabledPlugin = Path.of(loser + ".disabled");
elementsToAdd.add(disabledPlugin);
elementsToRemove.add(loser);
try {
diff --git a/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java b/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java
index 739dcdca16..e11be1b047 100644
--- a/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java
+++ b/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java
@@ -34,9 +34,10 @@ import java.nio.file.Path;
class ServerPluginInfoModule extends AbstractModule {
private final ServerPlugin plugin;
private final Path dataDir;
+ private final MetricMaker serverMetrics;
+ @SuppressWarnings("MutableGuiceModule")
private volatile boolean ready;
- private final MetricMaker serverMetrics;
ServerPluginInfoModule(ServerPlugin plugin, MetricMaker serverMetrics) {
this.plugin = plugin;
diff --git a/java/com/google/gerrit/server/plugins/package-info.java b/java/com/google/gerrit/server/plugins/package-info.java
new file mode 100644
index 0000000000..fac5fdf36e
--- /dev/null
+++ b/java/com/google/gerrit/server/plugins/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.plugins;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/project/ChildProjects.java b/java/com/google/gerrit/server/project/ChildProjects.java
index ce5ce2f323..5e723251a0 100644
--- a/java/com/google/gerrit/server/project/ChildProjects.java
+++ b/java/com/google/gerrit/server/project/ChildProjects.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.project;
import static java.util.stream.Collectors.toList;
import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.common.ProjectInfo;
@@ -54,7 +55,7 @@ public class ChildProjects {
/** Gets all child projects recursively. */
public List<ProjectInfo> list(Project.NameKey parent) throws PermissionBackendException {
Map<Project.NameKey, Project> projects = readAllReadableProjects();
- Multimap<Project.NameKey, Project.NameKey> children = parentToChildren(projects);
+ ListMultimap<Project.NameKey, Project.NameKey> children = parentToChildren(projects);
PermissionBackend.WithUser perm = permissionBackend.currentUser();
List<ProjectInfo> results = new ArrayList<>();
@@ -83,9 +84,9 @@ public class ChildProjects {
}
/** Map of parent project to direct child. */
- private Multimap<Project.NameKey, Project.NameKey> parentToChildren(
+ private ListMultimap<Project.NameKey, Project.NameKey> parentToChildren(
Map<Project.NameKey, Project> projects) {
- Multimap<Project.NameKey, Project.NameKey> m = ArrayListMultimap.create();
+ ListMultimap<Project.NameKey, Project.NameKey> m = ArrayListMultimap.create();
for (Map.Entry<Project.NameKey, Project> e : projects.entrySet()) {
if (!allProjects.equals(e.getKey())) {
m.put(e.getValue().getParent(allProjects), e.getKey());
diff --git a/java/com/google/gerrit/server/project/CommentLinkProvider.java b/java/com/google/gerrit/server/project/CommentLinkProvider.java
index 88f045e1d8..c799b56d88 100644
--- a/java/com/google/gerrit/server/project/CommentLinkProvider.java
+++ b/java/com/google/gerrit/server/project/CommentLinkProvider.java
@@ -35,14 +35,14 @@ import org.eclipse.jgit.lib.Config;
public class CommentLinkProvider implements Provider<List<CommentLinkInfo>>, GerritConfigListener {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- private volatile List<CommentLinkInfo> commentLinks;
+ private volatile ImmutableList<CommentLinkInfo> commentLinks;
@Inject
CommentLinkProvider(@GerritServerConfig Config cfg) {
this.commentLinks = parseConfig(cfg);
}
- private List<CommentLinkInfo> parseConfig(Config cfg) {
+ private ImmutableList<CommentLinkInfo> parseConfig(Config cfg) {
Set<String> subsections = cfg.getSubsections(ProjectConfig.COMMENTLINK);
ImmutableList.Builder<CommentLinkInfo> cls =
ImmutableList.builderWithExpectedSize(subsections.size());
diff --git a/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java b/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
index f054e849d5..aa0f87e3ab 100644
--- a/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
+++ b/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
@@ -17,6 +17,8 @@ package com.google.gerrit.server.project;
import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.AccountGroup.UUID;
import com.google.gerrit.entities.BooleanProjectConfig;
@@ -36,7 +38,6 @@ import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
@@ -89,7 +90,7 @@ public class ContributorAgreementsChecker {
}
IdentifiedUser iUser = user.asIdentifiedUser();
- Collection<ContributorAgreement> contributorAgreements =
+ ImmutableCollection<ContributorAgreement> contributorAgreements =
projectCache.getAllProjects().getConfig().getContributorAgreements().values();
List<UUID> okGroupIds = new ArrayList<>();
for (ContributorAgreement ca : contributorAgreements) {
@@ -97,14 +98,14 @@ public class ContributorAgreementsChecker {
groupIds = okGroupIds;
// matchProjects defaults to match all projects when missing.
- List<String> matchProjectsRegexes = ca.getMatchProjectsRegexes();
+ ImmutableList<String> matchProjectsRegexes = ca.getMatchProjectsRegexes();
if (!matchProjectsRegexes.isEmpty()
&& !projectMatchesAnyPattern(project.get(), matchProjectsRegexes)) {
// Doesn't match, isn't checked.
continue;
}
// excludeProjects defaults to exclude no projects when missing.
- List<String> excludeProjectsRegexes = ca.getExcludeProjectsRegexes();
+ ImmutableList<String> excludeProjectsRegexes = ca.getExcludeProjectsRegexes();
if (!excludeProjectsRegexes.isEmpty()
&& projectMatchesAnyPattern(project.get(), excludeProjectsRegexes)) {
// Matches, isn't checked.
diff --git a/java/com/google/gerrit/server/project/LabelConfigValidator.java b/java/com/google/gerrit/server/project/LabelConfigValidator.java
index 3e5ff6ba93..c4bc9fbfd9 100644
--- a/java/com/google/gerrit/server/project/LabelConfigValidator.java
+++ b/java/com/google/gerrit/server/project/LabelConfigValidator.java
@@ -24,16 +24,16 @@ import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.client.ChangeKind;
+import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.meta.VersionedConfigFile;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.git.validators.ValidationMessage;
import com.google.gerrit.server.patch.DiffNotAvailableException;
-import com.google.gerrit.server.patch.DiffOperations;
-import com.google.gerrit.server.patch.DiffOptions;
-import com.google.gerrit.server.patch.filediff.FileDiffOutput;
-import com.google.inject.Inject;
+import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
+import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.List;
@@ -103,11 +103,10 @@ public class LabelConfigValidator implements CommitValidationListener {
.put(KEY_COPY_ALL_SCORES_IF_LIST_OF_FILES_DID_NOT_CHANGE, "has:unchanged-files")
.build();
- private final DiffOperations diffOperations;
+ private final ApprovalQueryBuilder approvalQueryBuilder;
- @Inject
- public LabelConfigValidator(DiffOperations diffOperations) {
- this.diffOperations = diffOperations;
+ public LabelConfigValidator(ApprovalQueryBuilder approvalQueryBuilder) {
+ this.approvalQueryBuilder = approvalQueryBuilder;
}
@Override
@@ -143,93 +142,25 @@ public class LabelConfigValidator implements CommitValidationListener {
}
// Load the old config
- Optional<Config> oldConfig = loadOldConfig(receiveEvent);
+ @Nullable Config oldConfig = loadOldConfig(receiveEvent).orElse(null);
for (String labelName : newConfig.getSubsections(ProjectConfig.LABEL)) {
- for (String deprecatedFlag : DEPRECATED_FLAGS.keySet()) {
- if (flagChangedOrNewlySet(newConfig, oldConfig.orElse(null), labelName, deprecatedFlag)) {
- validationMessageBuilder.add(
- new CommitValidationMessage(
- String.format(
- "Parameter '%s.%s.%s' is deprecated and cannot be set,"
- + " use '%s' in '%s.%s.%s' instead.",
- ProjectConfig.LABEL,
- labelName,
- deprecatedFlag,
- DEPRECATED_FLAGS.get(deprecatedFlag),
- ProjectConfig.LABEL,
- labelName,
- ProjectConfig.KEY_COPY_CONDITION),
- ValidationMessage.Type.ERROR));
- }
- }
-
- if (copyValuesChangedOrNewlySet(newConfig, oldConfig.orElse(null), labelName)) {
- validationMessageBuilder.add(
- new CommitValidationMessage(
- String.format(
- "Parameter '%s.%s.%s' is deprecated and cannot be set,"
- + " use 'is:<copy-value>' in '%s.%s.%s' instead.",
- ProjectConfig.LABEL,
- labelName,
- KEY_COPY_VALUE,
- ProjectConfig.LABEL,
- labelName,
- ProjectConfig.KEY_COPY_CONDITION),
- ValidationMessage.Type.ERROR));
- }
-
- // Ban modifying label functions to any blocking function value
- if (flagChangedOrNewlySet(
- newConfig, oldConfig.orElse(null), labelName, ProjectConfig.KEY_FUNCTION)) {
- String fnName =
- newConfig.getString(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_FUNCTION);
- Optional<LabelFunction> labelFn = LabelFunction.parse(fnName);
- if (labelFn.isPresent() && !isLabelFunctionAllowed(labelFn.get())) {
- validationMessageBuilder.add(
- new CommitValidationMessage(
- String.format(
- "Value '%s' of '%s.%s.%s' is not allowed and cannot be set."
- + " Label functions can only be set to {%s, %s, %s}."
- + " Use submit requirements instead of label functions.",
- fnName,
- ProjectConfig.LABEL,
- labelName,
- ProjectConfig.KEY_FUNCTION,
- LabelFunction.NO_BLOCK,
- LabelFunction.NO_OP,
- LabelFunction.PATCH_SET_LOCK),
- ValidationMessage.Type.ERROR));
- }
- }
-
- // Ban deletions of label functions as well since the default is MaxWithBlock
- if (flagDeleted(newConfig, oldConfig.orElse(null), labelName, ProjectConfig.KEY_FUNCTION)) {
- validationMessageBuilder.add(
- new CommitValidationMessage(
- String.format(
- "Cannot delete '%s.%s.%s'."
- + " Label functions can only be set to {%s, %s, %s}."
- + " Use submit requirements instead of label functions.",
- ProjectConfig.LABEL,
- labelName,
- ProjectConfig.KEY_FUNCTION,
- LabelFunction.NO_BLOCK,
- LabelFunction.NO_OP,
- LabelFunction.PATCH_SET_LOCK),
- ValidationMessage.Type.ERROR));
- }
+ rejectSettingDeprecatedFlags(newConfig, oldConfig, labelName, validationMessageBuilder);
+ rejectSettingCopyValues(newConfig, oldConfig, labelName, validationMessageBuilder);
+ rejectSettingBlockingFunction(newConfig, oldConfig, labelName, validationMessageBuilder);
+ rejectDeletingFunction(newConfig, oldConfig, labelName, validationMessageBuilder);
+ rejectNonParseableCopyCondition(newConfig, oldConfig, labelName, validationMessageBuilder);
}
ImmutableList<CommitValidationMessage> validationMessages = validationMessageBuilder.build();
- if (!validationMessages.isEmpty()) {
+ if (validationMessages.stream().anyMatch(CommitValidationMessage::isError)) {
throw new CommitValidationException(
String.format(
"invalid %s file in revision %s",
ProjectConfig.PROJECT_CONFIG, receiveEvent.commit.getName()),
validationMessages);
}
- return ImmutableList.of();
+ return validationMessages;
} catch (IOException | DiffNotAvailableException e) {
String errorMessage =
String.format(
@@ -243,6 +174,149 @@ public class LabelConfigValidator implements CommitValidationListener {
}
}
+ private void rejectSettingDeprecatedFlags(
+ Config newConfig,
+ @Nullable Config oldConfig,
+ String labelName,
+ ImmutableList.Builder<CommitValidationMessage> validationMessageBuilder) {
+ for (String deprecatedFlag : DEPRECATED_FLAGS.keySet()) {
+ if (flagChangedOrNewlySet(newConfig, oldConfig, labelName, deprecatedFlag)) {
+ validationMessageBuilder.add(
+ new CommitValidationMessage(
+ String.format(
+ "Parameter '%s.%s.%s' is deprecated and cannot be set,"
+ + " use '%s' in '%s.%s.%s' instead.",
+ ProjectConfig.LABEL,
+ labelName,
+ deprecatedFlag,
+ DEPRECATED_FLAGS.get(deprecatedFlag),
+ ProjectConfig.LABEL,
+ labelName,
+ ProjectConfig.KEY_COPY_CONDITION),
+ ValidationMessage.Type.ERROR));
+ }
+ }
+ }
+
+ private void rejectSettingCopyValues(
+ Config newConfig,
+ @Nullable Config oldConfig,
+ String labelName,
+ ImmutableList.Builder<CommitValidationMessage> validationMessageBuilder) {
+ if (copyValuesChangedOrNewlySet(newConfig, oldConfig, labelName)) {
+ validationMessageBuilder.add(
+ new CommitValidationMessage(
+ String.format(
+ "Parameter '%s.%s.%s' is deprecated and cannot be set,"
+ + " use 'is:<copy-value>' in '%s.%s.%s' instead.",
+ ProjectConfig.LABEL,
+ labelName,
+ KEY_COPY_VALUE,
+ ProjectConfig.LABEL,
+ labelName,
+ ProjectConfig.KEY_COPY_CONDITION),
+ ValidationMessage.Type.ERROR));
+ }
+ }
+
+ private void rejectSettingBlockingFunction(
+ Config newConfig,
+ @Nullable Config oldConfig,
+ String labelName,
+ ImmutableList.Builder<CommitValidationMessage> validationMessageBuilder) {
+ if (flagChangedOrNewlySet(newConfig, oldConfig, labelName, ProjectConfig.KEY_FUNCTION)) {
+ String fnName =
+ newConfig.getString(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_FUNCTION);
+ Optional<LabelFunction> labelFn = LabelFunction.parse(fnName);
+ if (labelFn.isPresent() && !isLabelFunctionAllowed(labelFn.get())) {
+ validationMessageBuilder.add(
+ new CommitValidationMessage(
+ String.format(
+ "Value '%s' of '%s.%s.%s' is not allowed and cannot be set."
+ + " Label functions can only be set to {%s, %s, %s}."
+ + " Use submit requirements instead of label functions.",
+ fnName,
+ ProjectConfig.LABEL,
+ labelName,
+ ProjectConfig.KEY_FUNCTION,
+ LabelFunction.NO_BLOCK,
+ LabelFunction.NO_OP,
+ LabelFunction.PATCH_SET_LOCK),
+ ValidationMessage.Type.ERROR));
+ }
+ }
+ }
+
+ private void rejectDeletingFunction(
+ Config newConfig,
+ @Nullable Config oldConfig,
+ String labelName,
+ ImmutableList.Builder<CommitValidationMessage> validationMessageBuilder) {
+ // Ban deletion of label function since the default is MaxWithBlock which is deprecated
+ if (flagDeleted(newConfig, oldConfig, labelName, ProjectConfig.KEY_FUNCTION)) {
+ validationMessageBuilder.add(
+ new CommitValidationMessage(
+ String.format(
+ "Cannot delete '%s.%s.%s'."
+ + " Label functions can only be set to {%s, %s, %s}."
+ + " Use submit requirements instead of label functions.",
+ ProjectConfig.LABEL,
+ labelName,
+ ProjectConfig.KEY_FUNCTION,
+ LabelFunction.NO_BLOCK,
+ LabelFunction.NO_OP,
+ LabelFunction.PATCH_SET_LOCK),
+ ValidationMessage.Type.ERROR));
+ }
+ }
+
+ private void rejectNonParseableCopyCondition(
+ Config newConfig,
+ @Nullable Config oldConfig,
+ String labelName,
+ ImmutableList.Builder<CommitValidationMessage> validationMessageBuilder) {
+ if (flagChangedOrNewlySet(newConfig, oldConfig, labelName, ProjectConfig.KEY_COPY_CONDITION)) {
+ String copyCondition =
+ newConfig.getString(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_COPY_CONDITION);
+ try {
+ @SuppressWarnings("unused")
+ var unused = approvalQueryBuilder.parse(copyCondition);
+ } catch (QueryParseException e) {
+ validationMessageBuilder.add(
+ new CommitValidationMessage(
+ String.format(
+ "Cannot parse copy condition '%s' of label %s (parameter '%s.%s.%s'): %s",
+ copyCondition,
+ labelName,
+ ProjectConfig.LABEL,
+ labelName,
+ ProjectConfig.KEY_COPY_CONDITION,
+ e.getMessage()),
+ // if the old copy condition is not parseable allow updating it even if the new copy
+ // condition is also not parseable, only emit a warning in this case
+ hasUnparseableOldCopyCondition(oldConfig, labelName)
+ ? ValidationMessage.Type.WARNING
+ : ValidationMessage.Type.ERROR));
+ }
+ }
+ }
+
+ private boolean hasUnparseableOldCopyCondition(@Nullable Config oldConfig, String labelName) {
+ if (oldConfig == null) {
+ return false;
+ }
+
+ String oldCopyCondition =
+ oldConfig.getString(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_COPY_CONDITION);
+ try {
+ @SuppressWarnings("unused")
+ var unused = approvalQueryBuilder.parse(oldCopyCondition);
+ return false;
+ } catch (QueryParseException e) {
+ return true;
+ }
+ }
+
/**
* Whether the given file was changed in the given revision.
*
@@ -251,7 +325,7 @@ public class LabelConfigValidator implements CommitValidationListener {
*/
private boolean isFileChanged(CommitReceivedEvent receiveEvent, String fileName)
throws DiffNotAvailableException {
- Map<String, FileDiffOutput> fileDiffOutputs;
+ Map<String, ModifiedFile> fileDiffOutputs;
if (receiveEvent.commit.getParentCount() > 0) {
// normal commit with one parent: use listModifiedFilesAgainstParent with parentNum = 1 to
// compare against the only parent (using parentNum = 0 to compare against the default parent
@@ -260,23 +334,26 @@ public class LabelConfigValidator implements CommitValidationListener {
// = 1 to compare against the first parent (using parentNum = 0 would compare against the
// auto-merge)
fileDiffOutputs =
- diffOperations.listModifiedFilesAgainstParent(
- receiveEvent.getProjectNameKey(), receiveEvent.commit, 1, DiffOptions.DEFAULTS);
+ receiveEvent.diffOperations.loadModifiedFilesAgainstParentIfNecessary(
+ receiveEvent.getProjectNameKey(),
+ receiveEvent.commit,
+ 1,
+ /* enableRenameDetection= */ true);
} else {
// initial commit: must use listModifiedFilesAgainstParent with parentNum = 0
fileDiffOutputs =
- diffOperations.listModifiedFilesAgainstParent(
+ receiveEvent.diffOperations.loadModifiedFilesAgainstParentIfNecessary(
receiveEvent.getProjectNameKey(),
receiveEvent.commit,
/* parentNum=*/ 0,
- DiffOptions.DEFAULTS);
+ /* enableRenameDetection= */ true);
}
return fileDiffOutputs.keySet().contains(fileName);
}
private Config loadNewConfig(CommitReceivedEvent receiveEvent)
throws IOException, ConfigInvalidException {
- ProjectLevelConfig.Bare bareConfig = new ProjectLevelConfig.Bare(ProjectConfig.PROJECT_CONFIG);
+ VersionedConfigFile bareConfig = new VersionedConfigFile(ProjectConfig.PROJECT_CONFIG);
bareConfig.load(receiveEvent.project.getNameKey(), receiveEvent.revWalk, receiveEvent.commit);
return bareConfig.getConfig();
}
@@ -288,8 +365,7 @@ public class LabelConfigValidator implements CommitValidationListener {
}
try {
- ProjectLevelConfig.Bare bareConfig =
- new ProjectLevelConfig.Bare(ProjectConfig.PROJECT_CONFIG);
+ VersionedConfigFile bareConfig = new VersionedConfigFile(ProjectConfig.PROJECT_CONFIG);
bareConfig.load(
receiveEvent.project.getNameKey(),
receiveEvent.revWalk,
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 6c8087e0b0..a981c3c708 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -744,7 +744,7 @@ public class ProjectConfig extends VersionedMetaData implements ValidationError.
loadPatterns(rc, CONTRIBUTOR_AGREEMENT, name, KEY_EXCLUDE_PROJECTS));
ca.setMatchProjectsRegexes(loadPatterns(rc, CONTRIBUTOR_AGREEMENT, name, KEY_MATCH_PROJECTS));
- List<PermissionRule> rules =
+ ImmutableList<PermissionRule> rules =
loadPermissionRules(rc, CONTRIBUTOR_AGREEMENT, name, KEY_AUTO_VERIFY, false);
if (rules.isEmpty()) {
ca.setAutoVerify(null);
@@ -953,7 +953,7 @@ public class ProjectConfig extends VersionedMetaData implements ValidationError.
}
private static LabelValue parseLabelValue(String src) {
- List<String> parts =
+ ImmutableList<String> parts =
ImmutableList.copyOf(
Splitter.on(CharMatcher.whitespace()).omitEmptyStrings().limit(2).split(src));
if (parts.isEmpty()) {
@@ -1651,7 +1651,7 @@ public class ProjectConfig extends VersionedMetaData implements ValidationError.
rc.unset(LABEL, name, KEY_COPY_CONDITION);
}
- List<String> refPatterns = label.getRefPatterns();
+ ImmutableList<String> refPatterns = label.getRefPatterns();
if (refPatterns != null && !refPatterns.isEmpty()) {
rc.setStringList(LABEL, name, KEY_BRANCH, refPatterns);
} else {
diff --git a/java/com/google/gerrit/server/project/ProjectCreator.java b/java/com/google/gerrit/server/project/ProjectCreator.java
index 8bd18ffe66..71253eb96e 100644
--- a/java/com/google/gerrit/server/project/ProjectCreator.java
+++ b/java/com/google/gerrit/server/project/ProjectCreator.java
@@ -20,6 +20,8 @@ import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdate
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.AccessSection;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.BooleanProjectConfig;
@@ -36,6 +38,7 @@ import com.google.gerrit.git.LockFailureException;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.config.GerritInstanceId;
import com.google.gerrit.server.config.RepositoryConfig;
import com.google.gerrit.server.extensions.events.AbstractNoNotifyEvent;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
@@ -80,6 +83,7 @@ public class ProjectCreator {
private final Provider<PersonIdent> serverIdent;
private final Provider<IdentifiedUser> identifiedUser;
private final ProjectConfig.Factory projectConfigFactory;
+ private final String gerritInstanceId;
@Inject
ProjectCreator(
@@ -91,6 +95,7 @@ public class ProjectCreator {
GitReferenceUpdated referenceUpdated,
RepositoryConfig repositoryCfg,
@GerritPersonIdent Provider<PersonIdent> serverIdent,
+ @Nullable @GerritInstanceId String gerritInstanceId,
Provider<IdentifiedUser> identifiedUser,
ProjectConfig.Factory projectConfigFactory) {
this.repoManager = repoManager;
@@ -101,10 +106,12 @@ public class ProjectCreator {
this.referenceUpdated = referenceUpdated;
this.repositoryCfg = repositoryCfg;
this.serverIdent = serverIdent;
+ this.gerritInstanceId = gerritInstanceId;
this.identifiedUser = identifiedUser;
this.projectConfigFactory = projectConfigFactory;
}
+ @CanIgnoreReturnValue
public ProjectState createProject(CreateProjectArgs args)
throws BadRequestException, ResourceConflictException, IOException, ConfigInvalidException {
try (RefUpdateContext ctx = RefUpdateContext.open(INIT_REPO)) {
@@ -248,17 +255,19 @@ public class ProjectCreator {
return;
}
- ProjectCreator.Event event = new ProjectCreator.Event(name, head);
+ ProjectCreator.Event event = new ProjectCreator.Event(name, head, gerritInstanceId);
createdListeners.runEach(l -> l.onNewProjectCreated(event));
}
static class Event extends AbstractNoNotifyEvent implements NewProjectCreatedListener.Event {
private final Project.NameKey name;
private final String head;
+ private final String gerritInstanceId;
- Event(Project.NameKey name, String head) {
+ Event(Project.NameKey name, String head, @Nullable String gerritInstanceId) {
this.name = name;
this.head = head;
+ this.gerritInstanceId = gerritInstanceId;
}
@Override
@@ -270,5 +279,10 @@ public class ProjectCreator {
public String getHeadName() {
return head;
}
+
+ @Override
+ public String getInstanceId() {
+ return gerritInstanceId;
+ }
}
}
diff --git a/java/com/google/gerrit/server/project/ProjectLevelConfig.java b/java/com/google/gerrit/server/project/ProjectLevelConfig.java
index 825619829d..7ab76294e7 100644
--- a/java/com/google/gerrit/server/project/ProjectLevelConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectLevelConfig.java
@@ -18,60 +18,15 @@ import static java.util.stream.Collectors.toList;
import com.google.common.collect.Streams;
import com.google.gerrit.entities.ImmutableConfig;
-import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.server.git.meta.VersionedMetaData;
-import java.io.IOException;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
/** Configuration file in the projects refs/meta/config branch. */
public class ProjectLevelConfig {
- /**
- * This class is a low-level API that allows callers to read the config directly from a repository
- * and make updates to it.
- */
- public static class Bare extends VersionedMetaData {
- private final String fileName;
- @Nullable private Config cfg;
-
- public Bare(String fileName) {
- this.fileName = fileName;
- this.cfg = null;
- }
-
- public Config getConfig() {
- if (cfg == null) {
- cfg = new Config();
- }
- return cfg;
- }
-
- @Override
- protected String getRefName() {
- return RefNames.REFS_CONFIG;
- }
-
- @Override
- protected void onLoad() throws IOException, ConfigInvalidException {
- cfg = readConfig(fileName);
- }
-
- @Override
- protected boolean onSave(CommitBuilder commit) throws IOException {
- if (commit.getMessage() == null || "".equals(commit.getMessage())) {
- commit.setMessage("Updated configuration\n");
- }
- saveConfig(fileName, cfg);
- return true;
- }
- }
-
private final String fileName;
private final ProjectState project;
private final ImmutableConfig immutableConfig;
diff --git a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
index 4946bea530..a317361d10 100644
--- a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
@@ -257,7 +257,7 @@ public class ProjectsConsistencyChecker {
}
try {
- List<ChangeData> queryResult =
+ ImmutableList<ChangeData> queryResult =
retryHelper
.changeIndexQuery(
"projectsConsistencyCheckerQueryChanges",
@@ -273,34 +273,38 @@ public class ProjectsConsistencyChecker {
// Skip changes that we have already processed, either by this query or by
// earlier queries.
if (seenChanges.add(autoCloseableChange.getId())) {
- retryHelper
- .changeUpdate(
- "projectsConsistencyCheckerAutoCloseChanges",
- () -> {
- // Auto-close by change
- if (changeIdToMergedSha1.containsKey(autoCloseableChange.change().getKey())) {
- autoCloseableChangesByBranch.add(
- changeJson(
- fix,
- changeIdToMergedSha1.get(autoCloseableChange.change().getKey()))
- .format(autoCloseableChange));
- return null;
- }
-
- // Auto-close by commit
- for (ObjectId patchSetSha1 :
- autoCloseableChange.patchSets().stream()
- .map(PatchSet::commitId)
- .collect(toSet())) {
- if (mergedSha1s.contains(patchSetSha1)) {
- autoCloseableChangesByBranch.add(
- changeJson(fix, patchSetSha1).format(autoCloseableChange));
- break;
- }
- }
- return null;
- })
- .call();
+ @SuppressWarnings("unused")
+ var unused =
+ retryHelper
+ .changeUpdate(
+ "projectsConsistencyCheckerAutoCloseChanges",
+ () -> {
+ // Auto-close by change
+ if (changeIdToMergedSha1.containsKey(
+ autoCloseableChange.change().getKey())) {
+ autoCloseableChangesByBranch.add(
+ changeJson(
+ fix,
+ changeIdToMergedSha1.get(
+ autoCloseableChange.change().getKey()))
+ .format(autoCloseableChange));
+ return null;
+ }
+
+ // Auto-close by commit
+ for (ObjectId patchSetSha1 :
+ autoCloseableChange.patchSets().stream()
+ .map(PatchSet::commitId)
+ .collect(toSet())) {
+ if (mergedSha1s.contains(patchSetSha1)) {
+ autoCloseableChangesByBranch.add(
+ changeJson(fix, patchSetSha1).format(autoCloseableChange));
+ break;
+ }
+ }
+ return null;
+ })
+ .call();
}
}
diff --git a/java/com/google/gerrit/server/project/PrologRulesWarningValidator.java b/java/com/google/gerrit/server/project/PrologRulesWarningValidator.java
index 5683fe7974..e3de7631b5 100644
--- a/java/com/google/gerrit/server/project/PrologRulesWarningValidator.java
+++ b/java/com/google/gerrit/server/project/PrologRulesWarningValidator.java
@@ -26,10 +26,7 @@ import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.git.validators.ValidationMessage;
import com.google.gerrit.server.patch.DiffNotAvailableException;
-import com.google.gerrit.server.patch.DiffOperations;
-import com.google.gerrit.server.patch.DiffOptions;
-import com.google.gerrit.server.patch.filediff.FileDiffOutput;
-import com.google.inject.Inject;
+import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
import com.google.inject.Singleton;
import java.util.List;
import java.util.Map;
@@ -43,13 +40,6 @@ import java.util.stream.Collectors;
public class PrologRulesWarningValidator implements CommitValidationListener {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- private final DiffOperations diffOperations;
-
- @Inject
- public PrologRulesWarningValidator(DiffOperations diffOperations) {
- this.diffOperations = diffOperations;
- }
-
@Override
public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
throws CommitValidationException {
@@ -70,13 +60,13 @@ public class PrologRulesWarningValidator implements CommitValidationListener {
private boolean isFileAdded(CommitReceivedEvent receiveEvent, String fileName)
throws DiffNotAvailableException {
- List<Map.Entry<String, FileDiffOutput>> matchingEntries =
- diffOperations
- .listModifiedFilesAgainstParent(
+ List<Map.Entry<String, ModifiedFile>> matchingEntries =
+ receiveEvent.diffOperations
+ .loadModifiedFilesAgainstParentIfNecessary(
receiveEvent.project.getNameKey(),
receiveEvent.commit,
/* parentNum=*/ 0,
- DiffOptions.DEFAULTS)
+ /* enableRenameDetection= */ true)
.entrySet().stream()
.filter(e -> fileName.equals(e.getKey()))
.collect(Collectors.toList());
diff --git a/java/com/google/gerrit/server/project/Reachable.java b/java/com/google/gerrit/server/project/Reachable.java
index c935adf079..c31cd35ec2 100644
--- a/java/com/google/gerrit/server/project/Reachable.java
+++ b/java/com/google/gerrit/server/project/Reachable.java
@@ -77,7 +77,7 @@ public class Reachable {
.orElseGet(() -> permissionBackend.currentUser())
.project(project)
.filter(refs, repo, RefFilterOptions.defaults());
- Collection<RevCommit> visible = new ArrayList<>();
+ List<RevCommit> visible = new ArrayList<>();
for (Ref r : filtered) {
try {
visible.add(rw.parseCommit(r.getObjectId()));
diff --git a/java/com/google/gerrit/server/project/RefPatternMatcher.java b/java/com/google/gerrit/server/project/RefPatternMatcher.java
index 798838e26a..9249e3675c 100644
--- a/java/com/google/gerrit/server/project/RefPatternMatcher.java
+++ b/java/com/google/gerrit/server/project/RefPatternMatcher.java
@@ -111,7 +111,7 @@ public abstract class RefPatternMatcher {
// allows the pattern prefix to be clipped, saving time on
// evaluation.
String replacement = ":PLACEHOLDER:";
- Map<String, String> params =
+ ImmutableMap<String, String> params =
ImmutableMap.of(
RefPattern.USERID_SHARDED, replacement,
RefPattern.USERNAME, replacement);
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementConfigValidator.java b/java/com/google/gerrit/server/project/SubmitRequirementConfigValidator.java
index 6366a1411f..e715aca2cd 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementConfigValidator.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementConfigValidator.java
@@ -23,8 +23,6 @@ import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.git.validators.ValidationMessage;
import com.google.gerrit.server.patch.DiffNotAvailableException;
-import com.google.gerrit.server.patch.DiffOperations;
-import com.google.gerrit.server.patch.DiffOptions;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.List;
@@ -42,16 +40,13 @@ import org.eclipse.jgit.errors.ConfigInvalidException;
* {@link ProjectConfig} is cached in the project cache).
*/
public class SubmitRequirementConfigValidator implements CommitValidationListener {
- private final DiffOperations diffOperations;
private final ProjectConfig.Factory projectConfigFactory;
private final SubmitRequirementExpressionsValidator submitRequirementExpressionsValidator;
@Inject
SubmitRequirementConfigValidator(
- DiffOperations diffOperations,
ProjectConfig.Factory projectConfigFactory,
SubmitRequirementExpressionsValidator submitRequirementExpressionsValidator) {
- this.diffOperations = diffOperations;
this.projectConfigFactory = projectConfigFactory;
this.submitRequirementExpressionsValidator = submitRequirementExpressionsValidator;
}
@@ -116,12 +111,12 @@ public class SubmitRequirementConfigValidator implements CommitValidationListene
*/
private boolean isFileChanged(CommitReceivedEvent receiveEvent, String fileName)
throws DiffNotAvailableException {
- return diffOperations
- .listModifiedFilesAgainstParent(
+ return receiveEvent.diffOperations
+ .loadModifiedFilesAgainstParentIfNecessary(
receiveEvent.project.getNameKey(),
receiveEvent.commit,
/* parentNum=*/ 0,
- DiffOptions.DEFAULTS)
+ /* enableRenameDetection= */ true)
.keySet().stream()
.anyMatch(fileName::equals);
}
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java b/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
index 39ba8b40af..7f73cd334d 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
@@ -50,7 +50,7 @@ public class SubmitRequirementsAdapter {
* Retrieve legacy submit records (created by label functions and other {@link
* com.google.gerrit.server.rules.SubmitRule}s) and convert them to submit requirement results.
*/
- public static Map<SubmitRequirement, SubmitRequirementResult> getLegacyRequirements(
+ public static ImmutableMap<SubmitRequirement, SubmitRequirementResult> getLegacyRequirements(
ChangeData cd) {
// We use SubmitRuleOptions.defaults() which does not recompute submit rules for closed changes.
// This doesn't have an effect since we never call this class (i.e. to evaluate submit
@@ -105,9 +105,9 @@ public class SubmitRequirementsAdapter {
}
@VisibleForTesting
- static List<SubmitRequirementResult> createResult(
+ static ImmutableList<SubmitRequirementResult> createResult(
SubmitRecord record, List<LabelType> labelTypes, ObjectId psCommitId, boolean isForced) {
- List<SubmitRequirementResult> results;
+ ImmutableList<SubmitRequirementResult> results;
if (record.ruleName != null && record.ruleName.equals(DefaultSubmitRule.RULE_NAME)) {
results = createFromDefaultSubmitRecord(record.labels, labelTypes, psCommitId, isForced);
} else {
@@ -117,7 +117,7 @@ public class SubmitRequirementsAdapter {
return results;
}
- private static List<SubmitRequirementResult> createFromDefaultSubmitRecord(
+ private static ImmutableList<SubmitRequirementResult> createFromDefaultSubmitRecord(
@Nullable List<Label> labels,
List<LabelType> labelTypes,
ObjectId psCommitId,
@@ -159,7 +159,7 @@ public class SubmitRequirementsAdapter {
return result.build();
}
- private static List<SubmitRequirementResult> createFromCustomSubmitRecord(
+ private static ImmutableList<SubmitRequirementResult> createFromCustomSubmitRecord(
SubmitRecord record, ObjectId psCommitId, boolean isForced) {
String ruleName = record.ruleName != null ? record.ruleName : "Custom-Rule";
if (record.labels == null || record.labels.isEmpty()) {
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
index 77776781a5..d4db78a546 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
@@ -27,6 +27,9 @@ import com.google.gerrit.entities.SubmitRequirementExpressionResult.PredicateRes
import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.SubmitRequirementChangeQueryBuilder;
@@ -83,7 +86,8 @@ public class SubmitRequirementsEvaluatorImpl implements SubmitRequirementsEvalua
public void validateExpression(SubmitRequirementExpression expression)
throws QueryParseException {
try (ManualRequestContext ignored = requestContext.open()) {
- queryBuilder.get().parse(expression.expressionString());
+ @SuppressWarnings("unused")
+ var unused = queryBuilder.get().parse(expression.expressionString());
}
}
@@ -180,31 +184,36 @@ public class SubmitRequirementsEvaluatorImpl implements SubmitRequirementsEvalua
* SubmitRequirement#allowOverrideInChildProjects} of global {@link SubmitRequirement}.
*/
private ImmutableMap<SubmitRequirement, SubmitRequirementResult> getRequirements(ChangeData cd) {
- Map<String, SubmitRequirement> globalRequirements = getGlobalRequirements();
-
- ProjectState state = projectCache.get(cd.project()).orElseThrow(illegalState(cd.project()));
- Map<String, SubmitRequirement> projectConfigRequirements = state.getSubmitRequirements();
-
- ImmutableMap<String, SubmitRequirement> requirements =
- Stream.concat(
- globalRequirements.entrySet().stream(),
- projectConfigRequirements.entrySet().stream())
- .collect(
- toImmutableMap(
- Map.Entry::getKey,
- Map.Entry::getValue,
- (globalSubmitRequirement, projectConfigRequirement) ->
- // Override with projectConfigRequirement if allowed by
- // globalSubmitRequirement configuration
- globalSubmitRequirement.allowOverrideInChildProjects()
- ? projectConfigRequirement
- : globalSubmitRequirement));
- ImmutableMap.Builder<SubmitRequirement, SubmitRequirementResult> results =
- ImmutableMap.builder();
- for (SubmitRequirement requirement : requirements.values()) {
- results.put(requirement, evaluateRequirementInternal(requirement, cd));
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Get submit requirements",
+ Metadata.builder().changeId(cd.change().getId().get()).build())) {
+ ImmutableMap<String, SubmitRequirement> globalRequirements = getGlobalRequirements();
+
+ ProjectState state = projectCache.get(cd.project()).orElseThrow(illegalState(cd.project()));
+ Map<String, SubmitRequirement> projectConfigRequirements = state.getSubmitRequirements();
+
+ ImmutableMap<String, SubmitRequirement> requirements =
+ Stream.concat(
+ globalRequirements.entrySet().stream(),
+ projectConfigRequirements.entrySet().stream())
+ .collect(
+ toImmutableMap(
+ Map.Entry::getKey,
+ Map.Entry::getValue,
+ (globalSubmitRequirement, projectConfigRequirement) ->
+ // Override with projectConfigRequirement if allowed by
+ // globalSubmitRequirement configuration
+ globalSubmitRequirement.allowOverrideInChildProjects()
+ ? projectConfigRequirement
+ : globalSubmitRequirement));
+ ImmutableMap.Builder<SubmitRequirement, SubmitRequirementResult> results =
+ ImmutableMap.builder();
+ for (SubmitRequirement requirement : requirements.values()) {
+ results.put(requirement, evaluateRequirementInternal(requirement, cd));
+ }
+ return results.build();
}
- return results.build();
}
/**
@@ -212,7 +221,7 @@ public class SubmitRequirementsEvaluatorImpl implements SubmitRequirementsEvalua
*
* <p>The global {@link SubmitRequirement}s apply to all projects and can be bound by plugins.
*/
- private Map<String, SubmitRequirement> getGlobalRequirements() {
+ private ImmutableMap<String, SubmitRequirement> getGlobalRequirements() {
return globalSubmitRequirements.stream()
.collect(
toImmutableMap(
diff --git a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index 866ce1444c..aa2c297504 100644
--- a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -15,10 +15,9 @@
package com.google.gerrit.server.project;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.gerrit.server.project.ProjectCache.illegalState;
import com.google.common.collect.Streams;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.entities.SubmitTypeRecord;
import com.google.gerrit.exceptions.StorageException;
@@ -30,6 +29,9 @@ import com.google.gerrit.metrics.Timer0;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.index.OnlineReindexMode;
import com.google.gerrit.server.logging.CallerFinder;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.rules.DefaultSubmitRule;
@@ -46,8 +48,6 @@ import java.util.Optional;
* the results through rules found in the parent projects, all the way up to All-Projects.
*/
public class SubmitRuleEvaluator {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
public interface Factory {
/** Returns a new {@link SubmitRuleEvaluator} with the specified options */
SubmitRuleEvaluator create(SubmitRuleOptions options);
@@ -102,6 +102,7 @@ public class SubmitRuleEvaluator {
.addTarget(ChangeJson.class)
.addTarget(ChangeData.class)
.addTarget(SubmitRequirementsEvaluatorImpl.class)
+ .addTarget(SubmitRequirementsAdapter.class)
.build();
}
@@ -113,10 +114,14 @@ public class SubmitRuleEvaluator {
* @param cd ChangeData to evaluate
*/
public List<SubmitRecord> evaluate(ChangeData cd) {
- logger.atFine().log(
- "Evaluate submit rules for change %d (caller: %s)",
- cd.change().getId().get(), callerFinder.findCallerLazy());
- try (Timer0.Context ignored = metrics.submitRuleEvaluationLatency.start()) {
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Evaluate submit rules",
+ Metadata.builder()
+ .changeId(cd.change().getId().get())
+ .caller(callerFinder.findCaller())
+ .build());
+ Timer0.Context ignored = metrics.submitRuleEvaluationLatency.start()) {
if (cd.change() == null) {
throw new StorageException("Change not found");
}
@@ -151,7 +156,7 @@ public class SubmitRuleEvaluator {
// Skip evaluating the default submit rule if the project has prolog rules.
// Note that in this case, the prolog submit rule will handle labels for us
.filter(
- projectState.hasPrologRules()
+ projectState.hasPrologRules() && prologSubmitRuleUtil.isProjectRulesEnabled()
? rule -> !(rule.get() instanceof DefaultSubmitRule)
: rule -> true)
.map(
@@ -180,14 +185,10 @@ public class SubmitRuleEvaluator {
*/
public SubmitTypeRecord getSubmitType(ChangeData cd) {
try (Timer0.Context ignored = metrics.submitTypeEvaluationLatency.start()) {
- try {
- Project.NameKey name = cd.project();
- Optional<ProjectState> project = projectCache.get(name);
- if (!project.isPresent()) {
- throw new NoSuchProjectException(name);
- }
- } catch (NoSuchProjectException e) {
- throw new IllegalStateException("Unable to find project while evaluating submit rule", e);
+ ProjectState projectState =
+ projectCache.get(cd.project()).orElseThrow(illegalState(cd.project()));
+ if (!prologSubmitRuleUtil.isProjectRulesEnabled()) {
+ return SubmitTypeRecord.OK(projectState.getSubmitType());
}
return prologSubmitRuleUtil.getSubmitType(cd);
diff --git a/java/com/google/gerrit/server/project/package-info.java b/java/com/google/gerrit/server/project/package-info.java
new file mode 100644
index 0000000000..52df3e269c
--- /dev/null
+++ b/java/com/google/gerrit/server/project/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.project;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/project/testing/BUILD b/java/com/google/gerrit/server/project/testing/BUILD
index ab75ec70b8..bf448abda6 100644
--- a/java/com/google/gerrit/server/project/testing/BUILD
+++ b/java/com/google/gerrit/server/project/testing/BUILD
@@ -5,5 +5,8 @@ java_library(
testonly = True,
srcs = glob(["*.java"]),
visibility = ["//visibility:public"],
- deps = ["//java/com/google/gerrit/entities"],
+ deps = [
+ "//java/com/google/gerrit/entities",
+ "//lib/errorprone:annotations",
+ ],
)
diff --git a/java/com/google/gerrit/server/project/testing/package-info.java b/java/com/google/gerrit/server/project/testing/package-info.java
new file mode 100644
index 0000000000..bae7ec415c
--- /dev/null
+++ b/java/com/google/gerrit/server/project/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.project.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java b/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
index 9df01f4398..a5a73b2764 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
@@ -25,7 +25,6 @@ import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryProcessor;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountLimits;
import com.google.gerrit.server.account.AccountState;
@@ -44,7 +43,6 @@ import com.google.inject.Singleton;
*/
public class AccountQueryProcessor extends QueryProcessor<AccountState> {
private final AccountControl.Factory accountControlFactory;
- private final Sequences sequences;
private final IndexConfig indexConfig;
@Singleton
@@ -70,8 +68,7 @@ public class AccountQueryProcessor extends QueryProcessor<AccountState> {
IndexConfig indexConfig,
AccountIndexCollection indexes,
AccountIndexRewriter rewriter,
- AccountControl.Factory accountControlFactory,
- Sequences sequences) {
+ AccountControl.Factory accountControlFactory) {
super(
accountQueryMetrics,
AccountSchemaDefinitions.INSTANCE,
@@ -81,7 +78,6 @@ public class AccountQueryProcessor extends QueryProcessor<AccountState> {
FIELD_LIMIT,
() -> limitsFactory.create(userProvider.get()).getQueryLimit());
this.accountControlFactory = accountControlFactory;
- this.sequences = sequences;
this.indexConfig = indexConfig;
}
@@ -97,14 +93,4 @@ public class AccountQueryProcessor extends QueryProcessor<AccountState> {
protected String formatForLogging(AccountState accountState) {
return accountState.account().id().toString();
}
-
- @Override
- protected int getIndexSize() {
- return sequences.lastAccountId();
- }
-
- @Override
- protected int getBatchSize() {
- return sequences.accountBatchSize();
- }
}
diff --git a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
index fa1758a8a1..e5864779a2 100644
--- a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
+++ b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -21,6 +21,7 @@ import com.google.common.base.Joiner;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
@@ -124,7 +125,7 @@ public class InternalAccountQuery extends InternalQuery<AccountState, InternalAc
if (hasPreferredEmailExact()) {
List<List<AccountState>> r =
query(emails.stream().map(AccountPredicates::preferredEmailExact).collect(toList()));
- Multimap<String, AccountState> accountsByEmail = ArrayListMultimap.create();
+ ListMultimap<String, AccountState> accountsByEmail = ArrayListMultimap.create();
for (int i = 0; i < emails.size(); i++) {
accountsByEmail.putAll(emails.get(i), r.get(i));
}
@@ -137,7 +138,7 @@ public class InternalAccountQuery extends InternalQuery<AccountState, InternalAc
List<List<AccountState>> r =
query(emails.stream().map(AccountPredicates::preferredEmail).collect(toList()));
- Multimap<String, AccountState> accountsByEmail = ArrayListMultimap.create();
+ ListMultimap<String, AccountState> accountsByEmail = ArrayListMultimap.create();
for (int i = 0; i < emails.size(); i++) {
String email = emails.get(i);
Set<AccountState> matchingAccounts =
diff --git a/java/com/google/gerrit/server/query/account/package-info.java b/java/com/google/gerrit/server/query/account/package-info.java
new file mode 100644
index 0000000000..a4d76eecc8
--- /dev/null
+++ b/java/com/google/gerrit/server/query/account/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.query.account;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/query/approval/ApprovalContext.java b/java/com/google/gerrit/server/query/approval/ApprovalContext.java
index 901c51f65e..971996d9ef 100644
--- a/java/com/google/gerrit/server/query/approval/ApprovalContext.java
+++ b/java/com/google/gerrit/server/query/approval/ApprovalContext.java
@@ -22,8 +22,7 @@ import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.extensions.client.ChangeKind;
import com.google.gerrit.server.notedb.ChangeNotes;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.revwalk.RevWalk;
+import com.google.gerrit.server.update.RepoView;
/** Entity representing all required information to match predicates for copying approvals. */
@AutoValue
@@ -53,11 +52,7 @@ public abstract class ApprovalContext {
/** Whether the new patch set is a merge commit. */
public abstract boolean isMerge();
- /** {@link RevWalk} of the repository for the current commit. */
- public abstract RevWalk revWalk();
-
- /** {@link RevWalk} of the repository for the current commit. */
- public abstract Config repoConfig();
+ public abstract RepoView repoView();
public static ApprovalContext create(
ChangeNotes changeNotes,
@@ -68,8 +63,7 @@ public abstract class ApprovalContext {
PatchSet targetPatchSet,
ChangeKind changeKind,
boolean isMerge,
- RevWalk revWalk,
- Config repoConfig) {
+ RepoView repoView) {
checkState(
sourcePatchSetId.changeId().equals(targetPatchSet.id().changeId()),
"approval and target must be the same change. got: %s, %s",
@@ -90,7 +84,6 @@ public abstract class ApprovalContext {
changeNotes,
changeKind,
isMerge,
- revWalk,
- repoConfig);
+ repoView);
}
}
diff --git a/java/com/google/gerrit/server/query/approval/ApprovalQueryBuilder.java b/java/com/google/gerrit/server/query/approval/ApprovalQueryBuilder.java
index ed876c1a24..6ae47adecd 100644
--- a/java/com/google/gerrit/server/query/approval/ApprovalQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/approval/ApprovalQueryBuilder.java
@@ -27,10 +27,12 @@ import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.group.GroupResolver;
import com.google.inject.Inject;
+import com.google.inject.Singleton;
import java.util.Arrays;
import java.util.Locale;
import java.util.Optional;
+@Singleton
public class ApprovalQueryBuilder extends QueryBuilder<ApprovalContext, ApprovalQueryBuilder> {
private static final QueryBuilder.Definition<ApprovalContext, ApprovalQueryBuilder> mydef =
new QueryBuilder.Definition<>(ApprovalQueryBuilder.class);
diff --git a/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java b/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
index 958011c848..3b3a2ddf90 100644
--- a/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
+++ b/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
@@ -21,9 +21,9 @@ import com.google.gerrit.entities.Project;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.InMemoryInserter;
import com.google.gerrit.server.patch.DiffNotAvailableException;
import com.google.gerrit.server.patch.DiffOperations;
-import com.google.gerrit.server.patch.DiffOptions;
import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -34,6 +34,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -57,31 +58,31 @@ public class ListOfFilesUnchangedPredicate extends ApprovalPredicate {
Integer parentNum =
isInitialCommit(ctx.changeNotes().getProjectName(), targetPatchSet.commitId()) ? 0 : 1;
- try {
+ try (ObjectInserter ins = new InMemoryInserter(ctx.repoView().getRevWalk().getObjectReader())) {
Map<String, ModifiedFile> baseVsCurrent =
- diffOperations.loadModifiedFilesAgainstParent(
+ diffOperations.loadModifiedFilesAgainstParentIfNecessary(
ctx.changeNotes().getProjectName(),
targetPatchSet.commitId(),
parentNum,
- DiffOptions.DEFAULTS,
- ctx.revWalk(),
- ctx.repoConfig());
+ ctx.repoView(),
+ ins,
+ /* enableRenameDetection= */ false);
Map<String, ModifiedFile> baseVsPrior =
- diffOperations.loadModifiedFilesAgainstParent(
+ diffOperations.loadModifiedFilesAgainstParentIfNecessary(
ctx.changeNotes().getProjectName(),
sourcePatchSet.commitId(),
parentNum,
- DiffOptions.DEFAULTS,
- ctx.revWalk(),
- ctx.repoConfig());
+ ctx.repoView(),
+ ins,
+ /* enableRenameDetection= */ false);
Map<String, ModifiedFile> priorVsCurrent =
- diffOperations.loadModifiedFiles(
+ diffOperations.loadModifiedFilesIfNecessary(
ctx.changeNotes().getProjectName(),
sourcePatchSet.commitId(),
targetPatchSet.commitId(),
- DiffOptions.DEFAULTS,
- ctx.revWalk(),
- ctx.repoConfig());
+ ctx.repoView().getRevWalk(),
+ ctx.repoView().getConfig(),
+ /* enableRenameDetection= */ false);
return match(baseVsCurrent, baseVsPrior, priorVsCurrent);
} catch (DiffNotAvailableException ex) {
throw new StorageException(
diff --git a/java/com/google/gerrit/server/query/approval/package-info.java b/java/com/google/gerrit/server/query/approval/package-info.java
new file mode 100644
index 0000000000..fc0ad07b44
--- /dev/null
+++ b/java/com/google/gerrit/server/query/approval/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.query.approval;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 4718a69041..d153bc9442 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -36,6 +36,7 @@ import com.google.common.collect.SetMultimap;
import com.google.common.collect.Table;
import com.google.common.flogger.FluentLogger;
import com.google.common.primitives.Ints;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.Account;
@@ -49,7 +50,6 @@ import com.google.gerrit.entities.LabelTypes;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.Project.NameKey;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.RobotComment;
import com.google.gerrit.entities.SubmitRecord;
@@ -167,7 +167,8 @@ public class ChangeData {
}
for (ChangeData cd : changes) {
- cd.change();
+ @SuppressWarnings("unused")
+ var unused = cd.change();
}
}
@@ -178,7 +179,8 @@ public class ChangeData {
}
for (ChangeData cd : changes) {
- cd.patchSets();
+ @SuppressWarnings("unused")
+ var unused = cd.patchSets();
}
}
@@ -189,7 +191,8 @@ public class ChangeData {
}
for (ChangeData cd : changes) {
- cd.currentPatchSet();
+ @SuppressWarnings("unused")
+ var unused = cd.currentPatchSet();
}
}
@@ -200,7 +203,8 @@ public class ChangeData {
}
for (ChangeData cd : changes) {
- cd.currentApprovals();
+ @SuppressWarnings("unused")
+ var unused = cd.currentApprovals();
}
}
@@ -211,7 +215,8 @@ public class ChangeData {
}
for (ChangeData cd : changes) {
- cd.messages();
+ @SuppressWarnings("unused")
+ var unused = cd.messages();
}
}
@@ -227,7 +232,8 @@ public class ChangeData {
ensureAllPatchSetsLoaded(pending);
ensureMessagesLoaded(pending);
for (ChangeData cd : pending) {
- cd.reviewedBy();
+ @SuppressWarnings("unused")
+ var unused = cd.reviewedBy();
}
}
}
@@ -437,7 +443,7 @@ public class ChangeData {
private ImmutableList<Account.Id> stars;
private Account.Id starredBy;
- private ImmutableMap<Account.Id, Ref> starRefs;
+ private ImmutableList<Account.Id> starAccounts;
private ReviewerSet reviewers;
private ReviewerByEmailSet reviewersByEmail;
private ReviewerSet pendingReviewers;
@@ -451,7 +457,7 @@ public class ChangeData {
private Integer totalCommentCount;
private LabelTypes labelTypes;
private Optional<Instant> mergedOn;
- private ImmutableSetMultimap<NameKey, RefState> refStates;
+ private ImmutableSetMultimap<Project.NameKey, RefState> refStates;
private ImmutableList<byte[]> refStatePatterns;
private String changeServerId;
private ChangeNumberVirtualIdAlgorithm virtualIdFunc;
@@ -528,6 +534,7 @@ public class ChangeData {
* lazyLoad} is on, the {@code ChangeData} object will load from the database ("lazily") when a
* field accessor is called.
*/
+ @CanIgnoreReturnValue
public ChangeData setStorageConstraint(StorageConstraint storageConstraint) {
this.storageConstraint = storageConstraint;
return this;
@@ -655,7 +662,7 @@ public class ChangeData {
}
for (ChangeData cd : changes) {
- cd.changeServerId();
+ var unused = cd.changeServerId();
}
}
@@ -687,7 +694,9 @@ public class ChangeData {
return BranchNameKey.create(project, branch);
}
throwIfNotLazyLoad("branch");
- change();
+
+ @SuppressWarnings("unused")
+ var unused = change();
}
return change.getDest();
}
@@ -698,36 +707,49 @@ public class ChangeData {
return isPrivate;
}
throwIfNotLazyLoad("isPrivate");
- change();
+
+ @SuppressWarnings("unused")
+ var unused = change();
}
return change.isPrivate();
}
+ @CanIgnoreReturnValue
public ChangeData setMetaRevision(ObjectId metaRevision) {
this.metaRevision = metaRevision;
return this;
}
- public ObjectId metaRevisionOrThrow() {
+ public Optional<ObjectId> metaRevision() {
if (notes == null) {
if (metaRevision != null) {
- return metaRevision;
+ return Optional.of(metaRevision);
}
if (refStates != null) {
- Set<RefState> refs = refStates.get(project);
+ ImmutableSet<RefState> refs = refStates.get(project);
if (refs != null) {
String metaRef = RefNames.changeMetaRef(getId());
for (RefState r : refs) {
if (r.ref().equals(metaRef)) {
- return r.id();
+ return Optional.of(r.id());
}
}
}
}
- throwIfNotLazyLoad("metaRevision");
- notes();
+ if (!lazyload()) {
+ return Optional.empty();
+ }
+
+ @SuppressWarnings("unused")
+ var unused = notes();
}
- return notes.getRevision();
+ metaRevision = notes.getRevision();
+ return Optional.of(metaRevision);
+ }
+
+ public ObjectId metaRevisionOrThrow() {
+ return metaRevision()
+ .orElseThrow(() -> new IllegalStateException("'metaRevision' field not populated"));
}
boolean fastIsVisibleTo(CurrentUser user) {
@@ -738,6 +760,7 @@ public class ChangeData {
visibleTo = user;
}
+ @Nullable
public Change change() {
if (change == null && lazyload()) {
loadChange();
@@ -749,11 +772,13 @@ public class ChangeData {
change = c;
}
+ @CanIgnoreReturnValue
public Change reloadChange() {
metaRevision = null;
return loadChange();
}
+ @CanIgnoreReturnValue
private Change loadChange() {
try {
notes = notesFactory.createChecked(project, legacyId, metaRevision);
@@ -782,6 +807,7 @@ public class ChangeData {
}
notes = notesFactory.create(project(), legacyId, metaRevision);
change = notes.getChange();
+ setPatchSets(null);
}
return notes;
}
@@ -1166,7 +1192,7 @@ public class ChangeData {
*/
public Map<SubmitRequirement, SubmitRequirementResult> submitRequirementsIncludingLegacy() {
Map<SubmitRequirement, SubmitRequirementResult> projectConfigReqs = submitRequirements();
- Map<SubmitRequirement, SubmitRequirementResult> legacyReqs =
+ ImmutableMap<SubmitRequirement, SubmitRequirementResult> legacyReqs =
SubmitRequirementsAdapter.getLegacyRequirements(this);
return submitRequirementsUtil.mergeLegacyAndNonLegacyRequirements(
projectConfigReqs, legacyReqs, this);
@@ -1432,7 +1458,7 @@ public class ChangeData {
if (!lazyload()) {
return ImmutableList.of();
}
- return starRefs().keySet().asList();
+ return starAccounts();
}
return stars;
}
@@ -1441,14 +1467,14 @@ public class ChangeData {
this.stars = ImmutableList.copyOf(accountIds);
}
- private ImmutableMap<Account.Id, Ref> starRefs() {
- if (starRefs == null) {
+ private ImmutableList<Account.Id> starAccounts() {
+ if (starAccounts == null) {
if (!lazyload()) {
- return ImmutableMap.of();
+ return ImmutableList.of();
}
- starRefs = requireNonNull(starredChangesReader).byChange(virtualId());
+ starAccounts = requireNonNull(starredChangesReader).byChange(virtualId());
}
- return starRefs;
+ return starAccounts;
}
public boolean isStarred(Account.Id accountId) {
@@ -1509,13 +1535,14 @@ public class ChangeData {
}
}
- public SetMultimap<NameKey, RefState> getRefStates() {
+ public SetMultimap<Project.NameKey, RefState> getRefStates() {
if (refStates == null) {
if (!lazyload()) {
return ImmutableSetMultimap.of();
}
- ImmutableSetMultimap.Builder<NameKey, RefState> result = ImmutableSetMultimap.builder();
+ ImmutableSetMultimap.Builder<Project.NameKey, RefState> result =
+ ImmutableSetMultimap.builder();
for (Table.Cell<Account.Id, PatchSet.Id, Ref> edit : editRefs().cellSet()) {
result.put(
project,
@@ -1528,7 +1555,10 @@ public class ChangeData {
// TODO: instantiating the notes is too much. We don't want to parse NoteDb, we just want the
// refs.
result.put(project, RefState.create(notes().getRefName(), notes().getMetaId()));
- notes().getRobotComments(); // Force loading robot comments.
+
+ @SuppressWarnings("unused")
+ var unused = notes().getRobotComments(); // Force loading robot comments.
+
RobotCommentNotes robotNotes = notes().getRobotCommentNotes();
result.put(project, RefState.create(robotNotes.getRefName(), robotNotes.getMetaId()));
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
index d5cc9e6487..d5f9c5bc62 100644
--- a/java/com/google/gerrit/server/query/change/ChangePredicates.java
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -17,6 +17,8 @@ package com.google.gerrit.server.query.change;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.google.common.base.CharMatcher;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
@@ -33,7 +35,6 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
-import java.util.Set;
/** Predicates that match against {@link ChangeData}. */
public class ChangePredicates {
@@ -87,10 +88,13 @@ public class ChangePredicates {
/**
* Returns a predicate that matches changes where the provided {@link
* com.google.gerrit.entities.Account.Id} has a pending draft comment.
+ *
+ * <p>The predicates filter by "legacy_id_str" field.
*/
+ @UsedAt(UsedAt.Project.GOOGLE)
public static Predicate<ChangeData> draftBy(
DraftCommentsReader draftCommentsReader, Account.Id id) {
- Set<Predicate<ChangeData>> changeIdPredicates =
+ ImmutableSet<Predicate<ChangeData>> changeIdPredicates =
draftCommentsReader.getChangesWithDrafts(id).stream()
.map(ChangePredicates::idStr)
.collect(toImmutableSet());
@@ -102,10 +106,13 @@ public class ChangePredicates {
/**
* Returns a predicate that matches changes where the provided {@link
* com.google.gerrit.entities.Account.Id} has starred changes with {@code label}.
+ *
+ * <p>The predicates filter by "legacy_id_str" field.
*/
+ @UsedAt(UsedAt.Project.GOOGLE)
public static Predicate<ChangeData> starBy(
StarredChangesReader starredChangesReader, Account.Id id) {
- Set<Predicate<ChangeData>> starredChanges =
+ ImmutableSet<Predicate<ChangeData>> starredChanges =
starredChangesReader.byAccountId(id).stream()
.map(ChangePredicates::idStr)
.collect(toImmutableSet());
@@ -144,6 +151,19 @@ public class ChangePredicates {
}
/**
+ * Returns a predicate that matches the change number with the provided {@link
+ * com.google.gerrit.entities.Change.Id}.
+ */
+ public static Predicate<ChangeData> changeNumber(
+ Change.Id id, ChangeQueryBuilder.Arguments args) {
+ if (args.getSchema().hasField(ChangeField.CHANGENUM_SPEC)) {
+ return new ChangeIndexCardinalPredicate(
+ ChangeField.CHANGENUM_SPEC, ChangeQueryBuilder.FIELD_CHANGE, id.toString(), 1);
+ }
+ return idStr(id);
+ }
+
+ /**
* Returns a predicate that matches changes owned by the provided {@link
* com.google.gerrit.entities.Account.Id}.
*/
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index da14d45e31..d598739210 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -111,6 +111,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
+import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -143,7 +144,8 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuil
public interface ChangeIsOperandFactory extends ChangeOperandFactory {}
- private static final Pattern PAT_LEGACY_ID = Pattern.compile("^[1-9][0-9]*$");
+ private static final Pattern PAT_CHANGE_NUMBER = Pattern.compile("^[1-9][0-9]*$");
+ private static final Pattern PAT_PROJECT_CHANGE_NUM = Pattern.compile("^([^~]+)~([1-9][0-9]*)$");
private static final Pattern PAT_CHANGE_ID = Pattern.compile(CHANGE_ID_PATTERN);
private static final Pattern DEF_CHANGE =
Pattern.compile("^(?:[1-9][0-9]*|(?:[^~]+~[^~]+~)?[iI][0-9a-f]{4,}.*)$");
@@ -164,6 +166,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuil
public static final String FIELD_CHANGE = "change";
public static final String FIELD_CHANGE_ID = "change_id";
+ public static final String FIELD_CHANGE_NUMBER = "changenumber";
public static final String FIELD_COMMENT = "comment";
public static final String FIELD_COMMENTBY = "commentby";
public static final String FIELD_COMMIT = "commit";
@@ -586,6 +589,18 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuil
@Operator
public Predicate<ChangeData> change(String query) throws QueryParseException {
+ return getPredicateChangeData(query, changeId -> ChangePredicates.changeNumber(changeId, args));
+ }
+
+ // Keep using the index legacy document-id (legacy_id_str) for URLs queries like: "/q/123456",
+ // "/q/Iasdw2312321", "/q/project~123456" that are expecting to always find a single element.
+ private Predicate<ChangeData> defaultSearch(String query) throws QueryParseException {
+ return getPredicateChangeData(query, ChangePredicates::idStr);
+ }
+
+ private Predicate<ChangeData> getPredicateChangeData(
+ String query, Function<Change.Id, Predicate<ChangeData>> changePredicateGetter)
+ throws QueryParseException {
Optional<ChangeTriplet> triplet = ChangeTriplet.parse(query);
if (triplet.isPresent()) {
return Predicate.and(
@@ -593,10 +608,16 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuil
branch(triplet.get().branch().branch()),
ChangePredicates.idPrefix(parseChangeId(triplet.get().id().get())));
}
- if (PAT_LEGACY_ID.matcher(query).matches()) {
+
+ Matcher projectChangeNumber = PAT_PROJECT_CHANGE_NUM.matcher(query);
+ if (projectChangeNumber.matches()) {
+ return Predicate.and(
+ project(projectChangeNumber.group(1)),
+ ChangePredicates.idStr(projectChangeNumber.group(2)));
+ } else if (PAT_CHANGE_NUMBER.matcher(query).matches()) {
Integer id = Ints.tryParse(query);
if (id != null) {
- return ChangePredicates.idStr(Change.id(id));
+ return changePredicateGetter.apply(Change.id(id));
}
} else if (PAT_CHANGE_ID.matcher(query).matches()) {
return ChangePredicates.idPrefix(parseChangeId(query));
@@ -1221,7 +1242,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuil
if (isSelf(who)) {
return isVisible();
}
- Set<Account.Id> accounts = null;
+ ImmutableSet<Account.Id> accounts = null;
try {
accounts = parseAccount(who);
} catch (QueryParseException e) {
@@ -1280,7 +1301,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuil
private Predicate<ChangeData> ownerDefaultField(String who)
throws QueryParseException, IOException, ConfigInvalidException {
- Set<Account.Id> accounts = parseAccountIgnoreVisibility(who);
+ ImmutableSet<Account.Id> accounts = parseAccountIgnoreVisibility(who);
if (accounts.size() > MAX_ACCOUNTS_PER_DEFAULT_FIELD) {
return Predicate.any();
}
@@ -1442,7 +1463,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuil
@Operator
public Predicate<ChangeData> from(String who)
throws QueryParseException, IOException, ConfigInvalidException {
- Set<Account.Id> ownerIds = parseAccountIgnoreVisibility(who);
+ ImmutableSet<Account.Id> ownerIds = parseAccountIgnoreVisibility(who);
return Predicate.or(owner(ownerIds), commentby(ownerIds));
}
@@ -1470,7 +1491,8 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuil
try {
// [,user=<user>]
if (inputArgs.keyValue.containsKey(ARG_ID_USER)) {
- Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value());
+ ImmutableSet<Account.Id> accounts =
+ parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value());
if (accounts != null && accounts.size() > 1) {
throw error(
String.format(
@@ -1559,7 +1581,8 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuil
try {
// [,user=<user>]
if (inputArgs.keyValue.containsKey(ARG_ID_USER)) {
- Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value());
+ ImmutableSet<Account.Id> accounts =
+ parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value());
if (accounts != null && accounts.size() > 1) {
throw error(
String.format(
@@ -1679,13 +1702,13 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuil
} else if (DEF_CHANGE.matcher(query).matches()) {
List<Predicate<ChangeData>> predicates = Lists.newArrayListWithCapacity(2);
try {
- predicates.add(change(query));
+ predicates.add(defaultSearch(query));
} catch (QueryParseException e) {
// Skip.
}
- // For PAT_LEGACY_ID, it may also be the prefix of some commits.
- if (query.length() >= 6 && PAT_LEGACY_ID.matcher(query).matches()) {
+ // For PAT_CHANGE_NUMBER, it may also be the prefix of some commits.
+ if (query.length() >= 6 && PAT_CHANGE_NUMBER.matcher(query).matches()) {
predicates.add(commit(query));
}
@@ -1785,7 +1808,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuil
return accounts;
}
- private Set<Account.Id> parseAccount(String who)
+ private ImmutableSet<Account.Id> parseAccount(String who)
throws QueryParseException, IOException, ConfigInvalidException {
try {
return args.accountResolver.resolveAsUser(args.getUser(), who).asNonEmptyIdSet();
@@ -1797,7 +1820,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuil
}
}
- private Set<Account.Id> parseAccountIgnoreVisibility(String who)
+ private ImmutableSet<Account.Id> parseAccountIgnoreVisibility(String who)
throws QueryRequiresAuthException, IOException, ConfigInvalidException {
try {
return args.accountResolver
@@ -1811,7 +1834,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuil
}
}
- private Set<Account.Id> parseAccountIgnoreVisibility(
+ private ImmutableSet<Account.Id> parseAccountIgnoreVisibility(
String who, java.util.function.Predicate<AccountState> activityFilter)
throws QueryRequiresAuthException, IOException, ConfigInvalidException {
try {
@@ -1849,12 +1872,12 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuil
}
private List<ChangeData> parseChangeData(String value) throws QueryParseException {
- if (PAT_LEGACY_ID.matcher(value).matches()) {
+ if (PAT_CHANGE_NUMBER.matcher(value).matches()) {
Optional<Change.Id> id = Change.Id.tryParse(value);
if (!id.isPresent()) {
throw error("Invalid change id " + value);
}
- return args.queryProvider.get().byLegacyChangeId(id.get());
+ return args.queryProvider.get().byChangeNumber(id.get());
} else if (PAT_CHANGE_ID.matcher(value).matches()) {
List<ChangeData> changes = args.queryProvider.get().byKeyPrefix(parseChangeId(value));
if (changes.isEmpty()) {
@@ -1889,7 +1912,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuil
Predicate<ChangeData> reviewerPredicate = null;
try {
- Set<Account.Id> accounts = parseAccountIgnoreVisibility(who);
+ ImmutableSet<Account.Id> accounts = parseAccountIgnoreVisibility(who);
if (!forDefaultField || accounts.size() <= MAX_ACCOUNTS_PER_DEFAULT_FIELD) {
reviewerPredicate =
Predicate.or(
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
index 305316dab2..3ada9d79c7 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
@@ -19,6 +19,7 @@ import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_LIM
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Change;
import com.google.gerrit.extensions.common.PluginDefinedInfo;
import com.google.gerrit.extensions.registration.DynamicSet;
@@ -32,7 +33,6 @@ import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.DynamicOptions;
import com.google.gerrit.server.DynamicOptions.DynamicBean;
-import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.account.AccountLimits;
import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
import com.google.gerrit.server.change.PluginDefinedAttributesFactories;
@@ -64,7 +64,6 @@ public class ChangeQueryProcessor extends QueryProcessor<ChangeData>
private final Map<String, DynamicBean> dynamicBeans = new HashMap<>();
private final List<Extension<ChangePluginDefinedInfoFactory>>
changePluginDefinedInfoFactoriesByPlugin = new ArrayList<>();
- private final Sequences sequences;
private final IndexConfig indexConfig;
@Singleton
@@ -90,7 +89,6 @@ public class ChangeQueryProcessor extends QueryProcessor<ChangeData>
IndexConfig indexConfig,
ChangeIndexCollection indexes,
ChangeIndexRewriter rewriter,
- Sequences sequences,
ChangeIsVisibleToPredicate.Factory changeIsVisibleToPredicateFactory,
DynamicSet<ChangePluginDefinedInfoFactory> changePluginDefinedInfoFactories) {
super(
@@ -103,7 +101,6 @@ public class ChangeQueryProcessor extends QueryProcessor<ChangeData>
() -> limitsFactory.create(userProvider.get()).getQueryLimit());
this.userProvider = userProvider;
this.changeIsVisibleToPredicateFactory = changeIsVisibleToPredicateFactory;
- this.sequences = sequences;
this.indexConfig = indexConfig;
changePluginDefinedInfoFactories
@@ -112,6 +109,7 @@ public class ChangeQueryProcessor extends QueryProcessor<ChangeData>
}
@Override
+ @CanIgnoreReturnValue
public ChangeQueryProcessor enforceVisibility(boolean enforce) {
super.enforceVisibility(enforce);
return this;
@@ -171,16 +169,6 @@ public class ChangeQueryProcessor extends QueryProcessor<ChangeData>
}
@Override
- protected int getIndexSize() {
- return sequences.lastChangeId();
- }
-
- @Override
- protected int getBatchSize() {
- return sequences.changeBatchSize();
- }
-
- @Override
protected int getInitialPageSize(int limit) {
return Math.min(getUserQueryLimit().getAsInt(), limit);
}
diff --git a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index fc4c1d084d..87b891527a 100644
--- a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -89,7 +89,7 @@ public class ConflictsPredicate {
List<Predicate<ChangeData>> and = new ArrayList<>(5);
and.add(ChangePredicates.project(c.getProject()));
and.add(ChangePredicates.ref(c.getDest().branch()));
- and.add(Predicate.not(ChangePredicates.idStr(c.getId())));
+ and.add(Predicate.not(ChangePredicates.changeNumber(c.getId(), args)));
and.add(Predicate.or(filePredicates));
ChangeDataCache changeDataCache = new ChangeDataCache(cd, args.projectCache);
diff --git a/java/com/google/gerrit/server/query/change/EqualsLabelPredicates.java b/java/com/google/gerrit/server/query/change/EqualsLabelPredicates.java
index 0d6dc3c4e1..aba6a98393 100644
--- a/java/com/google/gerrit/server/query/change/EqualsLabelPredicates.java
+++ b/java/com/google/gerrit/server/query/change/EqualsLabelPredicates.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.change;
+import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
@@ -35,7 +36,6 @@ import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData.StorageConstraint;
import java.io.IOException;
-import java.util.List;
import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -137,6 +137,8 @@ public class EqualsLabelPredicates {
Change c = cd.change();
if (c == null) {
// The change has disappeared.
+ logger.atFine().log(
+ "%s=%s doesn't match because the change has disappeared.", label, expVal);
return false;
}
@@ -145,17 +147,28 @@ public class EqualsLabelPredicates {
// in the index. We do that since computing count=0 requires looping on all {label_type,
// vote_value} for the change and storing a {count=0} format for it in the change index
// which is computationally expensive.
+ logger.atFine().log(
+ "%s=%s doesn't match change %s because the count was specified as 0 which is not"
+ + " supported.",
+ label, expVal, cd.change().getChangeId());
return false;
}
Optional<ProjectState> project = projectCache.get(c.getDest().project());
if (!project.isPresent()) {
// The project has disappeared.
+ logger.atFine().log(
+ "%s=%s doesn't match change %s because its project %s has disappeared.",
+ label, expVal, cd.change().getChangeId(), c.getDest().project().get());
return false;
}
LabelType labelType = type(project.get().getLabelTypes(), label);
if (labelType == null) {
+ logger.atFine().log(
+ "%s=%s doesn't match change %s because the label is not defined by its project %s"
+ + " (label type = %s)",
+ label, expVal, cd.change().getChangeId(), project.get(), project.get().getLabelTypes());
return false; // Label is not defined by this project.
}
@@ -171,16 +184,51 @@ public class EqualsLabelPredicates {
}
}
}
+ logger.atFine().log(
+ "found %s matching votes for %s=%s on change %s (current approvals = %s)",
+ matchingVotes, label, expVal, cd.change().getChangeId(), cd.currentApprovals());
cd.setStorageConstraint(currentStorageConstraint);
if (!hasVote && expVal == 0) {
+ logger.atFine().log(
+ "%s=%s matches change %s because there is no vote for label %s",
+ label, expVal, cd.change().getChangeId(), label);
return true;
}
- return count == null ? matchingVotes >= 1 : matchingVotes == count;
+ if (count == null) {
+ if (matchingVotes >= 1) {
+ logger.atFine().log(
+ "%s=%s matches change %s because there are %s matching votes (count was not"
+ + " specified, hence 1 or more votes are needed)",
+ label, expVal, cd.change().getChangeId(), matchingVotes);
+ return true;
+ }
+ logger.atFine().log(
+ "%s=%s doesn't match change %s because there are no matching votes (count was not"
+ + " specified, hence 1 or more votes are needed)",
+ label, expVal, cd.change().getChangeId());
+ return false;
+ }
+
+ if (matchingVotes == count) {
+ logger.atFine().log(
+ "%s=%s matches change %s because there are %s matching votes which matches the"
+ + " expected count %s",
+ label, expVal, cd.change().getChangeId(), matchingVotes, count);
+ return true;
+ }
+ logger.atFine().log(
+ "%s=%s doesn't match change %s because there are %s matching votes which doesn't match"
+ + " the expected count %s",
+ label, expVal, cd.change().getChangeId(), matchingVotes, count);
+ return false;
}
private boolean match(ChangeData cd, PatchSetApproval psa) {
if (psa.value() != expVal) {
+ logger.atFine().log(
+ "vote %s on change %s doesn't match expected value %s",
+ psa, cd.change().getChangeId(), expVal);
return false;
}
Account.Id approver = psa.accountId();
@@ -188,18 +236,27 @@ public class EqualsLabelPredicates {
if (account != null) {
// case when account in query is numeric
if (!account.equals(approver) && !isMagicUser()) {
+ logger.atFine().log(
+ "vote %s on change %s doesn't match expected approver %s",
+ psa, cd.change().getChangeId(), account);
return false;
}
// case when account in query = owner
if (account.equals(ChangeQueryBuilder.OWNER_ACCOUNT_ID)
&& !cd.change().getOwner().equals(approver)) {
+ logger.atFine().log(
+ "vote %s on change %s doesn't match since it is not from the change owner %s",
+ psa, cd.change().getChangeId(), cd.change().getOwner());
return false;
}
// case when account in query = non_uploader
if (account.equals(ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID)
&& cd.currentPatchSet().uploader().equals(approver)) {
+ logger.atFine().log(
+ "vote %s on change %s doesn't match since it is not from the uploader %s",
+ psa, cd.change().getChangeId(), cd.currentPatchSet().uploader());
return false;
}
@@ -207,6 +264,14 @@ public class EqualsLabelPredicates {
if ((cd.currentPatchSet().uploader().equals(approver)
|| matchAccount(cd.getCommitter().getEmailAddress(), approver)
|| matchAccount(cd.getAuthor().getEmailAddress(), approver))) {
+ logger.atFine().log(
+ "vote %s on change %s doesn't match since it is not from a contributor"
+ + " (uploader: %s, committer: %s, author: %s)",
+ psa,
+ cd.change().getChangeId(),
+ cd.currentPatchSet().uploader(),
+ cd.getCommitter().getEmailAddress(),
+ cd.getAuthor().getEmailAddress());
return false;
}
}
@@ -214,6 +279,10 @@ public class EqualsLabelPredicates {
IdentifiedUser reviewer = userFactory.create(approver);
if (group != null && !reviewer.getEffectiveGroups().contains(group)) {
+ logger.atFine().log(
+ "vote %s on change %s doesn't match since the approver %s is not a member of the"
+ + " expected group %s",
+ psa, cd.change().getChangeId(), approver, group);
return false;
}
@@ -221,12 +290,19 @@ public class EqualsLabelPredicates {
try {
PermissionBackend.ForChange perm = permissionBackend.absentUser(approver).change(cd);
if (!projectCache.get(cd.project()).map(ProjectState::statePermitsRead).orElse(false)) {
+ logger.atFine().log(
+ "vote %s on change %s doesn't match since the project %s doesn't permit read",
+ psa, cd.change().getChangeId(), cd.project().get());
return false;
}
perm.check(ChangePermission.READ);
+ logger.atFine().log("vote %s on change %s matches", psa, cd.change().getChangeId());
return true;
} catch (PermissionBackendException | AuthException e) {
+ logger.atFine().log(
+ "vote %s on change %s doesn't match because the approver %s has no read access",
+ psa, cd.change().getChangeId(), approver);
return false;
}
}
@@ -237,7 +313,7 @@ public class EqualsLabelPredicates {
*/
private boolean matchAccount(String email, Account.Id accountId) {
try {
- List<AccountState> accountsList = accountResolver.resolve(email).asList();
+ ImmutableList<AccountState> accountsList = accountResolver.resolve(email).asList();
return accountsList.stream().anyMatch(c -> c.account().id().equals(accountId));
} catch (ConfigInvalidException | IOException e) {
logger.atWarning().withCause(e).log("Failed to resolve account %s", email);
diff --git a/java/com/google/gerrit/server/query/change/GroupPredicate.java b/java/com/google/gerrit/server/query/change/GroupPredicate.java
index c4aba0d97b..bff2575c98 100644
--- a/java/com/google/gerrit/server/query/change/GroupPredicate.java
+++ b/java/com/google/gerrit/server/query/change/GroupPredicate.java
@@ -14,9 +14,9 @@
package com.google.gerrit.server.query.change;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.server.index.change.ChangeField;
-import java.util.List;
public class GroupPredicate extends ChangeIndexPredicate {
public GroupPredicate(String group) {
@@ -26,7 +26,7 @@ public class GroupPredicate extends ChangeIndexPredicate {
@Override
public boolean match(ChangeData cd) {
for (PatchSet ps : cd.patchSets()) {
- List<String> groups = ps.groups();
+ ImmutableList<String> groups = ps.groups();
if (groups != null && groups.contains(getValue())) {
return true;
}
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index 3c7944c2d8..a8ee4bc00d 100644
--- a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -88,6 +88,7 @@ public class InternalChangeQuery extends InternalQuery<ChangeData, InternalChang
private final ChangeData.Factory changeDataFactory;
private final ChangeNotes.Factory notesFactory;
private final EditByPredicateProvider editByPredicateProvider;
+ private final Provider<ChangeQueryBuilder.Arguments> queryBuilderArgsProvider;
@Inject
InternalChangeQuery(
@@ -96,11 +97,13 @@ public class InternalChangeQuery extends InternalQuery<ChangeData, InternalChang
IndexConfig indexConfig,
ChangeData.Factory changeDataFactory,
ChangeNotes.Factory notesFactory,
- EditByPredicateProvider editByPredicateProvider) {
+ EditByPredicateProvider editByPredicateProvider,
+ Provider<ChangeQueryBuilder.Arguments> queryBuilderArgsProvider) {
super(queryProcessor, indexes, indexConfig);
this.changeDataFactory = changeDataFactory;
this.notesFactory = notesFactory;
this.editByPredicateProvider = editByPredicateProvider;
+ this.queryBuilderArgsProvider = queryBuilderArgsProvider;
}
public List<ChangeData> byKey(Change.Key key) {
@@ -115,6 +118,10 @@ public class InternalChangeQuery extends InternalQuery<ChangeData, InternalChang
return query(ChangePredicates.idStr(id));
}
+ public List<ChangeData> byChangeNumber(Change.Id id) {
+ return query(ChangePredicates.changeNumber(id, queryBuilderArgsProvider.get()));
+ }
+
@UsedAt(UsedAt.Project.GOOGLE)
public List<ChangeData> byLegacyChangeIds(Collection<Change.Id> ids) {
List<Predicate<ChangeData>> preds = new ArrayList<>(ids.size());
@@ -200,7 +207,7 @@ public class InternalChangeQuery extends InternalQuery<ChangeData, InternalChang
return Lists.transform(notes, n -> changeDataFactory.create(n));
}
- private List<ChangeData> byCommitsOnBranchNotMergedFromIndex(
+ private ImmutableList<ChangeData> byCommitsOnBranchNotMergedFromIndex(
BranchNameKey branch, Collection<String> hashes) {
return query(
and(
@@ -319,7 +326,7 @@ public class InternalChangeQuery extends InternalQuery<ChangeData, InternalChang
for (List<String> part : Iterables.partition(groups, batchSize)) {
for (ChangeData cd :
queryExhaustively(querySupplier, byProjectGroupsPredicate(indexConfig, project, part))) {
- if (!seen.add(cd.getId())) {
+ if (!seen.add(cd.virtualId())) {
result.add(cd);
}
}
diff --git a/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java b/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
index 054a69e3fd..046d24c74b 100644
--- a/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
+++ b/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -22,9 +22,9 @@ import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
public class IsWatchedByPredicate extends AndPredicate<ChangeData> {
protected static String describe(CurrentUser user) {
@@ -41,7 +41,7 @@ public class IsWatchedByPredicate extends AndPredicate<ChangeData> {
this.user = args.getUser();
}
- protected static List<Predicate<ChangeData>> filters(ChangeQueryBuilder.Arguments args)
+ protected static ImmutableList<Predicate<ChangeData>> filters(ChangeQueryBuilder.Arguments args)
throws QueryParseException {
List<Predicate<ChangeData>> r = new ArrayList<>();
ChangeQueryBuilder builder = new ChangeQueryBuilder(args);
@@ -85,7 +85,7 @@ public class IsWatchedByPredicate extends AndPredicate<ChangeData> {
return ImmutableList.of(or(r));
}
- protected static Collection<ProjectWatchKey> getWatches(ChangeQueryBuilder.Arguments args)
+ protected static Set<ProjectWatchKey> getWatches(ChangeQueryBuilder.Arguments args)
throws QueryParseException {
CurrentUser user = args.getUser();
if (user.isIdentifiedUser()) {
diff --git a/java/com/google/gerrit/server/query/change/LabelPredicate.java b/java/com/google/gerrit/server/query/change/LabelPredicate.java
index 5a38958053..dc859a30ed 100644
--- a/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -37,6 +37,12 @@ public class LabelPredicate extends OrPredicate<ChangeData> {
protected static final int MAX_LABEL_VALUE = 4;
protected static final int MAX_COUNT = 5; // inclusive
+ // Set a different max for label counts for in-memory label predicates. This is because the
+ // in-memory count is used by submit requirements to evaluate different expressions
+ // (applicability, submittability, override). The other MAX_COUNT is used by the change index and
+ // change queries.
+ private static final int MAX_COUNT_INTERNAL = 50; // inclusive
+
protected static class Args {
protected final AccountResolver accountResolver;
protected final ProjectCache projectCache;
@@ -244,7 +250,7 @@ public class LabelPredicate extends OrPredicate<ChangeData> {
switch (countOp) {
case GREATER:
case GREATER_EQUAL:
- IntStream.range(count + 1, MAX_COUNT + 1).forEach(result::add);
+ IntStream.range(count + 1, MAX_COUNT_INTERNAL + 1).forEach(result::add);
break;
case LESS:
case LESS_EQUAL:
diff --git a/java/com/google/gerrit/server/query/change/MagicLabelPredicates.java b/java/com/google/gerrit/server/query/change/MagicLabelPredicates.java
index 420ab61de7..fadffd7d57 100644
--- a/java/com/google/gerrit/server/query/change/MagicLabelPredicates.java
+++ b/java/com/google/gerrit/server/query/change/MagicLabelPredicates.java
@@ -150,16 +150,30 @@ public class MagicLabelPredicates {
public boolean match(ChangeData cd) {
Change change = cd.change();
if (change == null) {
+ logger.atFine().log(
+ "%s doesn't match because the change has disappeared.", magicLabelVote.formatLabel());
return false; // The change has disappeared.
}
Optional<ProjectState> project = args.projectCache.get(change.getDest().project());
if (!project.isPresent()) {
+ logger.atFine().log(
+ "%s doesn't match change %s because its project %s has disappeared.",
+ magicLabelVote.formatLabel(),
+ cd.change().getChangeId(),
+ change.getDest().project().get());
return false; // The project has disappeared.
}
LabelType labelType = type(project.get().getLabelTypes(), magicLabelVote.label());
if (labelType == null) {
+ logger.atFine().log(
+ "%s doesn't match change %s because the label is not defined by its project %s (label"
+ + " types = %s)",
+ magicLabelVote.formatLabel(),
+ cd.change().getChangeId(),
+ project.get(),
+ project.get().getLabelTypes());
return false; // Label is not defined by this project.
}
diff --git a/java/com/google/gerrit/server/query/change/OrSource.java b/java/com/google/gerrit/server/query/change/OrSource.java
index f2de536aff..889428707c 100644
--- a/java/com/google/gerrit/server/query/change/OrSource.java
+++ b/java/com/google/gerrit/server/query/change/OrSource.java
@@ -25,7 +25,6 @@ import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.ResultSet;
import java.util.Collection;
import java.util.HashSet;
-import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -45,7 +44,7 @@ public class OrSource extends OrPredicate<ChangeData> implements ChangeDataSourc
public ResultSet<ChangeData> read() {
// ResultSets are lazy. Calling #read here first and then dealing with ResultSets only when
// requested allows the index to run asynchronous queries.
- List<ResultSet<ChangeData>> results =
+ ImmutableList<ResultSet<ChangeData>> results =
getChildren().stream().map(p -> ((ChangeDataSource) p).read()).collect(toImmutableList());
return new LazyResultSet<>(
() -> {
@@ -53,7 +52,7 @@ public class OrSource extends OrPredicate<ChangeData> implements ChangeDataSourc
Set<Change.Id> have = new HashSet<>();
for (ResultSet<ChangeData> resultSet : results) {
for (ChangeData result : resultSet) {
- if (have.add(result.getId())) {
+ if (have.add(result.virtualId())) {
r.add(result);
}
}
diff --git a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index d21f5b62a3..d3b5605020 100644
--- a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.query.change;
import static com.google.common.base.Preconditions.checkState;
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
@@ -125,7 +126,7 @@ public class OutputStreamQuery {
}
void setLimit(int n) {
- queryProcessor.setUserProvidedLimit(n);
+ queryProcessor.setUserProvidedLimit(n, /* applyDefaultLimit */ false);
}
public void setNoLimit(boolean on) {
@@ -348,7 +349,7 @@ public class OutputStreamQuery {
eventFactory.addDependencies(rw, c, d.change(), d.currentPatchSet());
}
- List<PluginDefinedInfo> pluginInfos = pluginInfosByChange.get(d.getId());
+ ImmutableList<PluginDefinedInfo> pluginInfos = pluginInfosByChange.get(d.getId());
if (!pluginInfos.isEmpty()) {
c.plugins = pluginInfos;
}
diff --git a/java/com/google/gerrit/server/query/change/package-info.java b/java/com/google/gerrit/server/query/change/package-info.java
new file mode 100644
index 0000000000..a269041452
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.query.change;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java b/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
index 89c802d350..ee910f1930 100644
--- a/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
@@ -17,6 +17,8 @@ package com.google.gerrit.server.query.group;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;
import com.google.gerrit.entities.Account;
@@ -37,7 +39,6 @@ import com.google.inject.Inject;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
-import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
/** Parses a query string meant to be applied to group objects. */
@@ -135,8 +136,8 @@ public class GroupQueryBuilder extends QueryBuilder<InternalGroup, GroupQueryBui
@Operator
public Predicate<InternalGroup> member(String query)
throws QueryParseException, ConfigInvalidException, IOException {
- Set<Account.Id> accounts = parseAccount(query);
- List<Predicate<InternalGroup>> predicates =
+ ImmutableSet<Account.Id> accounts = parseAccount(query);
+ ImmutableList<Predicate<InternalGroup>> predicates =
accounts.stream().map(GroupPredicates::member).collect(toImmutableList());
return Predicate.or(predicates);
}
@@ -156,7 +157,7 @@ public class GroupQueryBuilder extends QueryBuilder<InternalGroup, GroupQueryBui
return new LimitPredicate<>(FIELD_LIMIT, limit);
}
- private Set<Account.Id> parseAccount(String nameOrEmail)
+ private ImmutableSet<Account.Id> parseAccount(String nameOrEmail)
throws QueryParseException, IOException, ConfigInvalidException {
try {
return args.accountResolver.resolve(nameOrEmail).asNonEmptyIdSet();
diff --git a/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java b/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
index e08ff1cc8c..8c97bb2fe9 100644
--- a/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
@@ -26,7 +26,6 @@ import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryProcessor;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.account.AccountLimits;
import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.index.group.GroupIndexCollection;
@@ -45,7 +44,6 @@ import com.google.inject.Singleton;
public class GroupQueryProcessor extends QueryProcessor<InternalGroup> {
private final Provider<CurrentUser> userProvider;
private final GroupControl.GenericFactory groupControlFactory;
- private final Sequences sequences;
private final IndexConfig indexConfig;
@Singleton
@@ -71,8 +69,7 @@ public class GroupQueryProcessor extends QueryProcessor<InternalGroup> {
IndexConfig indexConfig,
GroupIndexCollection indexes,
GroupIndexRewriter rewriter,
- GroupControl.GenericFactory groupControlFactory,
- Sequences sequences) {
+ GroupControl.GenericFactory groupControlFactory) {
super(
groupQueryMetrics,
GroupSchemaDefinitions.INSTANCE,
@@ -83,7 +80,6 @@ public class GroupQueryProcessor extends QueryProcessor<InternalGroup> {
() -> limitsFactory.create(userProvider.get()).getQueryLimit());
this.userProvider = userProvider;
this.groupControlFactory = groupControlFactory;
- this.sequences = sequences;
this.indexConfig = indexConfig;
}
@@ -100,14 +96,4 @@ public class GroupQueryProcessor extends QueryProcessor<InternalGroup> {
protected String formatForLogging(InternalGroup internalGroup) {
return internalGroup.getGroupUUID().get();
}
-
- @Override
- protected int getIndexSize() {
- return sequences.lastGroupId();
- }
-
- @Override
- protected int getBatchSize() {
- return sequences.groupBatchSize();
- }
}
diff --git a/java/com/google/gerrit/server/query/group/InternalGroupQuery.java b/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
index 078acd4b9c..29163a471d 100644
--- a/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
+++ b/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
@@ -72,7 +72,7 @@ public class InternalGroupQuery extends InternalQuery<InternalGroup, InternalGro
ImmutableSet<AccountGroup.UUID> subgroupIds) {
List<Predicate<InternalGroup>> predicates =
subgroupIds.stream().map(e -> GroupPredicates.subgroup(e)).collect(Collectors.toList());
- List<InternalGroup> groups = query(Predicate.or(predicates));
+ ImmutableList<InternalGroup> groups = query(Predicate.or(predicates));
Map<AccountGroup.UUID, Set<AccountGroup.UUID>> parentsByChild =
Maps.newHashMapWithExpectedSize(groups.size());
@@ -90,7 +90,7 @@ public class InternalGroupQuery extends InternalQuery<InternalGroup, InternalGro
private Optional<InternalGroup> getOnlyGroup(
Predicate<InternalGroup> predicate, String groupDescription) {
- List<InternalGroup> groups = query(predicate);
+ ImmutableList<InternalGroup> groups = query(predicate);
if (groups.isEmpty()) {
return Optional.empty();
}
diff --git a/java/com/google/gerrit/server/query/group/package-info.java b/java/com/google/gerrit/server/query/group/package-info.java
new file mode 100644
index 0000000000..22affc7210
--- /dev/null
+++ b/java/com/google/gerrit/server/query/group/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.query.group;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java b/java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java
index ddc7ccc9af..c24c439450 100644
--- a/java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java
@@ -31,7 +31,6 @@ import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountLimits;
import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.project.ProjectCache;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -47,7 +46,6 @@ import com.google.inject.Singleton;
public class ProjectQueryProcessor extends QueryProcessor<ProjectData> {
private final PermissionBackend permissionBackend;
private final Provider<CurrentUser> userProvider;
- private final ProjectCache projectCache;
private final IndexConfig indexConfig;
@Singleton
@@ -73,8 +71,7 @@ public class ProjectQueryProcessor extends QueryProcessor<ProjectData> {
IndexConfig indexConfig,
ProjectIndexCollection indexes,
ProjectIndexRewriter rewriter,
- PermissionBackend permissionBackend,
- ProjectCache projectCache) {
+ PermissionBackend permissionBackend) {
super(
projectQueryMetrics,
ProjectSchemaDefinitions.INSTANCE,
@@ -85,7 +82,6 @@ public class ProjectQueryProcessor extends QueryProcessor<ProjectData> {
() -> limitsFactory.create(userProvider.get()).getQueryLimit());
this.permissionBackend = permissionBackend;
this.userProvider = userProvider;
- this.projectCache = projectCache;
this.indexConfig = indexConfig;
}
@@ -102,14 +98,4 @@ public class ProjectQueryProcessor extends QueryProcessor<ProjectData> {
protected String formatForLogging(ProjectData projectData) {
return projectData.getProject().getName();
}
-
- @Override
- protected int getIndexSize() {
- return projectCache.all().size();
- }
-
- @Override
- protected int getBatchSize() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/project/package-info.java b/java/com/google/gerrit/server/query/project/package-info.java
new file mode 100644
index 0000000000..e8444ba1e1
--- /dev/null
+++ b/java/com/google/gerrit/server/query/project/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.query.project;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java b/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java
index c6599759ed..c5aa84cb82 100644
--- a/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java
+++ b/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java
@@ -65,7 +65,8 @@ public class DefaultQuotaBackend implements QuotaBackend {
// PluginSets can change their content when plugins (de-)register. Copy the currently registered
// plugins so that we can iterate twice on a stable list.
- List<PluginSetEntryContext<QuotaEnforcer>> enforcers = ImmutableList.copyOf(quotaEnforcers);
+ ImmutableList<PluginSetEntryContext<QuotaEnforcer>> enforcers =
+ ImmutableList.copyOf(quotaEnforcers);
List<QuotaResponse> responses = new ArrayList<>(enforcers.size());
for (PluginSetEntryContext<QuotaEnforcer> enforcer : enforcers) {
try {
@@ -107,7 +108,8 @@ public class DefaultQuotaBackend implements QuotaBackend {
QuotaRequestContext requestContext) {
// PluginSets can change their content when plugins (de-)register. Copy the currently registered
// plugins so that we can iterate twice on a stable list.
- List<PluginSetEntryContext<QuotaEnforcer>> enforcers = ImmutableList.copyOf(quotaEnforcers);
+ ImmutableList<PluginSetEntryContext<QuotaEnforcer>> enforcers =
+ ImmutableList.copyOf(quotaEnforcers);
List<QuotaResponse> responses = new ArrayList<>(enforcers.size());
for (PluginSetEntryContext<QuotaEnforcer> enforcer : enforcers) {
responses.add(enforcer.call(p -> p.availableTokens(quotaGroup, requestContext)));
diff --git a/java/com/google/gerrit/server/quota/package-info.java b/java/com/google/gerrit/server/quota/package-info.java
new file mode 100644
index 0000000000..d309f97934
--- /dev/null
+++ b/java/com/google/gerrit/server/quota/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.quota;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/restapi/access/package-info.java b/java/com/google/gerrit/server/restapi/access/package-info.java
new file mode 100644
index 0000000000..7e525ebd3b
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/access/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.restapi.access;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/restapi/account/CreateEmail.java b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
index 57cd466deb..fe925ce3e4 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
@@ -162,7 +162,9 @@ public class CreateEmail
throw new ResourceConflictException(e.getMessage());
}
if (input.preferred) {
- putPreferred.apply(new AccountResource.Email(user, email), null);
+ @SuppressWarnings("unused")
+ var unused = putPreferred.apply(new AccountResource.Email(user, email), null);
+
info.preferred = true;
}
} else {
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteDraftCommentsUtil.java b/java/com/google/gerrit/server/restapi/account/DeleteDraftCommentsUtil.java
index cbbaac5777..79038af7a7 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteDraftCommentsUtil.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteDraftCommentsUtil.java
@@ -18,6 +18,8 @@ import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdate
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
@@ -44,6 +46,7 @@ import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.restapi.change.CommentJson;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.BatchUpdates;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.UpdateException;
import com.google.gerrit.server.update.context.RefUpdateContext;
@@ -62,7 +65,8 @@ import java.util.Objects;
@Singleton
public class DeleteDraftCommentsUtil {
private final BatchUpdate.Factory batchUpdateFactory;
- private final ChangeQueryBuilder queryBuilder;
+ private final BatchUpdates batchUpdates;
+ private final Supplier<ChangeQueryBuilder> queryBuilderSupplier;
private final Provider<InternalChangeQuery> queryProvider;
private final ChangeData.Factory changeDataFactory;
private final ChangeJson.Factory changeJsonFactory;
@@ -75,7 +79,8 @@ public class DeleteDraftCommentsUtil {
@Inject
public DeleteDraftCommentsUtil(
BatchUpdate.Factory batchUpdateFactory,
- ChangeQueryBuilder queryBuilder,
+ BatchUpdates batchUpdates,
+ Provider<ChangeQueryBuilder> queryBuilderProvider,
Provider<InternalChangeQuery> queryProvider,
ChangeData.Factory changeDataFactory,
ChangeJson.Factory changeJsonFactory,
@@ -84,7 +89,8 @@ public class DeleteDraftCommentsUtil {
DraftCommentsReader draftCommentsReader,
PatchSetUtil psUtil) {
this.batchUpdateFactory = batchUpdateFactory;
- this.queryBuilder = queryBuilder;
+ this.batchUpdates = batchUpdates;
+ this.queryBuilderSupplier = Suppliers.memoize(queryBuilderProvider::get);
this.queryProvider = queryProvider;
this.changeDataFactory = changeDataFactory;
this.changeJsonFactory = changeJsonFactory;
@@ -120,7 +126,7 @@ public class DeleteDraftCommentsUtil {
// were,
// all updates from this operation only happen in All-Users and thus are fully atomic, so
// allowing partial failure would have little value.
- BatchUpdate.execute(updates.values(), ImmutableList.of(), false);
+ batchUpdates.execute(updates.values(), ImmutableList.of(), false);
}
return ops.stream().map(Op::getResult).filter(Objects::nonNull).collect(toImmutableList());
}
@@ -132,7 +138,7 @@ public class DeleteDraftCommentsUtil {
return hasDraft;
}
try {
- return Predicate.and(hasDraft, queryBuilder.parse(query));
+ return Predicate.and(hasDraft, queryBuilderSupplier.get().parse(query));
} catch (QueryParseException e) {
throw new BadRequestException("Invalid query: " + e.getMessage(), e);
}
diff --git a/java/com/google/gerrit/server/restapi/account/GetAgreements.java b/java/com/google/gerrit/server/restapi/account/GetAgreements.java
index eb2be10e83..b9c64e6aa6 100644
--- a/java/com/google/gerrit/server/restapi/account/GetAgreements.java
+++ b/java/com/google/gerrit/server/restapi/account/GetAgreements.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.restapi.account;
+import com.google.common.collect.ImmutableCollection;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.ContributorAgreement;
@@ -38,7 +39,6 @@ import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.List;
import org.eclipse.jgit.lib.Config;
@@ -93,7 +93,7 @@ public class GetAgreements implements RestReadView<AccountResource> {
}
List<AgreementInfo> results = new ArrayList<>();
- Collection<ContributorAgreement> cas =
+ ImmutableCollection<ContributorAgreement> cas =
projectCache.getAllProjects().getConfig().getContributorAgreements().values();
for (ContributorAgreement ca : cas) {
List<AccountGroup.UUID> groupIds = new ArrayList<>();
diff --git a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
index d7a5da1188..613a6510c9 100644
--- a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
+++ b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.restapi.account;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.common.AccountExternalIdInfo;
@@ -35,7 +36,6 @@ import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -71,7 +71,7 @@ public class GetExternalIds implements RestReadView<AccountResource> {
permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
}
- Collection<ExternalId> ids = externalIds.byAccount(resource.getUser().getAccountId());
+ ImmutableSet<ExternalId> ids = externalIds.byAccount(resource.getUser().getAccountId());
if (ids.isEmpty()) {
return Response.ok(ImmutableList.of());
}
diff --git a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
index 2131070b04..3f543affd3 100644
--- a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
@@ -100,7 +100,8 @@ public class PostWatchedProjects
if (!Strings.isNullOrEmpty(info.filter)) {
try {
- QueryParser.parse(info.filter);
+ @SuppressWarnings("unused")
+ var unused = QueryParser.parse(info.filter);
} catch (QueryParseException e) {
throw new BadRequestException(
String.format(
diff --git a/java/com/google/gerrit/server/restapi/account/PutPreferred.java b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
index 9a11891117..b1af85e335 100644
--- a/java/com/google/gerrit/server/restapi/account/PutPreferred.java
+++ b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.restapi.account;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
+import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.common.Input;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -42,7 +43,6 @@ import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
-import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -124,7 +124,7 @@ public class PutPreferred implements RestModifyView<AccountResource.Email, Input
// user doesn't have an external ID for this email
if (user.hasEmailAddress(preferredEmail)) {
// but Realm says the user is allowed to use this email
- Set<ExternalId> existingExtIdsWithThisEmail =
+ ImmutableSet<ExternalId> existingExtIdsWithThisEmail =
externalIds.byEmail(preferredEmail);
if (!existingExtIdsWithThisEmail.isEmpty()) {
// but the email is already assigned to another account
diff --git a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
index 9fc0c42a95..8966ec47af 100644
--- a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
+++ b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
@@ -197,9 +197,7 @@ public class QueryAccounts implements RestReadView<TopLevelResource> {
throw new MethodNotAllowedException("query disabled");
}
- if (limit != null) {
- queryProcessor.setUserProvidedLimit(limit);
- }
+ queryProcessor.setUserProvidedLimit(limit != null ? limit : 0, /* applyDefaultLimit */ true);
if (start != null) {
if (start < 0) {
@@ -213,7 +211,7 @@ public class QueryAccounts implements RestReadView<TopLevelResource> {
Predicate<AccountState> queryPred;
if (suggest) {
queryPred = queryBuilder.defaultQuery(query);
- queryProcessor.setUserProvidedLimit(suggestLimit);
+ queryProcessor.setUserProvidedLimit(suggestLimit, /* applyDefaultLimit */ true);
} else {
queryPred = queryBuilder.parse(query);
}
diff --git a/java/com/google/gerrit/server/restapi/account/package-info.java b/java/com/google/gerrit/server/restapi/account/package-info.java
new file mode 100644
index 0000000000..491593b342
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/account/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.restapi.account;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyPatch.java b/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
index 6adde99309..7d8c793e9e 100644
--- a/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
+++ b/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
@@ -163,7 +163,7 @@ public class ApplyPatch implements RestModifyView<ChangeResource, ApplyPatchPatc
RevCommit latestPatchset = revWalk.parseCommit(destChange.currentPatchSet().commitId());
RevCommit baseCommit;
- List<RevCommit> parents;
+ ImmutableList<RevCommit> parents;
if (!Strings.isNullOrEmpty(input.base)) {
baseCommit =
CommitUtil.getBaseCommit(
@@ -244,6 +244,13 @@ public class ApplyPatch implements RestModifyView<ChangeResource, ApplyPatchPatc
hasInputCommitMessage ? input.commitMessage : latestPatchset.getFullMessage();
// Since we might add error information to the message, we need to split the footers from the
// actual description.
+ // TODO: Fix parsing footers from the commit message. FooterLine#fromMessage expects the raw
+ // commit message that contains header lines, see RawParseUtils#commitMessage which is invoked
+ // from FooterLine#fromMessage. RawParseUtils#commitMessage always increases the pointer by 46
+ // to skip the "tree ..." line and if this line is not present the parsing of the footers is
+ // broken. This can lead to no footers being found although a Change-Id footer is present. This
+ // causes us to add the Change-Id again and as a result we end up with a commit message that
+ // contains the Change-Id line twice.
List<FooterLine> footerLines = FooterLine.fromMessage(fullMessage);
String messageWithNoFooters = removeFooters(fullMessage, footerLines);
if (FooterLine.getValues(footerLines, FOOTER_CHANGE_ID).isEmpty()) {
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java b/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
index c0c5f56c9a..56b3842679 100644
--- a/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
@@ -17,6 +17,8 @@ package com.google.gerrit.server.restapi.change;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -152,7 +154,7 @@ public final class ApplyPatchUtil {
+ "\n[[[Original patch trimmed due to size. Decoded string size: "
+ patchDescription.length()
+ ". Decoded string SHA1: "
- + Hashing.sha1().hashString(patchDescription, UTF_8)
+ + sha1(patchDescription)
+ ".]]]");
}
}
@@ -166,7 +168,7 @@ public final class ApplyPatchUtil {
+ "\n[[[Result patch trimmed due to size. Decoded string size: "
+ resultPatch.length()
+ ". Decoded string SHA1: "
- + Hashing.sha1().hashString(resultPatch, UTF_8)
+ + sha1(resultPatch)
+ ".]]]");
}
}
@@ -213,11 +215,24 @@ public final class ApplyPatchUtil {
}
private static String decodeIfNecessary(String patch) {
- if (Base64.isBase64(patch)) {
- return new String(org.eclipse.jgit.util.Base64.decode(patch), UTF_8);
+ if (Base64.isBase64(patch.getBytes(UTF_8))) {
+ try {
+ return new String(org.eclipse.jgit.util.Base64.decode(patch), UTF_8);
+ } catch (IllegalArgumentException e) {
+ // It's possible that all the chars in the patch are valid Base64 chars, but the full string
+ // is not a valid Base64 string as expected by jGit. In this case, we assume the patch is
+ // already unencoded.
+ return patch;
+ }
}
return patch;
}
+ @SuppressWarnings("deprecation")
+ @VisibleForTesting
+ public static HashCode sha1(String s) {
+ return Hashing.sha1().hashString(s, UTF_8);
+ }
+
private ApplyPatchUtil() {}
}
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
index 8a7beb60de..0877e62136 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
@@ -28,10 +28,12 @@ import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.changes.ChangeEditIdentityType;
import com.google.gerrit.extensions.api.changes.FileContentInput;
import com.google.gerrit.extensions.common.DiffWebLinkInfo;
import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.common.Input;
+import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -49,21 +51,28 @@ import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.WebLinks;
import com.google.gerrit.server.change.ChangeEditResource;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.FileContentUtil;
import com.google.gerrit.server.change.FileInfoJson;
import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.config.UrlFormatter;
import com.google.gerrit.server.edit.ChangeEdit;
import com.google.gerrit.server.edit.ChangeEditJson;
import com.google.gerrit.server.edit.ChangeEditModifier;
import com.google.gerrit.server.edit.ChangeEditUtil;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.RefPermission;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -72,6 +81,7 @@ import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -134,8 +144,7 @@ public class ChangeEdits implements ChildCollection<ChangeResource, ChangeEditRe
ChangeResource resource, IdString id, FileContentInput fileContentInput)
throws AuthException, BadRequestException, ResourceConflictException, IOException,
PermissionBackendException {
- putEdit.apply(resource, id.get(), fileContentInput);
- return Response.none();
+ return putEdit.apply(resource, id.get(), fileContentInput);
}
}
@@ -558,4 +567,87 @@ public class ChangeEdits implements ChildCollection<ChangeResource, ChangeEditRe
throw new ResourceNotFoundException();
}
}
+
+ @Singleton
+ public static class EditIdentity implements RestModifyView<ChangeResource, EditIdentity.Input> {
+ public static class Input {
+ public String name;
+ public String email;
+ public ChangeEditIdentityType type;
+ }
+
+ private final ChangeEditModifier editModifier;
+ private final GitRepositoryManager repositoryManager;
+ private final PermissionBackend permissionBackend;
+ private final Provider<CurrentUser> self;
+ private final Provider<PersonIdent> serverIdent;
+ private final DynamicItem<UrlFormatter> urlFormatter;
+
+ @Inject
+ EditIdentity(
+ ChangeEditModifier editModifier,
+ GitRepositoryManager repositoryManager,
+ PermissionBackend permissionBackend,
+ Provider<CurrentUser> self,
+ @GerritPersonIdent Provider<PersonIdent> serverIdent,
+ DynamicItem<UrlFormatter> urlFormatter) {
+ this.editModifier = editModifier;
+ this.repositoryManager = repositoryManager;
+ this.permissionBackend = permissionBackend;
+ this.self = self;
+ this.serverIdent = serverIdent;
+ this.urlFormatter = urlFormatter;
+ }
+
+ @Override
+ public Response<Object> apply(ChangeResource rsrc, EditIdentity.Input input)
+ throws AuthException, IOException, BadRequestException, ResourceConflictException,
+ PermissionBackendException {
+ if (input == null || input.type == null) {
+ throw new BadRequestException("type must be provided");
+ }
+ if (input.name == null && input.email == null) {
+ throw new BadRequestException("name or email must be provided");
+ }
+ input.name = Strings.nullToEmpty(input.name);
+ input.email = Strings.nullToEmpty(input.email);
+
+ RefPermission perm;
+ switch (input.type) {
+ case AUTHOR:
+ perm = RefPermission.FORGE_AUTHOR;
+ break;
+ case COMMITTER:
+ default:
+ perm = RefPermission.FORGE_COMMITTER;
+ break;
+ }
+
+ PersonIdent newIdent =
+ new PersonIdent(input.name, input.email, TimeUtil.now(), serverIdent.get().getZoneId());
+
+ if (!input.email.isEmpty() && !self.get().asIdentifiedUser().hasEmailAddress(input.email)) {
+ try {
+ permissionBackend.user(self.get()).ref(rsrc.getNotes().getChange().getDest()).check(perm);
+ } catch (AuthException e) {
+ throw new ResourceConflictException(
+ CommitValidators.invalidEmail(
+ input.type.toString(),
+ newIdent,
+ self.get().asIdentifiedUser(),
+ urlFormatter.get())
+ .getMessage(),
+ e);
+ }
+ }
+
+ try (Repository repository = repositoryManager.openRepository(rsrc.getProject())) {
+ editModifier.modifyIdentity(repository, rsrc.getNotes(), newIdent, input.type);
+ } catch (InvalidChangeOperationException e) {
+ throw new ResourceConflictException(e.getMessage());
+ }
+
+ return Response.none();
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java b/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
index 2ac24c67c2..8c95e932db 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
@@ -88,6 +88,7 @@ public class ChangeRestApiModule extends RestApiModule {
put(CHANGE_EDIT_KIND, "/").to(ChangeEdits.Put.class);
get(CHANGE_EDIT_KIND, "meta").to(ChangeEdits.GetMeta.class);
+ put(CHANGE_KIND, "edit:identity").to(ChangeEdits.EditIdentity.class);
put(CHANGE_KIND, "edit:message").to(ChangeEdits.EditMessage.class);
get(CHANGE_KIND, "edit:message").to(ChangeEdits.GetMessage.class);
post(CHANGE_KIND, "edit:publish").to(PublishChangeEdit.class);
@@ -98,6 +99,7 @@ public class ChangeRestApiModule extends RestApiModule {
post(CHANGE_KIND, "index").to(Index.class);
get(CHANGE_KIND, "meta_diff").to(GetMetaDiff.class);
post(CHANGE_KIND, "merge").to(CreateMergePatchSet.class);
+ get(CHANGE_KIND, "message").to(GetMessage.class);
put(CHANGE_KIND, "message").to(PutMessage.class);
child(CHANGE_KIND, "messages").to(ChangeMessages.class);
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index 4da8410c9f..63edb7b8ee 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -21,6 +21,7 @@ import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdate
import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
@@ -88,6 +89,8 @@ import org.eclipse.jgit.util.ChangeIdUtil;
@Singleton
public class CherryPickChange {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
@AutoValue
abstract static class Result {
static Result create(Change.Id changeId, ImmutableSet<String> filesWithGitConflicts) {
@@ -374,6 +377,7 @@ public class CherryPickChange {
input.parent - 1,
input.allowEmpty,
input.allowConflicts);
+ logger.atFine().log("flushing inserter %s", oi);
oi.flush();
} catch (MergeIdenticalTreeException | MergeConflictException e) {
throw new IntegrationConflictException("Cherry pick failed: " + e.getMessage(), e);
diff --git a/java/com/google/gerrit/server/restapi/change/CommentJson.java b/java/com/google/gerrit/server/restapi/change/CommentJson.java
index 2d0c739323..272afc95d0 100644
--- a/java/com/google/gerrit/server/restapi/change/CommentJson.java
+++ b/java/com/google/gerrit/server/restapi/change/CommentJson.java
@@ -227,6 +227,7 @@ public class CommentJson {
r.author = loader.get(c.author.getId());
}
r.commitId = c.getCommitId().getName();
+ r.fixSuggestions = toFixSuggestionInfos(c.fixSuggestions);
}
protected Range toRange(Comment.Range commentRange) {
@@ -240,32 +241,6 @@ public class CommentJson {
}
return range;
}
- }
-
- public class HumanCommentFormatter extends BaseCommentFormatter<HumanComment, CommentInfo> {
- @Override
- protected CommentInfo toInfo(HumanComment c, AccountLoader loader) {
- CommentInfo ci = new CommentInfo();
- fillCommentInfo(c, ci, loader);
- ci.unresolved = c.unresolved;
- return ci;
- }
-
- private HumanCommentFormatter() {}
- }
-
- class RobotCommentFormatter extends BaseCommentFormatter<RobotComment, RobotCommentInfo> {
- @Override
- protected RobotCommentInfo toInfo(RobotComment c, AccountLoader loader) {
- RobotCommentInfo rci = new RobotCommentInfo();
- rci.robotId = c.robotId;
- rci.robotRunId = c.robotRunId;
- rci.url = c.url;
- rci.properties = c.properties;
- rci.fixSuggestions = toFixSuggestionInfos(c.fixSuggestions);
- fillCommentInfo(c, rci, loader);
- return rci;
- }
@Nullable
private List<FixSuggestionInfo> toFixSuggestionInfos(
@@ -293,6 +268,31 @@ public class CommentJson {
fixReplacementInfo.replacement = fixReplacement.replacement;
return fixReplacementInfo;
}
+ }
+
+ public class HumanCommentFormatter extends BaseCommentFormatter<HumanComment, CommentInfo> {
+ @Override
+ protected CommentInfo toInfo(HumanComment c, AccountLoader loader) {
+ CommentInfo ci = new CommentInfo();
+ fillCommentInfo(c, ci, loader);
+ ci.unresolved = c.unresolved;
+ return ci;
+ }
+
+ private HumanCommentFormatter() {}
+ }
+
+ class RobotCommentFormatter extends BaseCommentFormatter<RobotComment, RobotCommentInfo> {
+ @Override
+ protected RobotCommentInfo toInfo(RobotComment c, AccountLoader loader) {
+ RobotCommentInfo rci = new RobotCommentInfo();
+ rci.robotId = c.robotId;
+ rci.robotRunId = c.robotRunId;
+ rci.url = c.url;
+ rci.properties = c.properties;
+ fillCommentInfo(c, rci, loader);
+ return rci;
+ }
private RobotCommentFormatter() {}
}
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index 5146a97c81..e254bfc38a 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -26,15 +26,18 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.converter.ChangeInputProtoConverter;
import com.google.gerrit.exceptions.InvalidMergeStrategyException;
import com.google.gerrit.exceptions.MergeWithConflictsNotSupportedException;
import com.google.gerrit.extensions.api.accounts.AccountInput;
+import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.ListChangesOption;
@@ -52,6 +55,7 @@ import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.proto.Entities;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
@@ -96,6 +100,7 @@ import java.time.ZoneId;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.InvalidObjectIdException;
import org.eclipse.jgit.errors.MissingObjectException;
@@ -118,6 +123,8 @@ import org.eclipse.jgit.util.ChangeIdUtil;
public class CreateChange
implements RestCollectionModifyView<TopLevelResource, ChangeResource, ChangeInput> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final ChangeInputProtoConverter CHANGE_INPUT_PROTO_CONVERTER =
+ ChangeInputProtoConverter.INSTANCE;
private final BatchUpdate.Factory updateFactory;
private final String anonymousCowardName;
@@ -191,11 +198,51 @@ public class CreateChange
return execute(updateFactory, input, projectsCollection.parse(input.project));
}
- /** Creates the changes in the given project. This is public for reuse in the project API. */
+ @UsedAt(UsedAt.Project.GOOGLE)
+ @FunctionalInterface
+ public interface CommitTreeSupplier {
+ @NonNull
+ ObjectId get(Repository repo, ObjectInserter oi, ChangeInput input, RevCommit mergeTip)
+ throws IOException, RestApiException;
+ }
+
+ /**
+ * Creates the changes in the given project, using the proto representation of ChangeInput -
+ * {@link com.google.gerrit.proto.Entities.ChangeInput}.
+ */
+ @UsedAt(UsedAt.Project.GOOGLE)
+ public Response<ChangeInfo> execute(
+ BatchUpdate.Factory updateFactory,
+ Entities.ChangeInput input,
+ CommitTreeSupplier commitTreeSupplier)
+ throws IOException, RestApiException, UpdateException, PermissionBackendException,
+ ConfigInvalidException {
+ return execute(
+ updateFactory,
+ CHANGE_INPUT_PROTO_CONVERTER.fromProto(input),
+ projectsCollection.parse(input.getProject()),
+ Optional.of(commitTreeSupplier));
+ }
+
+ /**
+ * Creates the changes in the given project, using the java-class representation of ChangeInput -
+ * {@link com.google.gerrit.extensions.common.ChangeInput}. This is public for reuse in the
+ * project API.
+ */
public Response<ChangeInfo> execute(
BatchUpdate.Factory updateFactory, ChangeInput input, ProjectResource projectResource)
throws IOException, RestApiException, UpdateException, PermissionBackendException,
ConfigInvalidException {
+ return execute(updateFactory, input, projectResource, Optional.empty());
+ }
+
+ private Response<ChangeInfo> execute(
+ BatchUpdate.Factory updateFactory,
+ ChangeInput input,
+ ProjectResource projectResource,
+ Optional<CommitTreeSupplier> commitTreeSupplier)
+ throws IOException, RestApiException, UpdateException, PermissionBackendException,
+ ConfigInvalidException {
if (!user.get().isIdentifiedUser()) {
throw new AuthException("Authentication required");
}
@@ -204,14 +251,15 @@ public class CreateChange
projectState.checkStatePermitsWrite();
IdentifiedUser me = user.get().asIdentifiedUser();
- checkAndSanitizeChangeInput(input, me);
+ checkAndSanitizeChangeInput(input, me, commitTreeSupplier);
Project.NameKey project = projectResource.getNameKey();
contributorAgreements.check(project, user.get());
checkRequiredPermissions(project, input.branch, input.author);
- ChangeInfo newChange = createNewChange(input, me, projectState, updateFactory);
+ ChangeInfo newChange =
+ createNewChange(input, me, projectState, updateFactory, commitTreeSupplier);
return Response.created(newChange);
}
@@ -225,7 +273,8 @@ public class CreateChange
* @param me the user who sent the current request to create a change.
* @throws BadRequestException if the input is not legal.
*/
- private void checkAndSanitizeChangeInput(ChangeInput input, IdentifiedUser me)
+ private void checkAndSanitizeChangeInput(
+ ChangeInput input, IdentifiedUser me, Optional<CommitTreeSupplier> commitTreeSupplier)
throws RestApiException, PermissionBackendException, IOException {
if (Strings.isNullOrEmpty(input.branch)) {
throw new BadRequestException("branch must be non-empty");
@@ -303,6 +352,11 @@ public class CreateChange
throw new BadRequestException("Only one of `merge` and `patch` arguments can be set.");
}
+ if ((input.merge != null || input.patch != null) && commitTreeSupplier.isPresent()) {
+ throw new BadRequestException(
+ "`CommitTreeSupplier` cannot be provided along with `merge` or `patch` arguments");
+ }
+
if (input.author != null
&& (Strings.isNullOrEmpty(input.author.email)
|| Strings.isNullOrEmpty(input.author.name))) {
@@ -332,7 +386,8 @@ public class CreateChange
ChangeInput input,
IdentifiedUser me,
ProjectState projectState,
- BatchUpdate.Factory updateFactory)
+ BatchUpdate.Factory updateFactory,
+ Optional<CommitTreeSupplier> commitTreeSupplier)
throws RestApiException, PermissionBackendException, IOException, ConfigInvalidException,
UpdateException {
try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
@@ -404,35 +459,28 @@ public class CreateChange
}
} else if (input.patch != null) {
// create a commit with the given patch.
- if (mergeTip == null) {
- throw new BadRequestException("Cannot apply patch on top of an empty tree.");
- }
- PatchApplier.Result applyResult =
- ApplyPatchUtil.applyPatch(git, oi, input.patch, mergeTip);
- ObjectId treeId = applyResult.getTreeId();
- String appliedPatchCommitMessage =
- getCommitMessage(
- ApplyPatchUtil.buildCommitMessage(
- input.subject,
- ImmutableList.of(),
- input.patch.patch,
- ApplyPatchUtil.getResultPatch(git, reader, mergeTip, rw.lookupTree(treeId)),
- applyResult.getErrors()),
- me);
c =
- rw.parseCommit(
- CommitUtil.createCommitWithTree(
- oi,
- author,
- committer,
- ImmutableList.of(mergeTip),
- appliedPatchCommitMessage,
- treeId));
+ createCommitWithPatch(
+ git, reader, oi, rw, mergeTip, input.patch, input.subject, author, committer, me);
+ } else if (commitTreeSupplier.isPresent()) {
+ c =
+ createCommitWithSuppliedTree(
+ git,
+ oi,
+ rw,
+ mergeTip,
+ input,
+ commitTreeSupplier.get(),
+ author,
+ committer,
+ commitMessage);
+
} else {
// create an empty commit.
c = createEmptyCommit(oi, rw, author, committer, mergeTip, commitMessage);
}
// Flush inserter so that commit becomes visible to validators
+ logger.atFine().log("flushing inserter %s", oi);
oi.flush();
Change.Id changeId = Change.id(seq.nextChangeId());
@@ -604,12 +652,70 @@ public class CreateChange
@Nullable RevCommit mergeTip,
String commitMessage)
throws IOException {
- logger.atFine().log("Creating empty commit");
- ObjectId treeID = mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree().getId();
+ logger.atFine().log("Creating empty commit (mergeTip = %s)", mergeTip);
+ ObjectId treeId = mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree().getId();
+ logger.atFine().log("Tree ID of empty commit: %s", treeId.name());
List<RevCommit> parents = mergeTip == null ? ImmutableList.of() : ImmutableList.of(mergeTip);
return rw.parseCommit(
CommitUtil.createCommitWithTree(
- oi, authorIdent, committerIdent, parents, commitMessage, treeID));
+ oi, authorIdent, committerIdent, parents, commitMessage, treeId));
+ }
+
+ private CodeReviewCommit createCommitWithPatch(
+ Repository repo,
+ ObjectReader reader,
+ ObjectInserter oi,
+ CodeReviewRevWalk rw,
+ RevCommit mergeTip,
+ ApplyPatchInput patch,
+ String subject,
+ PersonIdent authorIdent,
+ PersonIdent committerIdent,
+ IdentifiedUser me)
+ throws IOException, RestApiException {
+ if (mergeTip == null) {
+ throw new BadRequestException("Cannot apply patch on top of an empty tree.");
+ }
+ PatchApplier.Result applyResult = ApplyPatchUtil.applyPatch(repo, oi, patch, mergeTip);
+ ObjectId treeId = applyResult.getTreeId();
+ logger.atFine().log("tree ID after applying patch: %s", treeId.name());
+ String appliedPatchCommitMessage =
+ getCommitMessage(
+ ApplyPatchUtil.buildCommitMessage(
+ subject,
+ ImmutableList.of(),
+ patch.patch,
+ ApplyPatchUtil.getResultPatch(repo, reader, mergeTip, rw.lookupTree(treeId)),
+ applyResult.getErrors()),
+ me);
+ return rw.parseCommit(
+ CommitUtil.createCommitWithTree(
+ oi,
+ authorIdent,
+ committerIdent,
+ ImmutableList.of(mergeTip),
+ appliedPatchCommitMessage,
+ treeId));
+ }
+
+ private static CodeReviewCommit createCommitWithSuppliedTree(
+ Repository repo,
+ ObjectInserter oi,
+ CodeReviewRevWalk rw,
+ RevCommit mergeTip,
+ ChangeInput input,
+ CommitTreeSupplier commitTreeSupplier,
+ PersonIdent authorIdent,
+ PersonIdent committerIdent,
+ String commitMessage)
+ throws IOException, RestApiException {
+ if (mergeTip == null) {
+ throw new BadRequestException("`CommitTreeSupplier` cannot be used on top of an empty tree.");
+ }
+ ObjectId treeId = commitTreeSupplier.get(repo, oi, input, mergeTip);
+ return rw.parseCommit(
+ CommitUtil.createCommitWithTree(
+ oi, authorIdent, committerIdent, ImmutableList.of(mergeTip), commitMessage, treeId));
}
private static ObjectId emptyTreeId(ObjectInserter inserter) throws IOException {
@@ -653,17 +759,20 @@ public class CreateChange
logger.atFine().log("merge strategy = %s", mergeStrategy);
try {
- return MergeUtil.createMergeCommit(
- oi,
- repo.getConfig(),
- mergeTip,
- sourceCommit,
- mergeStrategy,
- merge.allowConflicts,
- authorIdent,
- committerIdent,
- commitMessage,
- rw);
+ CodeReviewCommit mergeCommit =
+ MergeUtil.createMergeCommit(
+ oi,
+ repo.getConfig(),
+ mergeTip,
+ sourceCommit,
+ mergeStrategy,
+ merge.allowConflicts,
+ authorIdent,
+ committerIdent,
+ commitMessage,
+ rw);
+ logger.atFine().log("tree ID of merge commit: %s", mergeCommit.getTree().getId().name());
+ return mergeCommit;
} catch (NoMergeBaseException e) {
throw new ResourceConflictException(
String.format("Cannot create merge commit: %s", e.getMessage()), e);
diff --git a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
index 8849c8227a..c386b297ea 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
@@ -41,6 +41,7 @@ import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.PublishCommentUtil;
+import com.google.gerrit.server.change.CommentsValidator;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -134,6 +135,8 @@ public class CreateDraftComment implements RestModifyView<RevisionResource, Draf
rsrc.getPatchSet(),
commentsUtil);
+ CommentsValidator.ensureFixSuggestionsAreAddable(in.fixSuggestions, in.path);
+
CommentValidationContext ctx =
CommentValidationContext.create(
rsrc.getChange().getChangeId(),
@@ -182,7 +185,8 @@ public class CreateDraftComment implements RestModifyView<RevisionResource, Draf
draftInput.side(),
draftInput.message.trim(),
draftInput.unresolved,
- parentUuid);
+ parentUuid,
+ CommentsUtil.createFixSuggestionsFromInput(draftInput.fixSuggestions));
comment.setLineNbrAndRange(draftInput.line, draftInput.range);
comment.tag = draftInput.tag;
diff --git a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
index 989dc7a943..d3561fc304 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
@@ -19,6 +19,7 @@ import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdate
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Iterables;
import com.google.gerrit.entities.BranchNameKey;
@@ -172,7 +173,7 @@ public class CreateMergePatchSet implements RestModifyView<ChangeResource, Merge
}
RevCommit currentPsCommit;
- List<String> groups = null;
+ ImmutableList<String> groups = null;
if (!in.inheritParent && !in.baseChange.isEmpty()) {
PatchSet basePS = findBasePatchSet(in.baseChange);
currentPsCommit = rw.parseCommit(basePS.commitId());
diff --git a/java/com/google/gerrit/server/restapi/change/Fixes.java b/java/com/google/gerrit/server/restapi/change/Fixes.java
index 38240e3a98..080c1e7d7f 100644
--- a/java/com/google/gerrit/server/restapi/change/Fixes.java
+++ b/java/com/google/gerrit/server/restapi/change/Fixes.java
@@ -14,8 +14,8 @@
package com.google.gerrit.server.restapi.change;
+import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.FixSuggestion;
-import com.google.gerrit.entities.RobotComment;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.ChildCollection;
import com.google.gerrit.extensions.restapi.IdString;
@@ -27,6 +27,7 @@ import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -53,10 +54,13 @@ public class Fixes implements ChildCollection<RevisionResource, FixResource> {
String fixId = id.get();
ChangeNotes changeNotes = revisionResource.getNotes();
- List<RobotComment> robotComments =
- commentsUtil.robotCommentsByPatchSet(changeNotes, revisionResource.getPatchSet().id());
- for (RobotComment robotComment : robotComments) {
- for (FixSuggestion fixSuggestion : robotComment.fixSuggestions) {
+ List<Comment> allComments = new ArrayList<>();
+ allComments.addAll(
+ commentsUtil.publishedByPatchSet(changeNotes, revisionResource.getPatchSet().id()));
+ allComments.addAll(
+ commentsUtil.robotCommentsByPatchSet(changeNotes, revisionResource.getPatchSet().id()));
+ for (Comment comment : allComments) {
+ for (FixSuggestion fixSuggestion : comment.fixSuggestions) {
if (Objects.equals(fixId, fixSuggestion.fixId)) {
return new FixResource(revisionResource, fixSuggestion.replacements);
}
diff --git a/java/com/google/gerrit/server/restapi/change/GetBlame.java b/java/com/google/gerrit/server/restapi/change/GetBlame.java
index 04828f257e..bc2755e831 100644
--- a/java/com/google/gerrit/server/restapi/change/GetBlame.java
+++ b/java/com/google/gerrit/server/restapi/change/GetBlame.java
@@ -25,10 +25,8 @@ import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.change.FileResource;
-import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.InMemoryInserter;
-import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.patch.AutoMerger;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gitiles.blame.cache.BlameCache;
@@ -38,13 +36,11 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
-import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.kohsuke.args4j.Option;
@@ -53,7 +49,6 @@ public class GetBlame implements RestReadView<FileResource> {
private final GitRepositoryManager repoManager;
private final BlameCache blameCache;
- private final ThreeWayMergeStrategy mergeStrategy;
private final AutoMerger autoMerger;
@Option(
@@ -65,14 +60,9 @@ public class GetBlame implements RestReadView<FileResource> {
private boolean base;
@Inject
- GetBlame(
- GitRepositoryManager repoManager,
- BlameCache blameCache,
- @GerritServerConfig Config cfg,
- AutoMerger autoMerger) {
+ GetBlame(GitRepositoryManager repoManager, BlameCache blameCache, AutoMerger autoMerger) {
this.repoManager = repoManager;
this.blameCache = blameCache;
- this.mergeStrategy = MergeUtil.getMergeStrategy(cfg);
this.autoMerger = autoMerger;
}
@@ -116,8 +106,7 @@ public class GetBlame implements RestReadView<FileResource> {
} else if (parents.length == 2) {
ObjectId automerge =
- autoMerger.lookupFromGitOrMergeInMemory(
- repository, revWalk, ins, revCommit, mergeStrategy);
+ autoMerger.lookupFromGitOrMergeInMemory(repository, revWalk, ins, revCommit);
result = blame(automerge, path, repository, revWalk);
} else {
diff --git a/java/com/google/gerrit/server/restapi/change/GetDiff.java b/java/com/google/gerrit/server/restapi/change/GetDiff.java
index 942419859b..3de0f279d9 100644
--- a/java/com/google/gerrit/server/restapi/change/GetDiff.java
+++ b/java/com/google/gerrit/server/restapi/change/GetDiff.java
@@ -19,6 +19,7 @@ import static com.google.gerrit.server.project.ProjectCache.illegalState;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.PatchScript;
import com.google.gerrit.entities.PatchSet;
@@ -256,21 +257,25 @@ public class GetDiff implements RestReadView<FileResource> {
}
}
+ @CanIgnoreReturnValue
public GetDiff setBase(String base) {
this.base = base;
return this;
}
+ @CanIgnoreReturnValue
public GetDiff setParent(int parentNum) {
this.parentNum = parentNum;
return this;
}
+ @CanIgnoreReturnValue
public GetDiff setIntraline(boolean intraline) {
this.intraline = intraline;
return this;
}
+ @CanIgnoreReturnValue
public GetDiff setWhitespace(Whitespace whitespace) {
this.whitespace = whitespace;
return this;
diff --git a/java/com/google/gerrit/server/restapi/change/GetMessage.java b/java/com/google/gerrit/server/restapi/change/GetMessage.java
new file mode 100644
index 0000000000..5715caa545
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/GetMessage.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.change;
+
+import static java.util.stream.Collectors.toMap;
+
+import com.google.gerrit.extensions.common.CommitMessageInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.eclipse.jgit.revwalk.FooterLine;
+
+@Singleton
+public class GetMessage implements RestReadView<ChangeResource> {
+ private final ChangeData.Factory changeDataFactory;
+
+ @Inject
+ GetMessage(ChangeData.Factory changeDataFactory) {
+ this.changeDataFactory = changeDataFactory;
+ }
+
+ @Override
+ public Response<CommitMessageInfo> apply(ChangeResource resource)
+ throws AuthException, BadRequestException, ResourceConflictException, Exception {
+ CommitMessageInfo commitMessageInfo = new CommitMessageInfo();
+ commitMessageInfo.subject = resource.getChange().getSubject();
+
+ ChangeData cd = changeDataFactory.create(resource.getNotes());
+ commitMessageInfo.fullMessage = cd.commitMessage();
+ commitMessageInfo.footers =
+ cd.commitFooters().stream().collect(toMap(FooterLine::getKey, FooterLine::getValue));
+
+ return Response.ok(commitMessageInfo);
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeComments.java b/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
index c90e4fce86..f5356f892e 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
@@ -87,7 +87,7 @@ public class ListChangeComments implements RestReadView<ChangeResource> {
return getAsList(listComments(rsrc), rsrc);
}
- private Iterable<HumanComment> listComments(ChangeResource rsrc) {
+ private List<HumanComment> listComments(ChangeResource rsrc) {
ChangeData cd = changeDataFactory.create(rsrc.getNotes());
return commentsUtil.publishedHumanCommentsByChange(cd.notes());
}
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java b/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
index 9faa9b57ab..e5d11f5806 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
@@ -70,7 +70,7 @@ public class ListChangeDrafts implements RestReadView<ChangeResource> {
this.draftCommentsReader = draftCommentsReader;
}
- private Iterable<HumanComment> listComments(ChangeResource rsrc) {
+ private List<HumanComment> listComments(ChangeResource rsrc) {
ChangeData cd = changeDataFactory.create(rsrc.getNotes());
return draftCommentsReader.getDraftsByChangeAndDraftAuthor(
cd.notes(), rsrc.getUser().getAccountId());
diff --git a/java/com/google/gerrit/server/restapi/change/ListRobotComments.java b/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
index 25f40058cc..c5feb9649f 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
@@ -78,7 +78,7 @@ public class ListRobotComments implements RestReadView<RevisionResource> {
return commentInfosMap;
}
- private Iterable<RobotComment> listComments(RevisionResource rsrc) {
+ private List<RobotComment> listComments(RevisionResource rsrc) {
return commentsUtil.robotCommentsByPatchSet(rsrc.getNotes(), rsrc.getPatchSet().id());
}
diff --git a/java/com/google/gerrit/server/restapi/change/Mergeable.java b/java/com/google/gerrit/server/restapi/change/Mergeable.java
index 9797bda579..8d6f03e831 100644
--- a/java/com/google/gerrit/server/restapi/change/Mergeable.java
+++ b/java/com/google/gerrit/server/restapi/change/Mergeable.java
@@ -16,6 +16,7 @@ package com.google.gerrit.server.restapi.change;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.BranchOrderSection;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
@@ -43,7 +44,6 @@ import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -126,7 +126,7 @@ public class Mergeable implements RestReadView<RevisionResource> {
Optional<BranchOrderSection> branchOrder = projectState.getBranchOrderSection();
if (branchOrder.isPresent()) {
int prefixLen = Constants.R_HEADS.length();
- List<String> names = branchOrder.get().getMoreStable(ref.getName());
+ ImmutableList<String> names = branchOrder.get().getMoreStable(ref.getName());
Map<String, Ref> refs =
git.getRefDatabase().exactRef(names.toArray(new String[names.size()]));
for (String n : names) {
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 32474a41af..3c30b845db 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -16,12 +16,10 @@ package com.google.gerrit.server.restapi.change;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
-import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
import static com.google.gerrit.server.permissions.AbstractLabelPermission.ForUser.ON_BEHALF_OF;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
@@ -42,7 +40,6 @@ import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelTypes;
-import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -52,12 +49,8 @@ import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
import com.google.gerrit.extensions.api.changes.ReviewResult;
import com.google.gerrit.extensions.api.changes.ReviewerInput;
import com.google.gerrit.extensions.api.changes.ReviewerResult;
-import com.google.gerrit.extensions.client.Comment.Range;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.client.Side;
-import com.google.gerrit.extensions.common.FixReplacementInfo;
-import com.google.gerrit.extensions.common.FixSuggestionInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -70,7 +63,6 @@ import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Field;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.IdentifiedUser;
@@ -81,6 +73,7 @@ import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.CommentsValidator;
import com.google.gerrit.server.change.ModifyReviewersEmail;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.ReviewerModifier;
@@ -92,11 +85,6 @@ import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.extensions.events.ReviewerAdded;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.patch.DiffSummary;
-import com.google.gerrit.server.patch.DiffSummaryKey;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListKey;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.LabelPermission;
@@ -106,6 +94,8 @@ import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.BatchUpdates;
+import com.google.gerrit.server.update.RetryHelper;
import com.google.gerrit.server.update.UpdateException;
import com.google.gerrit.server.update.context.RefUpdateContext;
import com.google.gerrit.server.util.time.TimeUtil;
@@ -115,17 +105,14 @@ import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
-import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
@Singleton
public class PostReview implements RestModifyView<RevisionResource, ReviewInput> {
@@ -158,16 +145,13 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
public static final String ERROR_WIP_READY_MUTUALLY_EXCLUSIVE =
"work_in_progress and ready are mutually exclusive";
- private final BatchUpdate.Factory updateFactory;
+ private final RetryHelper retryHelper;
private final PostReviewOp.Factory postReviewOpFactory;
private final ChangeResource.Factory changeResourceFactory;
- private final ChangeData.Factory changeDataFactory;
private final AccountCache accountCache;
private final ApprovalsUtil approvalsUtil;
- private final CommentsUtil commentsUtil;
private final DraftCommentsReader draftCommentsReader;
- private final PatchListCache patchListCache;
private final AccountResolver accountResolver;
private final ReviewerModifier reviewerModifier;
private final Metrics metrics;
@@ -181,18 +165,16 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
private final ReviewerAdded reviewerAdded;
private final boolean strictLabels;
private final ChangeJson.Factory changeJsonFactory;
+ private final CommentsValidator commentsValidator;
@Inject
PostReview(
- BatchUpdate.Factory updateFactory,
+ RetryHelper retryHelper,
PostReviewOp.Factory postReviewOpFactory,
ChangeResource.Factory changeResourceFactory,
- ChangeData.Factory changeDataFactory,
AccountCache accountCache,
ApprovalsUtil approvalsUtil,
- CommentsUtil commentsUtil,
DraftCommentsReader draftCommentsReader,
- PatchListCache patchListCache,
AccountResolver accountResolver,
ReviewerModifier reviewerModifier,
Metrics metrics,
@@ -204,15 +186,13 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
PermissionBackend permissionBackend,
ReplyAttentionSetUpdates replyAttentionSetUpdates,
ReviewerAdded reviewerAdded,
- ChangeJson.Factory changeJsonFactory) {
- this.updateFactory = updateFactory;
+ ChangeJson.Factory changeJsonFactory,
+ CommentsValidator commentsValidator) {
+ this.retryHelper = retryHelper;
this.postReviewOpFactory = postReviewOpFactory;
this.changeResourceFactory = changeResourceFactory;
- this.changeDataFactory = changeDataFactory;
this.accountCache = accountCache;
- this.commentsUtil = commentsUtil;
this.draftCommentsReader = draftCommentsReader;
- this.patchListCache = patchListCache;
this.approvalsUtil = approvalsUtil;
this.accountResolver = accountResolver;
this.reviewerModifier = reviewerModifier;
@@ -226,6 +206,7 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
this.reviewerAdded = reviewerAdded;
this.strictLabels = gerritConfig.getBoolean("change", "strictLabels", false);
this.changeJsonFactory = changeJsonFactory;
+ this.commentsValidator = commentsValidator;
}
@Override
@@ -261,7 +242,7 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
}
if (input.comments != null) {
input.comments = cleanUpComments(input.comments);
- checkComments(revision, input.comments);
+ commentsValidator.checkComments(revision, input.comments);
}
if (input.draftIdsToPublish != null) {
checkDraftIds(revision, input.draftIdsToPublish, input.drafts);
@@ -311,99 +292,58 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
}
output.labels = input.labels;
- // Notify based on ReviewInput, ignoring the notify settings from any ReviewerInputs.
- NotifyResolver.Result notify = notifyResolver.resolve(input.notify, input.notifyDetails);
- try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
- try (BatchUpdate bu =
- updateFactory.create(revision.getChange().getProject(), revision.getUser(), ts)) {
- bu.setNotify(notify);
-
- Account account = revision.getUser().asIdentifiedUser().getAccount();
- boolean ccOrReviewer = false;
- if (input.labels != null && !input.labels.isEmpty()) {
- ccOrReviewer = input.labels.values().stream().anyMatch(v -> v != 0);
- if (ccOrReviewer) {
- logger.atFine().log(
- "calling user is cc/reviewer on the change due to voting on a label");
- }
- }
+ Account account = revision.getUser().asIdentifiedUser().getAccount();
+ boolean ccOrReviewer = false;
+ if (input.labels != null && !input.labels.isEmpty()) {
+ ccOrReviewer = input.labels.values().stream().anyMatch(v -> v != 0);
+ if (ccOrReviewer) {
+ logger.atFine().log("calling user is cc/reviewer on the change due to voting on a label");
+ }
+ }
- if (!ccOrReviewer) {
- // Check if user was already CCed or reviewing prior to this review.
- ReviewerSet currentReviewers =
- approvalsUtil.getReviewers(revision.getChangeResource().getNotes());
- ccOrReviewer = currentReviewers.all().contains(account.id());
- if (ccOrReviewer) {
- logger.atFine().log("calling user is already cc/reviewer on the change");
- }
- }
+ if (!ccOrReviewer) {
+ // Check if user was already CCed or reviewing prior to this review.
+ ReviewerSet currentReviewers =
+ approvalsUtil.getReviewers(revision.getChangeResource().getNotes());
+ ccOrReviewer = currentReviewers.all().contains(account.id());
+ if (ccOrReviewer) {
+ logger.atFine().log("calling user is already cc/reviewer on the change");
+ }
+ }
- // Apply reviewer changes first. Revision emails should be sent to the
- // updated set of reviewers. Also keep track of whether the user added
- // themselves as a reviewer or to the CC list.
- logger.atFine().log("adding reviewer additions");
- for (ReviewerModification reviewerResult : reviewerResults) {
- reviewerResult.op.suppressEmail(); // Send a single batch email below.
- reviewerResult.op.suppressEvent(); // Send events below, if possible as batch.
- bu.addOp(revision.getChange().getId(), reviewerResult.op);
- if (!ccOrReviewer && reviewerResult.reviewers.contains(account)) {
- logger.atFine().log("calling user is explicitly added as reviewer or CC");
- ccOrReviewer = true;
- }
- }
+ for (ReviewerModification reviewerResult : reviewerResults) {
+ reviewerResult.op.suppressEmail(); // Send a single batch email below.
+ reviewerResult.op.suppressEvent(); // Send events below, if possible as batch.
+ if (!ccOrReviewer && reviewerResult.reviewers.contains(account)) {
+ logger.atFine().log("calling user is explicitly added as reviewer or CC");
+ ccOrReviewer = true;
+ }
+ }
- if (!ccOrReviewer) {
- // User posting this review isn't currently in the reviewer or CC list,
- // isn't being explicitly added, and isn't voting on any label.
- // Automatically CC them on this change so they receive replies.
- logger.atFine().log("CCing calling user");
- ReviewerModification selfAddition =
- reviewerModifier.ccCurrentUser(revision.getUser(), revision);
- selfAddition.op.suppressEmail();
- selfAddition.op.suppressEvent();
- bu.addOp(revision.getChange().getId(), selfAddition.op);
- }
+ // Notify based on ReviewInput, ignoring the notify settings from any ReviewerInputs.
+ NotifyResolver.Result notify = notifyResolver.resolve(input.notify, input.notifyDetails);
- // Add WorkInProgressOp if requested.
- if ((input.ready || input.workInProgress)
- && didWorkInProgressChange(revision.getChange().isWorkInProgress(), input)) {
- if (input.ready && input.workInProgress) {
- output.error = ERROR_WIP_READY_MUTUALLY_EXCLUSIVE;
- return Response.withStatusCode(SC_BAD_REQUEST, output);
- }
-
- revision
- .getChangeResource()
- .permissions()
- .check(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE);
-
- if (input.ready) {
- output.ready = true;
- }
-
- logger.atFine().log("setting work-in-progress to %s", input.workInProgress);
- WorkInProgressOp wipOp =
- workInProgressOpFactory.create(input.workInProgress, new WorkInProgressOp.Input());
- wipOp.suppressEmail();
- bu.addOp(revision.getChange().getId(), wipOp);
- }
+ if ((input.ready || input.workInProgress)
+ && didWorkInProgressChange(revision.getChange().isWorkInProgress(), input)) {
+ if (input.ready && input.workInProgress) {
+ output.error = ERROR_WIP_READY_MUTUALLY_EXCLUSIVE;
+ return Response.withStatusCode(SC_BAD_REQUEST, output);
+ }
+
+ revision
+ .getChangeResource()
+ .permissions()
+ .check(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE);
- // Add the review ops.
- logger.atFine().log("posting review");
- PostReviewOp postReviewOp =
- postReviewOpFactory.create(
- projectState, revision.getPatchSet().id(), input, revision.getAccountId());
- bu.addOp(revision.getChange().getId(), postReviewOp);
-
- // Adjust the attention set based on the input
- replyAttentionSetUpdates.updateAttentionSet(
- bu, revision.getNotes(), input, revision.getUser());
- bu.execute();
+ if (input.ready) {
+ output.ready = true;
}
}
- // Re-read change to take into account results of the update.
- ChangeData cd = changeDataFactory.create(revision.getProject(), revision.getChange().getId());
+ BatchUpdates.Result batchUpdateResult =
+ runBatchUpdate(projectState, revision, input, ts, notify, reviewerResults, ccOrReviewer);
+ ChangeData cd =
+ batchUpdateResult.getChangeData(revision.getProject(), revision.getChange().getId());
for (ReviewerModification reviewerResult : reviewerResults) {
reviewerResult.gatherResults(cd);
}
@@ -415,11 +355,83 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
if (input.responseFormatOptions != null) {
output.changeInfo = changeJsonFactory.create(input.responseFormatOptions).format(cd);
+ } else {
+ output.changeInfo = changeJsonFactory.noOptions().format(cd);
}
return Response.ok(output);
}
+ private BatchUpdates.Result runBatchUpdate(
+ ProjectState projectState,
+ RevisionResource revision,
+ ReviewInput input,
+ Instant ts,
+ NotifyResolver.Result notify,
+ List<ReviewerModification> reviewerResults,
+ boolean ccOrReviewer)
+ throws UpdateException, RestApiException {
+ return retryHelper
+ .changeUpdate(
+ "batchUpdate",
+ updateFactory -> {
+ try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+ try (BatchUpdate bu =
+ updateFactory.create(
+ revision.getChange().getProject(), revision.getUser(), ts)) {
+ bu.setNotify(notify);
+
+ // Apply reviewer changes first. Revision emails should be sent to the
+ // updated set of reviewers. Also keep track of whether the user added
+ // themselves as a reviewer or to the CC list.
+ logger.atFine().log("adding reviewer additions");
+ reviewerResults.forEach(
+ reviewerResult -> bu.addOp(revision.getChange().getId(), reviewerResult.op));
+
+ if (!ccOrReviewer) {
+ // User posting this review isn't currently in the reviewer or CC list,
+ // isn't being explicitly added, and isn't voting on any label.
+ // Automatically CC them on this change so they receive replies.
+ logger.atFine().log("CCing calling user");
+ ReviewerModification selfAddition =
+ reviewerModifier.ccCurrentUser(revision.getUser(), revision);
+ selfAddition.op.suppressEmail();
+ selfAddition.op.suppressEvent();
+ bu.addOp(revision.getChange().getId(), selfAddition.op);
+ }
+
+ // Add WorkInProgressOp if requested.
+ if ((input.ready || input.workInProgress)
+ && didWorkInProgressChange(revision.getChange().isWorkInProgress(), input)) {
+ logger.atFine().log("setting work-in-progress to %s", input.workInProgress);
+ WorkInProgressOp wipOp =
+ workInProgressOpFactory.create(
+ input.workInProgress, new WorkInProgressOp.Input());
+ wipOp.suppressEmail();
+ bu.addOp(revision.getChange().getId(), wipOp);
+ }
+
+ // Add the review ops.
+ logger.atFine().log("posting review");
+ PostReviewOp postReviewOp =
+ postReviewOpFactory.create(
+ projectState,
+ revision.getPatchSet().id(),
+ input,
+ revision.getAccountId());
+ bu.addOp(revision.getChange().getId(), postReviewOp);
+
+ // Adjust the attention set based on the input
+ replyAttentionSetUpdates.updateAttentionSetOnPostReview(
+ bu, postReviewOp, revision.getNotes(), input, revision.getUser());
+
+ return bu.execute();
+ }
+ }
+ })
+ .call();
+ }
+
private boolean didWorkInProgressChange(boolean currentWorkInProgress, ReviewInput input) {
return input.ready == currentWorkInProgress || input.workInProgress != currentWorkInProgress;
}
@@ -667,27 +679,6 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
.collect(toList());
}
- private <T extends com.google.gerrit.extensions.client.Comment> void checkComments(
- RevisionResource revision, Map<String, List<T>> commentsPerPath)
- throws BadRequestException, PatchListNotAvailableException {
- logger.atFine().log("checking comments");
- Set<String> revisionFilePaths = getAffectedFilePaths(revision);
- for (Map.Entry<String, List<T>> entry : commentsPerPath.entrySet()) {
- String path = entry.getKey();
- PatchSet.Id patchSetId = revision.getPatchSet().id();
- ensurePathRefersToAvailableOrMagicFile(path, revisionFilePaths, patchSetId);
-
- List<T> comments = entry.getValue();
- for (T comment : comments) {
- ensureLineIsNonNegative(comment.line, path);
- ensureCommentNotOnMagicFilesOfAutoMerge(path, comment);
- ensureRangeIsValid(path, comment.range);
- ensureValidPatchsetLevelComment(path, comment);
- ensureValidInReplyTo(revision.getNotes(), comment.inReplyTo);
- }
- }
- }
-
/**
* Asserts that the draft IDs to publish are valid, i.e. they exist and belong to the current
* user. If the {@code draftHandling} parameter is equal to {@link DraftHandling#PUBLISH}, then
@@ -724,59 +715,6 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
}
}
- private Set<String> getAffectedFilePaths(RevisionResource revision)
- throws PatchListNotAvailableException {
- ObjectId newId = revision.getPatchSet().commitId();
- DiffSummaryKey key =
- DiffSummaryKey.fromPatchListKey(
- PatchListKey.againstDefaultBase(newId, Whitespace.IGNORE_NONE));
- DiffSummary ds = patchListCache.getDiffSummary(key, revision.getProject());
- return new HashSet<>(ds.getPaths());
- }
-
- private static void ensurePathRefersToAvailableOrMagicFile(
- String path, Set<String> availableFilePaths, PatchSet.Id patchSetId)
- throws BadRequestException {
- if (!availableFilePaths.contains(path) && !Patch.isMagic(path)) {
- throw new BadRequestException(
- String.format("file %s not found in revision %s", path, patchSetId));
- }
- }
-
- private static void ensureLineIsNonNegative(Integer line, String path)
- throws BadRequestException {
- if (line != null && line < 0) {
- throw new BadRequestException(
- String.format("negative line number %d not allowed on %s", line, path));
- }
- }
-
- private static <T extends com.google.gerrit.extensions.client.Comment>
- void ensureCommentNotOnMagicFilesOfAutoMerge(String path, T comment)
- throws BadRequestException {
- if (Patch.isMagic(path) && comment.side == Side.PARENT && comment.parent == null) {
- throw new BadRequestException(String.format("cannot comment on %s on auto-merge", path));
- }
- }
-
- private static <T extends com.google.gerrit.extensions.client.Comment>
- void ensureValidPatchsetLevelComment(String path, T comment) throws BadRequestException {
- if (path.equals(PATCHSET_LEVEL)
- && (comment.side != null || comment.range != null || comment.line != null)) {
- throw new BadRequestException("Patchset-level comments can't have side, range, or line");
- }
- }
-
- private void ensureValidInReplyTo(ChangeNotes changeNotes, String inReplyTo)
- throws BadRequestException {
- if (inReplyTo != null
- && !commentsUtil.getPublishedHumanComment(changeNotes, inReplyTo).isPresent()
- && !commentsUtil.getRobotComment(changeNotes, inReplyTo).isPresent()) {
- throw new BadRequestException(
- String.format("Invalid inReplyTo, comment %s not found", inReplyTo));
- }
- }
-
private void checkRobotComments(
RevisionResource revision, Map<String, List<RobotCommentInput>> in)
throws BadRequestException, PatchListNotAvailableException {
@@ -786,18 +724,17 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
for (RobotCommentInput c : e.getValue()) {
ensureRobotIdIsSet(c.robotId, commentPath);
ensureRobotRunIdIsSet(c.robotRunId, commentPath);
- ensureFixSuggestionsAreAddable(c.fixSuggestions, commentPath);
// Size is validated later, in CommentLimitsValidator.
}
}
- checkComments(revision, in);
+ commentsValidator.checkComments(revision, in);
}
private static void ensureRobotIdIsSet(String robotId, String commentPath)
throws BadRequestException {
if (robotId == null) {
throw new BadRequestException(
- String.format("robotId is missing for robot comment on %s", commentPath));
+ String.format("robotId is missing for comment on %s", commentPath));
}
}
@@ -805,131 +742,7 @@ public class PostReview implements RestModifyView<RevisionResource, ReviewInput>
throws BadRequestException {
if (robotRunId == null) {
throw new BadRequestException(
- String.format("robotRunId is missing for robot comment on %s", commentPath));
- }
- }
-
- private static void ensureFixSuggestionsAreAddable(
- List<FixSuggestionInfo> fixSuggestionInfos, String commentPath) throws BadRequestException {
- if (fixSuggestionInfos == null) {
- return;
- }
-
- for (FixSuggestionInfo fixSuggestionInfo : fixSuggestionInfos) {
- ensureDescriptionIsSet(commentPath, fixSuggestionInfo.description);
- ensureFixReplacementsAreAddable(commentPath, fixSuggestionInfo.replacements);
- }
- }
-
- private static void ensureDescriptionIsSet(String commentPath, String description)
- throws BadRequestException {
- if (description == null) {
- throw new BadRequestException(
- String.format(
- "A description is required for the suggested fix of the robot comment on %s",
- commentPath));
- }
- }
-
- private static void ensureFixReplacementsAreAddable(
- String commentPath, List<FixReplacementInfo> fixReplacementInfos) throws BadRequestException {
- ensureReplacementsArePresent(commentPath, fixReplacementInfos);
-
- for (FixReplacementInfo fixReplacementInfo : fixReplacementInfos) {
- ensureReplacementPathIsSetAndNotPatchsetLevel(commentPath, fixReplacementInfo.path);
- ensureRangeIsSet(commentPath, fixReplacementInfo.range);
- ensureRangeIsValid(commentPath, fixReplacementInfo.range);
- ensureReplacementStringIsSet(commentPath, fixReplacementInfo.replacement);
- }
-
- Map<String, List<FixReplacementInfo>> replacementsPerFilePath =
- fixReplacementInfos.stream().collect(groupingBy(fixReplacement -> fixReplacement.path));
- for (List<FixReplacementInfo> sameFileReplacements : replacementsPerFilePath.values()) {
- ensureRangesDoNotOverlap(commentPath, sameFileReplacements);
- }
- }
-
- private static void ensureReplacementsArePresent(
- String commentPath, List<FixReplacementInfo> fixReplacementInfos) throws BadRequestException {
- if (fixReplacementInfos == null || fixReplacementInfos.isEmpty()) {
- throw new BadRequestException(
- String.format(
- "At least one replacement is "
- + "required for the suggested fix of the robot comment on %s",
- commentPath));
- }
- }
-
- private static void ensureReplacementPathIsSetAndNotPatchsetLevel(
- String commentPath, String replacementPath) throws BadRequestException {
- if (replacementPath == null) {
- throw new BadRequestException(
- String.format(
- "A file path must be given for the replacement of the robot comment on %s",
- commentPath));
- }
- if (replacementPath.equals(PATCHSET_LEVEL)) {
- throw new BadRequestException(
- String.format(
- "A file path must not be %s for the replacement of the robot comment on %s",
- PATCHSET_LEVEL, commentPath));
- }
- }
-
- private static void ensureRangeIsSet(String commentPath, Range range) throws BadRequestException {
- if (range == null) {
- throw new BadRequestException(
- String.format(
- "A range must be given for the replacement of the robot comment on %s", commentPath));
- }
- }
-
- private static void ensureRangeIsValid(String commentPath, Range range)
- throws BadRequestException {
- if (range == null) {
- return;
- }
- if (!range.isValid()) {
- throw new BadRequestException(
- String.format(
- "Range (%s:%s - %s:%s) is not valid for the comment on %s",
- range.startLine,
- range.startCharacter,
- range.endLine,
- range.endCharacter,
- commentPath));
- }
- }
-
- private static void ensureReplacementStringIsSet(String commentPath, String replacement)
- throws BadRequestException {
- if (replacement == null) {
- throw new BadRequestException(
- String.format(
- "A content for replacement "
- + "must be indicated for the replacement of the robot comment on %s",
- commentPath));
- }
- }
-
- private static void ensureRangesDoNotOverlap(
- String commentPath, List<FixReplacementInfo> fixReplacementInfos) throws BadRequestException {
- List<Range> sortedRanges =
- fixReplacementInfos.stream()
- .map(fixReplacementInfo -> fixReplacementInfo.range)
- .sorted()
- .collect(toList());
-
- int previousEndLine = 0;
- int previousOffset = -1;
- for (Range range : sortedRanges) {
- if (range.startLine < previousEndLine
- || (range.startLine == previousEndLine && range.startCharacter < previousOffset)) {
- throw new BadRequestException(
- String.format("Replacements overlap for the robot comment on %s", commentPath));
- }
- previousEndLine = range.endLine;
- previousOffset = range.endCharacter;
+ String.format("robotRunId is missing for comment on %s", commentPath));
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewOp.java b/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
index a47e179574..490ff490e6 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
@@ -15,12 +15,12 @@
package com.google.gerrit.server.restapi.change;
import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.joining;
-import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import com.google.auto.value.AutoValue;
@@ -35,8 +35,6 @@ import com.google.common.collect.Streams;
import com.google.common.collect.Table.Cell;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Comment;
-import com.google.gerrit.entities.FixReplacement;
-import com.google.gerrit.entities.FixSuggestion;
import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelTypes;
@@ -47,8 +45,6 @@ import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
-import com.google.gerrit.extensions.common.FixReplacementInfo;
-import com.google.gerrit.extensions.common.FixSuggestionInfo;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.extensions.restapi.Url;
@@ -57,7 +53,6 @@ import com.google.gerrit.extensions.validators.CommentValidationContext;
import com.google.gerrit.extensions.validators.CommentValidationFailure;
import com.google.gerrit.extensions.validators.CommentValidator;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.IdentifiedUser;
@@ -177,6 +172,54 @@ public class PostReviewOp implements BatchUpdateOp {
}
}
+ @AutoValue
+ public abstract static class Result {
+ /**
+ * Whether this {@code PostReviewOp} updated any vote on the current patch set.
+ *
+ * @return returns {@code true} if a) ReviewInput contained votes and b) ReviewInput was applied
+ * on the current patch set or any votes got copied to the current patch set.
+ */
+ abstract boolean updatedAnyVoteOnCurrentPatchSet();
+
+ /**
+ * Whether this {@code PostReviewOp} applied any negative vote on the current patch set.
+ *
+ * @return returns {@code true} if a) ReviewInput contained negative votes and b) ReviewInput
+ * was applied on the current patch set or any of the negative votes got copied to the
+ * current patch set.
+ */
+ abstract boolean updatedAnyNegativeVoteOnCurrentPatchSet();
+
+ /**
+ * Whether this {@code PostReviewOp} applied votes on an outdated patch set that were not copied
+ * to the current patch set.
+ *
+ * @return returns {@code true} if a) ReviewInput contained votes, b) ReviewInput was applied on
+ * an outdated patch set and c) not all of the votes got copied to the current patch set
+ */
+ abstract boolean appliedVotesOnOutdatedPatchSetThatWereNotCopiedToCurrentPatchSet();
+
+ /**
+ * Whether this {@code PostReviewOp} posted a change message.
+ *
+ * @return returns {@code true} if ReviewInput contained a message.
+ */
+ abstract boolean postedChangeMessage();
+
+ static Result create(
+ boolean updatedAnyVoteOnCurrentPatchSet,
+ boolean updatedAnyNegativeVoteOnCurrentPatchSet,
+ boolean appliedVotesOnOutdatedPatchSetThatWereNotCopiedToCurrentPatchSet,
+ boolean postedChangeMessage) {
+ return new AutoValue_PostReviewOp_Result(
+ updatedAnyVoteOnCurrentPatchSet,
+ updatedAnyNegativeVoteOnCurrentPatchSet,
+ appliedVotesOnOutdatedPatchSetThatWereNotCopiedToCurrentPatchSet,
+ postedChangeMessage);
+ }
+ }
+
@VisibleForTesting
public static final String START_REVIEW_MESSAGE = "This change is ready for review.";
@@ -209,6 +252,8 @@ public class PostReviewOp implements BatchUpdateOp {
private Map<String, Short> approvals = new HashMap<>();
private Map<String, Short> oldApprovals = new HashMap<>();
+ private Result result;
+
@Inject
PostReviewOp(
@GerritServerConfig Config gerritConfig,
@@ -272,6 +317,14 @@ public class PostReviewOp implements BatchUpdateOp {
try (TraceContext.TraceTimer ignored = newTimer("insertMessage")) {
dirty |= insertMessage(ctx);
}
+
+ result =
+ Result.create(
+ updatedAnyVoteOnCurrentPatchSet(),
+ updatedAnyNegativeVoteOnCurrentPatchSet(),
+ appliedVotesOnOutdatedPatchSetThatWereNotCopiedToCurrentPatchSet(),
+ postedChangeMessage());
+
return dirty;
}
@@ -404,7 +457,8 @@ public class PostReviewOp implements BatchUpdateOp {
inputComment.side(),
inputComment.message,
inputComment.unresolved,
- parent);
+ parent,
+ CommentsUtil.createFixSuggestionsFromInput(inputComment.fixSuggestions));
} else {
// In ChangeUpdate#putDraftComment() the draft with the same ID will be deleted.
comment.writtenOn = Timestamp.from(ctx.getWhen());
@@ -510,39 +564,11 @@ public class PostReviewOp implements BatchUpdateOp {
robotComment.setLineNbrAndRange(robotCommentInput.line, robotCommentInput.range);
robotComment.tag = in.tag;
commentsUtil.setCommentCommitId(robotComment, ctx.getChange(), ps);
- robotComment.fixSuggestions = createFixSuggestionsFromInput(robotCommentInput.fixSuggestions);
+ robotComment.fixSuggestions =
+ CommentsUtil.createFixSuggestionsFromInput(robotCommentInput.fixSuggestions);
return robotComment;
}
- private ImmutableList<FixSuggestion> createFixSuggestionsFromInput(
- List<FixSuggestionInfo> fixSuggestionInfos) {
- if (fixSuggestionInfos == null) {
- return ImmutableList.of();
- }
-
- ImmutableList.Builder<FixSuggestion> fixSuggestions =
- ImmutableList.builderWithExpectedSize(fixSuggestionInfos.size());
- for (FixSuggestionInfo fixSuggestionInfo : fixSuggestionInfos) {
- fixSuggestions.add(createFixSuggestionFromInput(fixSuggestionInfo));
- }
- return fixSuggestions.build();
- }
-
- private FixSuggestion createFixSuggestionFromInput(FixSuggestionInfo fixSuggestionInfo) {
- List<FixReplacement> fixReplacements = toFixReplacements(fixSuggestionInfo.replacements);
- String fixId = ChangeUtil.messageUuid();
- return new FixSuggestion(fixId, fixSuggestionInfo.description, fixReplacements);
- }
-
- private List<FixReplacement> toFixReplacements(List<FixReplacementInfo> fixReplacementInfos) {
- return fixReplacementInfos.stream().map(this::toFixReplacement).collect(toList());
- }
-
- private FixReplacement toFixReplacement(FixReplacementInfo fixReplacementInfo) {
- Comment.Range range = new Comment.Range(fixReplacementInfo.range);
- return new FixReplacement(fixReplacementInfo.path, range, fixReplacementInfo.replacement);
- }
-
private Set<CommentSetEntry> readExistingComments(ChangeContext ctx) {
return commentsUtil.publishedHumanCommentsByChange(ctx.getNotes()).stream()
.map(CommentSetEntry::create)
@@ -687,8 +713,21 @@ public class PostReviewOp implements BatchUpdateOp {
addLabelDelta(normName, c.value());
oldApprovals.put(normName, previous.get(normName));
approvals.put(normName, c.value());
- update.putReviewer(user.getAccountId(), REVIEWER);
update.putApproval(normName, ent.getValue());
+
+ // Votes may be applied on outdated patch sets, using a ChangeUpdate that was created for
+ // the outdated patch set. Reviewers however cannot be added on outdated patch sets, but
+ // only on the change. This means reviewers should always be added using a ChangeUpdate
+ // that was created for the current patch set.
+ // This is important so that updates on the current patch set that are done by other ops
+ // within the same BatchUpdate after this PostReviewOp was executed can see the reviewer
+ // updates. E.g. the AddToAttentionSetOp, that updates the attention set on the current
+ // patch set, needs to see newly added reviewers, as otherwise attention set updates for
+ // these reviewers are dropped (ChangeUpdate#updateAttentionSet drops attention set updates
+ // for users that are not active on the change, i.e. for users that are neither change
+ // owner, uploader nor reviewer).
+ ctx.getUpdate(notes.getChange().currentPatchSetId())
+ .putReviewer(user.getAccountId(), REVIEWER);
}
}
@@ -1147,6 +1186,98 @@ public class PostReviewOp implements BatchUpdateOp {
labelDelta.add(LabelVote.create(name, value));
}
+ /**
+ * Gets the result of running this {@code PostReviewOp}.
+ *
+ * <p>Must only be invoked after this {@code PostReviewOp} has been executed with {@link
+ * com.google.gerrit.server.update.BatchUpdate}.
+ *
+ * @throws IllegalStateException thrown if invoked before this {@code PostReviewOp} has been
+ * executed
+ */
+ public Result getResult() {
+ checkState(result != null, "cannot retrieve result, change update has not been executed yet");
+ return result;
+ }
+
+ /**
+ * Whether this {@code PostReviewOp} updated any vote on the current patch set.
+ *
+ * <p>Must only be invoked after this {@code PostReviewOp} has been executed with {@link
+ * com.google.gerrit.server.update.BatchUpdate}.
+ *
+ * @return returns {@code true} if a) ReviewInput contained votes and b) ReviewInput was applied
+ * on the current patch set or any votes got copied to the current patch set.
+ */
+ private boolean updatedAnyVoteOnCurrentPatchSet() {
+ return in.labels != null
+ && !in.labels.isEmpty()
+ && (notes.getCurrentPatchSet().id().equals(psId)
+ || labelUpdatesOnFollowUpPatchSets.values().stream()
+ .anyMatch(
+ copiedLabelUpdate ->
+ copiedLabelUpdate.patchSetId().equals(notes.getCurrentPatchSet().id())));
+ }
+
+ /**
+ * Whether this {@code PostReviewOp} applied any negative vote on the current patch set.
+ *
+ * <p>Must only be invoked after this {@code PostReviewOp} has been executed with {@link
+ * com.google.gerrit.server.update.BatchUpdate}.
+ *
+ * @return returns {@code true} if a) ReviewInput contained negative votes and b) ReviewInput was
+ * applied on the current patch set or any of the negative votes got copied to the current
+ * patch set.
+ */
+ private boolean updatedAnyNegativeVoteOnCurrentPatchSet() {
+ return in.labels != null
+ && in.labels.values().stream().anyMatch(vote -> vote < 0)
+ && (notes.getCurrentPatchSet().id().equals(psId)
+ || labelUpdatesOnFollowUpPatchSets.entries().stream()
+ .filter(e -> e.getKey().value() < 0)
+ .anyMatch(e -> e.getValue().patchSetId().equals(notes.getCurrentPatchSet().id())));
+ }
+
+ /**
+ * Whether this {@code PostReviewOp} applied votes on an outdated patch set that were not copied
+ * to the current patch set.
+ *
+ * <p>Must only be invoked after this {@code PostReviewOp} has been executed with {@link
+ * com.google.gerrit.server.update.BatchUpdate}.
+ *
+ * @return returns {@code true} if a) ReviewInput contained votes, b) ReviewInput was applied on
+ * an outdated patch set and c) not all of the votes got copied to the current patch set
+ */
+ private boolean appliedVotesOnOutdatedPatchSetThatWereNotCopiedToCurrentPatchSet() {
+ if (in.labels == null || notes.getCurrentPatchSet().id().equals(psId)) {
+ return false;
+ }
+
+ for (Map.Entry<String, Short> labelEntry : in.labels.entrySet()) {
+ if (labelUpdatesOnFollowUpPatchSets
+ .get(LabelVote.create(labelEntry.getKey(), labelEntry.getValue())).stream()
+ .anyMatch(
+ copiedLabelUpdate ->
+ copiedLabelUpdate.patchSetId().equals(notes.getCurrentPatchSet().id()))) {
+ continue;
+ }
+
+ // vote was not copied to current patch set
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Whether this {@code PostReviewOp} posted a change message.
+ *
+ * @return returns {@code true} if ReviewInput contained a message.
+ */
+ private boolean postedChangeMessage() {
+ return !Strings.isNullOrEmpty(in.message);
+ }
+
private TraceContext.TraceTimer newTimer(String method) {
return TraceContext.newTimer(getClass().getSimpleName() + "#" + method, Metadata.empty());
}
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewers.java b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
index b0e58c5403..675610d449 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
@@ -65,10 +65,6 @@ public class PostReviewers
public Response<ReviewerResult> apply(ChangeResource rsrc, ReviewerInput input)
throws IOException, RestApiException, UpdateException, PermissionBackendException,
ConfigInvalidException {
- if (input.reviewer == null) {
- throw new BadRequestException("missing reviewer field");
- }
-
ReviewerModification modification =
reviewerModifier.prepare(rsrc.getNotes(), rsrc.getUser(), input, true);
if (modification.op == null) {
diff --git a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
index 345d915d18..912239b758 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
@@ -181,6 +181,9 @@ public class PutDraftComment implements RestModifyView<DraftCommentResource, Dra
if (in.unresolved != null) {
e.unresolved = in.unresolved;
}
+ if (in.fixSuggestions != null) {
+ e.fixSuggestions = CommentsUtil.createFixSuggestionsFromInput(in.fixSuggestions);
+ }
return e;
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/PutMessage.java b/java/com/google/gerrit/server/restapi/change/PutMessage.java
index 3717e02f1d..91f15759cf 100644
--- a/java/com/google/gerrit/server/restapi/change/PutMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/PutMessage.java
@@ -142,7 +142,8 @@ public class PutMessage implements RestModifyView<ChangeResource, CommitMessageI
PatchSet.Id psId = ChangeUtil.nextPatchSetId(repository, ps.id());
ObjectId newCommit =
- createCommit(objectInserter, patchSetCommit, sanitizedCommitMessage, ts);
+ createCommit(
+ objectInserter, patchSetCommit, sanitizedCommitMessage, ts, input.committerEmail);
PatchSetInserter inserter =
psInserterFactory.create(resource.getNotes(), psId, newCommit);
inserter.setMessage(
@@ -171,20 +172,35 @@ public class PutMessage implements RestModifyView<ChangeResource, CommitMessageI
ObjectInserter objectInserter,
RevCommit basePatchSetCommit,
String commitMessage,
- Instant timestamp)
- throws IOException {
+ Instant timestamp,
+ String committerEmail)
+ throws IOException, BadRequestException {
CommitBuilder builder = new CommitBuilder();
builder.setTreeId(basePatchSetCommit.getTree());
builder.setParentIds(basePatchSetCommit.getParents());
builder.setAuthor(basePatchSetCommit.getAuthorIdent());
IdentifiedUser user = userProvider.get().asIdentifiedUser();
- PersonIdent committer =
- Optional.ofNullable(basePatchSetCommit.getCommitterIdent())
- .map(
- ident ->
- user.newCommitterIdent(ident.getEmailAddress(), timestamp, zoneId)
- .orElseGet(() -> user.newCommitterIdent(timestamp, zoneId)))
- .orElseGet(() -> user.newCommitterIdent(timestamp, zoneId));
+ PersonIdent committer;
+ if (committerEmail == null) {
+ committer =
+ Optional.ofNullable(basePatchSetCommit.getCommitterIdent())
+ .map(
+ ident ->
+ user.newCommitterIdent(ident.getEmailAddress(), timestamp, zoneId)
+ .orElseGet(() -> user.newCommitterIdent(timestamp, zoneId)))
+ .orElseGet(() -> user.newCommitterIdent(timestamp, zoneId));
+ } else {
+ committer =
+ user.newCommitterIdent(committerEmail, timestamp, zoneId)
+ .orElseThrow(
+ () ->
+ new BadRequestException(
+ String.format(
+ "Cannot set commit message using committer email %s, "
+ + "as it is not among the registered emails of account %s",
+ committerEmail, user.getAccountId().get())));
+ }
+
builder.setCommitter(committer);
builder.setMessage(commitMessage);
ObjectId newCommitId = objectInserter.insert(builder);
diff --git a/java/com/google/gerrit/server/restapi/change/QueryChanges.java b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
index 4d279b0f8d..812711add1 100644
--- a/java/com/google/gerrit/server/restapi/change/QueryChanges.java
+++ b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
@@ -172,9 +172,7 @@ public class QueryChanges implements RestReadView<TopLevelResource>, DynamicOpti
throw new QueryParseException("query disabled");
}
- if (limit != null) {
- queryProcessor.setUserProvidedLimit(limit);
- }
+ queryProcessor.setUserProvidedLimit(limit != null ? limit : 0, /* applyDefaultLimit */ true);
if (start != null) {
if (start < 0) {
throw new BadRequestException("'start' parameter cannot be less than zero");
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index 98a3f8396b..9d574a4c83 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -172,7 +172,7 @@ public class Rebase
boolean enabled = false;
try (Repository repo = repoManager.openRepository(change.getDest().project());
RevWalk rw = new RevWalk(repo)) {
- if (RebaseUtil.hasOneParent(rw, rsrc.getPatchSet())) {
+ if (RebaseUtil.hasAtLeastOneParent(rw, rsrc.getPatchSet())) {
enabled = rebaseUtil.canRebase(rsrc.getPatchSet(), change.getDest(), repo, rw);
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/RebaseChain.java b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
index 76c5253588..68d3c6314b 100644
--- a/java/com/google/gerrit/server/restapi/change/RebaseChain.java
+++ b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
@@ -311,7 +311,7 @@ public class RebaseChain
} else {
for (RevisionResource psRsrc : chainAsRevisionResources) {
if (patchSetUtil.isPatchSetLocked(psRsrc.getNotes())
- || !RebaseUtil.hasOneParent(rw, psRsrc.getPatchSet())) {
+ || !RebaseUtil.hasAtLeastOneParent(rw, psRsrc.getPatchSet())) {
enabled = false;
break;
}
diff --git a/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java b/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
index 7f4b10f343..bc47adc292 100644
--- a/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
+++ b/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
@@ -15,11 +15,13 @@
package com.google.gerrit.server.restapi.change;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.HumanComment;
@@ -37,6 +39,7 @@ import com.google.gerrit.server.account.ServiceUserClassifier;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.AddToAttentionSetOp;
import com.google.gerrit.server.change.AttentionSetUnchangedOp;
+import com.google.gerrit.server.change.AttentionSetUpdateCondition;
import com.google.gerrit.server.change.CommentThread;
import com.google.gerrit.server.change.CommentThreads;
import com.google.gerrit.server.change.RemoveFromAttentionSetOp;
@@ -106,6 +109,7 @@ public class ReplyAttentionSetUpdates {
}
processRules(
bu,
+ /* postReviewOp= */ null,
changeNotes,
readyForReview,
currentUser,
@@ -113,14 +117,24 @@ public class ReplyAttentionSetUpdates {
}
/**
- * Adjusts the attention set by adding and removing users. If the same user should be added and
- * removed or added/removed twice, the user will only be added/removed once, based on first
- * addition/removal.
+ * Adjusts the attention set when a review is posted.
+ *
+ * <p>If the same user should be added and removed or added/removed twice, the user will only be
+ * added/removed once, based on first addition/removal.
+ *
+ * @param postReviewOp the {@link PostReviewOp} that is being executed before the attention set
+ * updates
*/
- public void updateAttentionSet(
- BatchUpdate bu, ChangeNotes changeNotes, ReviewInput input, CurrentUser currentUser)
+ public void updateAttentionSetOnPostReview(
+ BatchUpdate bu,
+ PostReviewOp postReviewOp,
+ ChangeNotes changeNotes,
+ ReviewInput input,
+ CurrentUser currentUser)
throws BadRequestException, IOException, PermissionBackendException,
UnprocessableEntityException, ConfigInvalidException {
+ requireNonNull(postReviewOp, "postReviewOp must not be null");
+
processManualUpdates(bu, changeNotes, input);
if (input.ignoreAutomaticAttentionSetRules) {
@@ -133,12 +147,13 @@ public class ReplyAttentionSetUpdates {
boolean isReadyForReview = isReadyForReview(changeNotes, input);
if (isReadyForReview && serviceUserClassifier.isServiceUser(currentUser.getAccountId())) {
- botsWithNegativeLabelsAddOwnerAndUploader(bu, changeNotes, input);
+ botsWithNegativeLabelsAddOwnerAndUploader(bu, postReviewOp, changeNotes);
return;
}
processRules(
bu,
+ postReviewOp,
changeNotes,
isReadyForReview,
currentUser,
@@ -163,7 +178,8 @@ public class ReplyAttentionSetUpdates {
commentInput.side(),
commentInput.message,
commentInput.unresolved,
- commentInput.inReplyTo));
+ commentInput.inReplyTo,
+ CommentsUtil.createFixSuggestionsFromInput(commentInput.fixSuggestions)));
}
}
List<HumanComment> drafts = new ArrayList<>();
@@ -181,27 +197,33 @@ public class ReplyAttentionSetUpdates {
}
/**
- * Process the automatic rules of the attention set. All of the automatic rules except
- * adding/removing reviewers and entering/exiting WIP state are done here, and the rest are done
- * in {@link ChangeUpdate}
+ * Process the automatic rules of the attention set.
+ *
+ * <p>All of the automatic rules except adding/removing reviewers and entering/exiting WIP state
+ * are done here, and the rest are done in {@link ChangeUpdate}.
+ *
+ * @param postReviewOp {@link PostReviewOp} that is being executed before the attention set
+ * updates, may be {@code null}
*/
private void processRules(
BatchUpdate bu,
+ @Nullable PostReviewOp postReviewOp,
ChangeNotes changeNotes,
boolean readyForReview,
CurrentUser currentUser,
ImmutableSet<HumanComment> allNewComments) {
- // Replying removes the publishing user from the attention set.
- removeFromAttentionSet(bu, changeNotes, currentUser.getAccountId(), "removed on reply", false);
-
- Account.Id uploader = changeNotes.getCurrentPatchSet().uploader();
- Account.Id owner = changeNotes.getChange().getOwner();
+ updateAttentionSetForCurrentUser(bu, postReviewOp, changeNotes, currentUser);
// The rest of the conditions only apply if the change is open.
if (changeNotes.getChange().getStatus().isClosed()) {
// We still add the owner if a new comment thread was created, on closed changes.
if (allNewComments.stream().anyMatch(c -> c.parentUuid == null)) {
- addToAttentionSet(bu, changeNotes, owner, "A new comment thread was created", false);
+ addToAttentionSet(
+ bu,
+ changeNotes,
+ changeNotes.getChange().getOwner(),
+ "A new comment thread was created",
+ false);
}
return;
}
@@ -211,14 +233,98 @@ public class ReplyAttentionSetUpdates {
return;
}
+ addOwnerAndUploaderToAttentionSetIfSomeoneElseReplied(
+ bu, postReviewOp, changeNotes, currentUser, readyForReview, allNewComments);
+ addAllAuthorsOfCommentThreads(bu, changeNotes, allNewComments);
+ }
+
+ /**
+ * Updates the attention set for the current user.
+ *
+ * <p>Removes the current user from the attention set (since they replied) unless they voted on an
+ * outdated patch set and some of the votes were not copied to the current patch set (in this case
+ * they should be in the attention set to re-apply their votes).
+ *
+ * <p>If the current user voted on an outdated patch set and some of the votes were not copied to
+ * the current patch set:
+ *
+ * <ul>
+ * <li>the current user is added to the attention set (if they are not in the attention set yet)
+ * or
+ * <li>the reason for the current user to be in the attention set is updated (if they are
+ * already in the attention set).
+ * </ul>
+ */
+ private void updateAttentionSetForCurrentUser(
+ BatchUpdate bu,
+ @Nullable PostReviewOp postReviewOp,
+ ChangeNotes changeNotes,
+ CurrentUser currentUser) {
+ if (postReviewOp == null) {
+ // Replying removes the current user from the attention set.
+ removeFromAttentionSet(
+ bu, changeNotes, currentUser.getAccountId(), "removed on reply", false);
+ } else {
+ // If the current user voted on an outdated patch set and some of the votes were not copied to
+ // the current patch set the current user should stay in the attention set, or be added to the
+ // attention set. In case the user stays in the attention set, this updates the reason for
+ // being in the attention set.
+ AttentionSetUpdateCondition addOrKeepCondition =
+ () ->
+ postReviewOp
+ .getResult()
+ .appliedVotesOnOutdatedPatchSetThatWereNotCopiedToCurrentPatchSet();
+ maybeAddToAttentionSet(
+ bu,
+ addOrKeepCondition,
+ changeNotes,
+ currentUser.getAccountId(),
+ "Some votes were not copied to the current patch set",
+ false);
+
+ // Otherwise replying removes the current user from the attention set.
+ AttentionSetUpdateCondition removeCondition = () -> !addOrKeepCondition.check();
+ maybeRemoveFromAttentionSet(
+ bu, removeCondition, changeNotes, currentUser.getAccountId(), "removed on reply", false);
+ }
+ }
+
+ /**
+ * Adds the owner and uploader to the attention set if someone else replied.
+ *
+ * <p>Replying means they either updated the votes on the current patch set (either directly on
+ * the current patch set or the votes were copied to the current patch set), they posted a change
+ * message, they marked the change as ready or they posted new comments.
+ */
+ private void addOwnerAndUploaderToAttentionSetIfSomeoneElseReplied(
+ BatchUpdate bu,
+ @Nullable PostReviewOp postReviewOp,
+ ChangeNotes changeNotes,
+ CurrentUser currentUser,
+ boolean readyForReview,
+ ImmutableSet<HumanComment> allNewComments) {
+ AttentionSetUpdateCondition condition =
+ postReviewOp != null
+ ? () ->
+ postReviewOp.getResult().updatedAnyVoteOnCurrentPatchSet()
+ || postReviewOp.getResult().postedChangeMessage()
+ || (changeNotes.getChange().isWorkInProgress() && readyForReview)
+ || !allNewComments.isEmpty()
+ : () ->
+ (changeNotes.getChange().isWorkInProgress() && readyForReview)
+ || !allNewComments.isEmpty();
+
+ Account.Id owner = changeNotes.getChange().getOwner();
if (!currentUser.getAccountId().equals(owner)) {
- addToAttentionSet(bu, changeNotes, owner, "Someone else replied on the change", false);
+ maybeAddToAttentionSet(
+ bu, condition, changeNotes, owner, "Someone else replied on the change", false);
}
+
+ Account.Id uploader = changeNotes.getCurrentPatchSet().uploader();
if (!owner.equals(uploader) && !currentUser.getAccountId().equals(uploader)) {
- addToAttentionSet(bu, changeNotes, uploader, "Someone else replied on the change", false);
+ maybeAddToAttentionSet(
+ bu, condition, changeNotes, uploader, "Someone else replied on the change", false);
}
-
- addAllAuthorsOfCommentThreads(bu, changeNotes, allNewComments);
}
/** Adds all authors of all comment threads that received a reply during this update */
@@ -266,20 +372,26 @@ public class ReplyAttentionSetUpdates {
/**
* Bots don't process automatic rules, the only attention set change they do is this rule: Add
- * owner and uploader when a bot votes negatively, but only if the change is open.
+ * owner and uploader when a bot votes negatively on the current patch set, but only if the change
+ * is open.
*/
private void botsWithNegativeLabelsAddOwnerAndUploader(
- BatchUpdate bu, ChangeNotes changeNotes, ReviewInput input) {
+ BatchUpdate bu, PostReviewOp postReviewOp, ChangeNotes changeNotes) {
if (changeNotes.getChange().isClosed()) {
return;
}
- if (input.labels != null && input.labels.values().stream().anyMatch(vote -> vote < 0)) {
- Account.Id uploader = changeNotes.getCurrentPatchSet().uploader();
- Account.Id owner = changeNotes.getChange().getOwner();
- addToAttentionSet(bu, changeNotes, owner, "A robot voted negatively on a label", false);
- if (!owner.equals(uploader)) {
- addToAttentionSet(bu, changeNotes, uploader, "A robot voted negatively on a label", false);
- }
+
+ AttentionSetUpdateCondition condition =
+ () -> postReviewOp.getResult().updatedAnyNegativeVoteOnCurrentPatchSet();
+
+ Account.Id owner = changeNotes.getChange().getOwner();
+ maybeAddToAttentionSet(
+ bu, condition, changeNotes, owner, "A robot voted negatively on a label", false);
+
+ Account.Id uploader = changeNotes.getCurrentPatchSet().uploader();
+ if (!owner.equals(uploader)) {
+ maybeAddToAttentionSet(
+ bu, condition, changeNotes, uploader, "A robot voted negatively on a label", false);
}
}
@@ -299,6 +411,28 @@ public class ReplyAttentionSetUpdates {
}
/**
+ * Adds the user to the attention set if the given condition is true.
+ *
+ * @param bu BatchUpdate to perform the updates to the attention set
+ * @param condition condition that decides whether the attention set update should be performed
+ * @param changeNotes current change
+ * @param user user to add to the attention set
+ * @param reason reason for adding
+ * @param notify whether or not to notify about this addition
+ */
+ private void maybeAddToAttentionSet(
+ BatchUpdate bu,
+ AttentionSetUpdateCondition condition,
+ ChangeNotes changeNotes,
+ Account.Id user,
+ String reason,
+ boolean notify) {
+ AddToAttentionSetOp addToAttentionSet =
+ addToAttentionSetOpFactory.create(user, reason, notify).setCondition(condition);
+ bu.addOp(changeNotes.getChangeId(), addToAttentionSet);
+ }
+
+ /**
* Removes the user from the attention set
*
* @param bu BatchUpdate to perform the updates to the attention set.
@@ -314,6 +448,28 @@ public class ReplyAttentionSetUpdates {
bu.addOp(changeNotes.getChangeId(), removeFromAttentionSetOp);
}
+ /**
+ * Removes the user from the attention set if the given condition is true.
+ *
+ * @param bu BatchUpdate to perform the updates to the attention set.
+ * @param condition condition that decides whether the attention set update should be performed
+ * @param changeNotes current change.
+ * @param user user to add remove from the attention set.
+ * @param reason reason for removing.
+ * @param notify whether or not to notify about this removal.
+ */
+ private void maybeRemoveFromAttentionSet(
+ BatchUpdate bu,
+ AttentionSetUpdateCondition condition,
+ ChangeNotes changeNotes,
+ Account.Id user,
+ String reason,
+ boolean notify) {
+ RemoveFromAttentionSetOp removeFromAttentionSetOp =
+ removeFromAttentionSetOpFactory.create(user, reason, notify).setCondition(condition);
+ bu.addOp(changeNotes.getChangeId(), removeFromAttentionSetOp);
+ }
+
private static boolean isReadyForReview(ChangeNotes changeNotes, ReviewInput input) {
return (!changeNotes.getChange().isWorkInProgress() && !input.workInProgress) || input.ready;
}
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index 5bf0e8b98b..f04042c55d 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -25,7 +25,7 @@ import static java.util.Objects.requireNonNull;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Iterables;
-import com.google.common.collect.Multimap;
+import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
@@ -81,7 +81,6 @@ import java.text.MessageFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
@@ -236,7 +235,7 @@ public class RevertSubmission
throws RestApiException, IOException, UpdateException, ConfigInvalidException,
StorageException, PermissionBackendException {
- Multimap<BranchNameKey, ChangeData> changesPerProjectAndBranch = ArrayListMultimap.create();
+ ListMultimap<BranchNameKey, ChangeData> changesPerProjectAndBranch = ArrayListMultimap.create();
changeData.stream().forEach(c -> changesPerProjectAndBranch.put(c.change().getDest(), c));
cherryPickInput = createCherryPickInput(revertInput);
Instant timestamp = TimeUtil.now();
@@ -246,8 +245,7 @@ public class RevertSubmission
cherryPickInput.base = null;
Project.NameKey project = projectAndBranch.project();
cherryPickInput.destination = projectAndBranch.branch();
- Collection<ChangeData> changesInProjectAndBranch =
- changesPerProjectAndBranch.get(projectAndBranch);
+ List<ChangeData> changesInProjectAndBranch = changesPerProjectAndBranch.get(projectAndBranch);
// Sort the changes topologically.
Iterator<PatchSetData> sortedChangesInProjectAndBranch =
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
index a6600d789a..819ae72032 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
@@ -203,7 +203,7 @@ public class ReviewerRecommender {
int numberOfRelevantChanges = config.getInt("suggest", "relevantChanges", 50);
// Get the user's last numberOfRelevantChanges changes, check reviewers
try {
- List<ChangeData> result =
+ ImmutableList<ChangeData> result =
queryProvider
.get()
.setLimit(numberOfRelevantChanges)
diff --git a/java/com/google/gerrit/server/restapi/change/Reviewers.java b/java/com/google/gerrit/server/restapi/change/Reviewers.java
index 9da7c88b9d..864a559e00 100644
--- a/java/com/google/gerrit/server/restapi/change/Reviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/Reviewers.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.restapi.change;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Address;
import com.google.gerrit.extensions.registration.DynamicMap;
@@ -29,7 +30,6 @@ import com.google.gerrit.server.change.ReviewerResource;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.Collection;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
@@ -86,7 +86,7 @@ public class Reviewers implements ChildCollection<ChangeResource, ReviewerResour
throw new ResourceNotFoundException(id);
}
- private Collection<Account.Id> fetchAccountIds(ChangeResource rsrc) {
+ private ImmutableSet<Account.Id> fetchAccountIds(ChangeResource rsrc) {
return approvalsUtil.getReviewers(rsrc.getNotes()).all();
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index 8dd20c92b2..05648d5758 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -254,6 +254,8 @@ public class ReviewersUtil {
return Account.id(Integer.valueOf(f.<String>getValue(AccountField.ID_STR_FIELD_SPEC)));
}
+ // More accounts are suggested here than the requested limit because
+ // visibility filtering will be applied later.
private List<Account.Id> suggestAccounts(SuggestReviewers suggestReviewers)
throws BadRequestException {
try (Timer0.Context ctx = metrics.queryAccountsLatency.start()) {
@@ -278,7 +280,7 @@ public class ReviewersUtil {
QueryOptions.create(
indexConfig,
0,
- suggestReviewers.getLimit(),
+ suggestReviewers.getLimit() + 30,
ImmutableSet.of(idField.getName())))
.readRaw();
List<Account.Id> matches =
diff --git a/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java b/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
index 97383cdae9..de0014a39e 100644
--- a/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.restapi.change;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Address;
import com.google.gerrit.extensions.registration.DynamicMap;
@@ -31,7 +32,6 @@ import com.google.gerrit.server.restapi.account.AccountsCollection;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.Collection;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
@@ -83,7 +83,7 @@ public class RevisionReviewers implements ChildCollection<RevisionResource, Revi
throw e;
}
}
- Collection<Account.Id> reviewers = approvalsUtil.getReviewers(rsrc.getNotes()).all();
+ ImmutableSet<Account.Id> reviewers = approvalsUtil.getReviewers(rsrc.getNotes()).all();
// See if the id exists as a reviewer for this change
if (reviewers.contains(accountId)) {
return resourceFactory.create(rsrc, accountId);
diff --git a/java/com/google/gerrit/server/restapi/change/RobotComments.java b/java/com/google/gerrit/server/restapi/change/RobotComments.java
index 9f81d0ab5a..e02a39f091 100644
--- a/java/com/google/gerrit/server/restapi/change/RobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/RobotComments.java
@@ -27,6 +27,7 @@ import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.inject.Inject;
import com.google.inject.Singleton;
+@Deprecated
@Singleton
public class RobotComments implements ChildCollection<RevisionResource, RobotCommentResource> {
private final DynamicMap<RestView<RobotCommentResource>> views;
diff --git a/java/com/google/gerrit/server/restapi/change/Submit.java b/java/com/google/gerrit/server/restapi/change/Submit.java
index be2fae3b96..36b859c9e6 100644
--- a/java/com/google/gerrit/server/restapi/change/Submit.java
+++ b/java/com/google/gerrit/server/restapi/change/Submit.java
@@ -50,6 +50,7 @@ import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.MergeabilityComputationBehavior;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -73,7 +74,7 @@ import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.Map;
+import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -120,6 +121,8 @@ public class Submit
private final ProjectCache projectCache;
private final ChangeJson.Factory json;
+ private final boolean useMergeabilityCheck;
+
@Inject
Submit(
GitRepositoryManager repoManager,
@@ -166,6 +169,7 @@ public class Submit
this.psUtil = psUtil;
this.projectCache = projectCache;
this.json = json;
+ this.useMergeabilityCheck = MergeabilityComputationBehavior.fromConfig(cfg).includeInApi();
}
@Override
@@ -278,6 +282,9 @@ public class Submit
}
}
+ if (!useMergeabilityCheck) {
+ return null;
+ }
Collection<ChangeData> unmergeable = getUnmergeableChanges(cs);
if (unmergeable == null) {
return CLICK_FAILURE_TOOLTIP;
@@ -347,10 +354,10 @@ public class Submit
// cd.setMergeable(null);
// That was done in unmergeableChanges which was called by problemsForSubmittingChangeset, so
// now it is safe to read from the cache, as it yields the same result.
- Boolean enabled = cd.isMergeable();
+ Boolean enabled = useMergeabilityCheck ? cd.isMergeable() : true;
if (treatWithTopic) {
- Map<String, String> params =
+ ImmutableMap<String, String> params =
ImmutableMap.of(
"topicSize", String.valueOf(topicSize),
"submitSize", String.valueOf(cs.size()));
@@ -360,7 +367,7 @@ public class Submit
.setVisible(true)
.setEnabled(Boolean.TRUE.equals(enabled));
}
- Map<String, String> params =
+ ImmutableMap<String, String> params =
ImmutableMap.of(
"patchSet", String.valueOf(resource.getPatchSet().number()),
"branch", change.getDest().shortName(),
@@ -384,7 +391,7 @@ public class Submit
}
ListMultimap<BranchNameKey, ChangeData> cbb = cs.changesByBranch();
for (BranchNameKey branch : cbb.keySet()) {
- Collection<ChangeData> targetBranch = cbb.get(branch);
+ List<ChangeData> targetBranch = cbb.get(branch);
HashMap<Change.Id, RevCommit> commits = mapToCommits(targetBranch, branch.project());
Set<ObjectId> allParents =
commits.values().stream()
diff --git a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
index 2ce82ab6f9..6e589484c3 100644
--- a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
+++ b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
@@ -188,9 +188,12 @@ public class SubmittedTogether implements RestReadView<ChangeResource> {
// ChangeData or check if any of the ChangeDatas was loaded from the database and allow
// lazyloading if so.
for (ChangeData cd : cds) {
- cd.submitRecords(ChangeJson.SUBMIT_RULE_OPTIONS_LENIENT);
- cd.submitRecords(ChangeJson.SUBMIT_RULE_OPTIONS_STRICT);
- cd.currentPatchSet();
+ @SuppressWarnings("unused")
+ var unused = cd.submitRecords(ChangeJson.SUBMIT_RULE_OPTIONS_LENIENT);
+ unused = cd.submitRecords(ChangeJson.SUBMIT_RULE_OPTIONS_STRICT);
+
+ @SuppressWarnings("unused")
+ var unused2 = cd.currentPatchSet();
}
return cds;
}
diff --git a/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java b/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
index 26a04159a7..4d0026f8d9 100644
--- a/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
@@ -16,6 +16,7 @@ package com.google.gerrit.server.restapi.change;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.AccountVisibility;
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
@@ -53,6 +54,7 @@ public class SuggestChangeReviewers extends SuggestReviewers
name = "--exclude-groups",
aliases = {"-e"},
usage = "exclude groups from query")
+ @CanIgnoreReturnValue
public SuggestChangeReviewers setExcludeGroups(boolean excludeGroups) {
this.excludeGroups = excludeGroups;
return this;
@@ -63,6 +65,7 @@ public class SuggestChangeReviewers extends SuggestReviewers
usage =
"The type of reviewers that should be suggested"
+ " (can be 'REVIEWER' or 'CC', default is 'REVIEWER')")
+ @CanIgnoreReturnValue
public SuggestChangeReviewers setReviewerState(ReviewerState reviewerState) {
this.reviewerState = reviewerState;
return this;
diff --git a/java/com/google/gerrit/server/restapi/change/package-info.java b/java/com/google/gerrit/server/restapi/change/package-info.java
new file mode 100644
index 0000000000..14992b8cc8
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.restapi.change;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/restapi/config/ConfigRestApiModule.java b/java/com/google/gerrit/server/restapi/config/ConfigRestApiModule.java
index 3de05e9fd9..a17305f4d9 100644
--- a/java/com/google/gerrit/server/restapi/config/ConfigRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/config/ConfigRestApiModule.java
@@ -15,6 +15,9 @@
package com.google.gerrit.server.restapi.config;
import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
+import static com.google.gerrit.server.config.ExperimentResource.EXPERIMENT_KIND;
+import static com.google.gerrit.server.config.IndexResource.INDEX_KIND;
+import static com.google.gerrit.server.config.IndexVersionResource.INDEX_VERSION_KIND;
import static com.google.gerrit.server.config.TaskResource.TASK_KIND;
import com.google.gerrit.extensions.registration.DynamicMap;
@@ -27,12 +30,19 @@ public class ConfigRestApiModule extends RestApiModule {
protected void configure() {
DynamicMap.mapOf(binder(), CapabilityResource.CAPABILITY_KIND);
DynamicMap.mapOf(binder(), CONFIG_KIND);
+ DynamicMap.mapOf(binder(), EXPERIMENT_KIND);
DynamicMap.mapOf(binder(), TASK_KIND);
DynamicMap.mapOf(binder(), TopMenuResource.TOP_MENU_KIND);
+ DynamicMap.mapOf(binder(), INDEX_KIND);
+ DynamicMap.mapOf(binder(), INDEX_VERSION_KIND);
child(CONFIG_KIND, "capabilities").to(CapabilitiesCollection.class);
post(CONFIG_KIND, "check.consistency").to(CheckConsistency.class);
put(CONFIG_KIND, "email.confirm").to(ConfirmEmail.class);
+
+ child(CONFIG_KIND, "experiments").to(ExperimentsCollection.class);
+ get(EXPERIMENT_KIND).to(GetExperiment.class);
+
post(CONFIG_KIND, "index.changes").to(IndexChanges.class);
get(CONFIG_KIND, "info").to(GetServerInfo.class);
get(CONFIG_KIND, "preferences").to(GetPreferences.class);
@@ -42,6 +52,7 @@ public class ConfigRestApiModule extends RestApiModule {
get(CONFIG_KIND, "preferences.edit").to(GetEditPreferences.class);
put(CONFIG_KIND, "preferences.edit").to(SetEditPreferences.class);
post(CONFIG_KIND, "reload").to(ReloadConfig.class);
+ post(CONFIG_KIND, "snapshot.indexes").to(SnapshotIndexes.class);
child(CONFIG_KIND, "tasks").to(TasksCollection.class);
delete(TASK_KIND).to(DeleteTask.class);
@@ -50,6 +61,15 @@ public class ConfigRestApiModule extends RestApiModule {
child(CONFIG_KIND, "top-menus").to(TopMenuCollection.class);
get(CONFIG_KIND, "version").to(GetVersion.class);
+ child(CONFIG_KIND, "indexes").to(IndexCollection.class);
+ post(INDEX_KIND, "snapshot").to(SnapshotIndex.class);
+ get(INDEX_KIND).to(GetIndex.class);
+
+ child(INDEX_KIND, "versions").to(IndexVersionsCollection.class);
+ get(INDEX_VERSION_KIND).to(GetIndexVersion.class);
+ post(INDEX_VERSION_KIND, "snapshot").to(SnapshotIndexVersion.class);
+ post(INDEX_VERSION_KIND, "reindex").to(ReindexIndexVersion.class);
+
// The caches and summary REST endpoints are bound via RestCacheAdminModule.
}
}
diff --git a/java/com/google/gerrit/server/restapi/config/ExperimentsCollection.java b/java/com/google/gerrit/server/restapi/config/ExperimentsCollection.java
new file mode 100644
index 0000000000..0fb141c68d
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/ExperimentsCollection.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.config;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.config.ExperimentResource;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.inject.Inject;
+
+public class ExperimentsCollection implements ChildCollection<ConfigResource, ExperimentResource> {
+ private final PermissionBackend permissionBackend;
+ private final DynamicMap<RestView<ExperimentResource>> views;
+ private final ListExperiments list;
+
+ @Inject
+ ExperimentsCollection(
+ PermissionBackend permissionBackend,
+ DynamicMap<RestView<ExperimentResource>> views,
+ ListExperiments list) {
+ this.permissionBackend = permissionBackend;
+ this.views = views;
+ this.list = list;
+ }
+
+ @Override
+ public RestView<ConfigResource> list() throws RestApiException {
+ return list;
+ }
+
+ @Override
+ public ExperimentResource parse(ConfigResource parent, IdString id)
+ throws ResourceNotFoundException, Exception {
+ permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
+
+ if (ListExperiments.getExperiments().stream().noneMatch(id.get()::equalsIgnoreCase)) {
+ throw new ResourceNotFoundException(id.get());
+ }
+
+ return new ExperimentResource(id.get());
+ }
+
+ @Override
+ public DynamicMap<RestView<ExperimentResource>> views() {
+ return views;
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/GetExperiment.java b/java/com/google/gerrit/server/restapi/config/GetExperiment.java
new file mode 100644
index 0000000000..1c3bd0a463
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/GetExperiment.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.config;
+
+import com.google.gerrit.extensions.common.ExperimentInfo;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.config.ExperimentResource;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class GetExperiment implements RestReadView<ExperimentResource> {
+ private final ExperimentFeatures experimentFeatures;
+
+ @Inject
+ public GetExperiment(ExperimentFeatures experimentFeatures) {
+ this.experimentFeatures = experimentFeatures;
+ }
+
+ @Override
+ public Response<ExperimentInfo> apply(ExperimentResource resource) {
+ return Response.ok(getExperimentInfo(resource.getName()));
+ }
+
+ public ExperimentInfo getExperimentInfo(String experimentName) {
+ ExperimentInfo experimentInfo = new ExperimentInfo();
+ experimentInfo.enabled = experimentFeatures.isFeatureEnabled(experimentName);
+ return experimentInfo;
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/GetIndex.java b/java/com/google/gerrit/server/restapi/config/GetIndex.java
new file mode 100644
index 0000000000..c96c66e16e
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/GetIndex.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.config;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.config.IndexResource;
+
+public class GetIndex implements RestReadView<IndexResource> {
+
+ @Override
+ public Response<IndexInfo> apply(IndexResource rsrc)
+ throws AuthException, BadRequestException, ResourceConflictException, Exception {
+ return Response.ok(IndexInfo.fromIndexDefinition(rsrc.getIndexDefinition()));
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/GetIndexVersion.java b/java/com/google/gerrit/server/restapi/config/GetIndexVersion.java
new file mode 100644
index 0000000000..195551110a
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/GetIndexVersion.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.config;
+
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.index.IndexCollection;
+import com.google.gerrit.server.config.IndexVersionResource;
+import com.google.gerrit.server.restapi.config.IndexInfo.IndexVersionInfo;
+
+public class GetIndexVersion implements RestReadView<IndexVersionResource> {
+
+ @Override
+ public Response<IndexVersionInfo> apply(IndexVersionResource rsrc)
+ throws ResourceNotFoundException {
+ IndexCollection<?, ?, ?> indexCollection = rsrc.getIndexDefinition().getIndexCollection();
+ int version = rsrc.getIndex().getSchema().getVersion();
+ boolean isSearch = indexCollection.getSearchIndex().getSchema().getVersion() == version;
+ boolean isWrite = indexCollection.getWriteIndex(version) != null;
+ return Response.ok(IndexInfo.IndexVersionInfo.create(isWrite, isSearch));
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index 9e0eac7668..a9f16e7052 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -18,6 +18,7 @@ import static java.util.stream.Collectors.toList;
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.Lists;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.ContributorAgreement;
@@ -67,7 +68,6 @@ import com.google.inject.Inject;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -175,7 +175,7 @@ public class GetServerInfo implements RestReadView<ConfigResource> {
info.gitBasicAuthPolicy = authConfig.getGitBasicAuthPolicy();
if (info.useContributorAgreements != null) {
- Collection<ContributorAgreement> agreements =
+ ImmutableCollection<ContributorAgreement> agreements =
projectCache.getAllProjects().getConfig().getContributorAgreements().values();
if (!agreements.isEmpty()) {
info.contributorAgreements = Lists.newArrayListWithCapacity(agreements.size());
@@ -252,6 +252,7 @@ public class GetServerInfo implements RestReadView<ConfigResource> {
private DownloadSchemeInfo getDownloadSchemeInfo(DownloadScheme scheme) {
DownloadSchemeInfo info = new DownloadSchemeInfo();
info.url = scheme.getUrl("${project}");
+ info.description = scheme.getDescription();
info.isAuthRequired = toBoolean(scheme.isAuthRequired());
info.isAuthSupported = toBoolean(scheme.isAuthSupported());
diff --git a/java/com/google/gerrit/server/restapi/config/GetSummary.java b/java/com/google/gerrit/server/restapi/config/GetSummary.java
index faa3871029..77af0f37c2 100644
--- a/java/com/google/gerrit/server/restapi/config/GetSummary.java
+++ b/java/com/google/gerrit/server/restapi/config/GetSummary.java
@@ -33,9 +33,7 @@ import java.lang.management.ThreadMXBean;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.Arrays;
-import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -75,7 +73,7 @@ public class GetSummary implements RestReadView<ConfigResource> {
}
private TaskSummaryInfo getTaskSummary() {
- Collection<Task<?>> pending = workQueue.getTasks();
+ List<Task<?>> pending = workQueue.getTasks();
int tasksTotal = pending.size();
int tasksStopping = 0;
int tasksRunning = 0;
@@ -203,7 +201,7 @@ public class GetSummary implements RestReadView<ConfigResource> {
// Ignored
}
- jvmSummary.currentWorkingDirectory = path(Paths.get(".").toAbsolutePath().getParent());
+ jvmSummary.currentWorkingDirectory = path(Path.of(".").toAbsolutePath().getParent());
jvmSummary.site = path(sitePath);
return jvmSummary;
}
diff --git a/java/com/google/gerrit/server/restapi/config/IndexCollection.java b/java/com/google/gerrit/server/restapi/config/IndexCollection.java
new file mode 100644
index 0000000000..99a57182df
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/IndexCollection.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.config;
+
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.index.IndexDefinition;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.config.IndexResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.util.Collection;
+
+@RequiresCapability(MAINTAIN_SERVER)
+@Singleton
+public class IndexCollection implements ChildCollection<ConfigResource, IndexResource> {
+ private final DynamicMap<RestView<IndexResource>> views;
+ private final Provider<ListIndexes> list;
+ private final Collection<IndexDefinition<?, ?, ?>> defs;
+
+ @Inject
+ IndexCollection(
+ DynamicMap<RestView<IndexResource>> views,
+ Provider<ListIndexes> list,
+ Collection<IndexDefinition<?, ?, ?>> defs) {
+ this.views = views;
+ this.list = list;
+ this.defs = defs;
+ }
+
+ @Override
+ public IndexResource parse(ConfigResource parent, IdString id) throws ResourceNotFoundException {
+ String indexName = id.toString();
+ for (IndexDefinition<?, ?, ?> def : defs) {
+ if (def.getName().equals(indexName)) {
+ return new IndexResource(def);
+ }
+ }
+ throw new ResourceNotFoundException("Unknown index requested: " + indexName);
+ }
+
+ @Override
+ public RestView<ConfigResource> list() throws RestApiException {
+ return list.get();
+ }
+
+ @Override
+ public DynamicMap<RestView<IndexResource>> views() {
+ return views;
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/IndexInfo.java b/java/com/google/gerrit/server/restapi/config/IndexInfo.java
new file mode 100644
index 0000000000..5d52fe1b55
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/IndexInfo.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.config;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.gerrit.index.Index;
+import com.google.gerrit.index.IndexCollection;
+import com.google.gerrit.index.IndexDefinition;
+
+@AutoValue
+public abstract class IndexInfo {
+
+ public static IndexInfo fromIndexCollection(
+ String name, IndexCollection<?, ?, ?> indexCollection) {
+ ImmutableSortedMap.Builder<Integer, IndexVersionInfo> versions =
+ ImmutableSortedMap.naturalOrder();
+ int searchIndexVersion = indexCollection.getSearchIndex().getSchema().getVersion();
+ boolean searchIndexAdded = false;
+ for (Index<?, ?> index : indexCollection.getWriteIndexes()) {
+ boolean isSearchIndex = index.getSchema().getVersion() == searchIndexVersion;
+ versions.put(index.getSchema().getVersion(), IndexVersionInfo.create(true, isSearchIndex));
+ searchIndexAdded = searchIndexAdded || isSearchIndex;
+ }
+ if (!searchIndexAdded) {
+ versions.put(searchIndexVersion, IndexVersionInfo.create(false, true));
+ }
+
+ return new AutoValue_IndexInfo(name, versions.build());
+ }
+
+ public static IndexInfo fromIndexDefinition(IndexDefinition<?, ?, ?> def) {
+ return fromIndexCollection(def.getName(), def.getIndexCollection());
+ }
+
+ public abstract String getName();
+
+ public abstract ImmutableMap<Integer, IndexVersionInfo> getVersions();
+
+ @AutoValue
+ public abstract static class IndexVersionInfo {
+ static IndexVersionInfo create(boolean write, boolean search) {
+ return new AutoValue_IndexInfo_IndexVersionInfo(write, search);
+ }
+
+ abstract boolean isWrite();
+
+ abstract boolean isSearch();
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/IndexVersionsCollection.java b/java/com/google/gerrit/server/restapi/config/IndexVersionsCollection.java
new file mode 100644
index 0000000000..44c49f3a91
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/IndexVersionsCollection.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.config;
+
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.index.Index;
+import com.google.gerrit.index.IndexCollection;
+import com.google.gerrit.index.IndexDefinition;
+import com.google.gerrit.server.config.IndexResource;
+import com.google.gerrit.server.config.IndexVersionResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@RequiresCapability(MAINTAIN_SERVER)
+@Singleton
+public class IndexVersionsCollection
+ implements ChildCollection<IndexResource, IndexVersionResource> {
+
+ private final DynamicMap<RestView<IndexVersionResource>> views;
+ private final Provider<ListIndexVersions> list;
+
+ @Inject
+ IndexVersionsCollection(
+ DynamicMap<RestView<IndexVersionResource>> views, Provider<ListIndexVersions> list) {
+ this.views = views;
+ this.list = list;
+ }
+
+ @Override
+ public RestView<IndexResource> list() throws RestApiException {
+ return list.get();
+ }
+
+ @Override
+ public IndexVersionResource parse(IndexResource parent, IdString id)
+ throws ResourceNotFoundException, Exception {
+ try {
+ int version = Integer.parseInt(id.get());
+ IndexDefinition<?, ?, ?> def = parent.getIndexDefinition();
+ IndexCollection<?, ?, ?> indexCollection = def.getIndexCollection();
+ Index<?, ?> index = indexCollection.getWriteIndex(version);
+ if (index == null) {
+ Index<?, ?> searchIndex = indexCollection.getSearchIndex();
+ if (searchIndex.getSchema().getVersion() == version) {
+ index = searchIndex;
+ }
+ }
+ if (index != null) {
+ return new IndexVersionResource(def, index);
+ }
+ } catch (NumberFormatException e) {
+ throw new ResourceNotFoundException("'" + id.get() + "' is not a number", e);
+ }
+
+ throw new ResourceNotFoundException();
+ }
+
+ @Override
+ public DynamicMap<RestView<IndexVersionResource>> views() {
+ return views;
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/ListExperiments.java b/java/com/google/gerrit/server/restapi/config/ListExperiments.java
new file mode 100644
index 0000000000..a41b917037
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/ListExperiments.java
@@ -0,0 +1,88 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.config;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.extensions.common.ExperimentInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.function.Function;
+import org.kohsuke.args4j.Option;
+
+/** List capabilities visible to the calling user. */
+public class ListExperiments implements RestReadView<ConfigResource> {
+ public static ImmutableList<String> getExperiments() {
+ return Arrays.stream(ExperimentFeaturesConstants.class.getDeclaredFields())
+ .filter(field -> field.getType().equals(String.class))
+ .map(
+ field -> {
+ try {
+ return (String) field.get(null);
+ } catch (IllegalAccessException e) {
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .sorted()
+ .collect(toImmutableList());
+ }
+
+ private final PermissionBackend permissionBackend;
+ private final ExperimentFeatures experimentFeatures;
+ private final GetExperiment getExperiment;
+
+ private boolean enabledOnly;
+
+ @Option(name = "--enabled-only", usage = "only return enabled experiments")
+ public void setEnabledOnly(boolean enabledOnly) {
+ this.enabledOnly = enabledOnly;
+ }
+
+ @Inject
+ public ListExperiments(
+ PermissionBackend permissionBackend,
+ ExperimentFeatures experimentFeatures,
+ GetExperiment getExperiment) {
+ this.permissionBackend = permissionBackend;
+ this.experimentFeatures = experimentFeatures;
+ this.getExperiment = getExperiment;
+ }
+
+ @Override
+ public Response<ImmutableMap<String, ExperimentInfo>> apply(ConfigResource resource)
+ throws AuthException, PermissionBackendException {
+ permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
+ return Response.ok(
+ getExperiments().stream()
+ .filter(
+ experimentName ->
+ !enabledOnly || experimentFeatures.isFeatureEnabled(experimentName))
+ .collect(toImmutableMap(Function.identity(), getExperiment::getExperimentInfo)));
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/ListIndexVersions.java b/java/com/google/gerrit/server/restapi/config/ListIndexVersions.java
new file mode 100644
index 0000000000..91bdae0a58
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/ListIndexVersions.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.config;
+
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.config.IndexResource;
+import java.util.Map;
+
+@RequiresCapability(MAINTAIN_SERVER)
+public class ListIndexVersions implements RestReadView<IndexResource> {
+
+ @Override
+ public Response<Map<Integer, IndexInfo.IndexVersionInfo>> apply(IndexResource rsrc)
+ throws AuthException, BadRequestException, ResourceConflictException, Exception {
+ IndexInfo info = IndexInfo.fromIndexDefinition(rsrc.getIndexDefinition());
+ return Response.ok(info.getVersions());
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/ListIndexes.java b/java/com/google/gerrit/server/restapi/config/ListIndexes.java
new file mode 100644
index 0000000000..2e6664b360
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/ListIndexes.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.config;
+
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.index.IndexDefinition;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.inject.Inject;
+import java.util.Collection;
+
+@RequiresCapability(MAINTAIN_SERVER)
+public class ListIndexes implements RestReadView<ConfigResource> {
+ private final Collection<IndexDefinition<?, ?, ?>> defs;
+
+ @Inject
+ public ListIndexes(Collection<IndexDefinition<?, ?, ?>> defs) {
+ this.defs = defs;
+ }
+
+ private ImmutableList<IndexInfo> getIndexInfos() {
+ ImmutableList.Builder<IndexInfo> indexInfos = ImmutableList.builder();
+ for (IndexDefinition<?, ?, ?> def : defs) {
+ indexInfos.add(IndexInfo.fromIndexDefinition(def));
+ }
+ return indexInfos.build();
+ }
+
+ @Override
+ public Response<Object> apply(ConfigResource rsrc) {
+ return Response.ok(getIndexInfos());
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/PostCaches.java b/java/com/google/gerrit/server/restapi/config/PostCaches.java
index c9480c54c2..59dcc24233 100644
--- a/java/com/google/gerrit/server/restapi/config/PostCaches.java
+++ b/java/com/google/gerrit/server/restapi/config/PostCaches.java
@@ -103,7 +103,8 @@ public class PostCaches implements RestCollectionModifyView<ConfigResource, Cach
if (FlushCache.WEB_SESSIONS.equals(cacheResource.getName())) {
continue;
}
- flushCache.apply(cacheResource, null);
+ @SuppressWarnings("unused")
+ var unused = flushCache.apply(cacheResource, null);
}
}
@@ -129,7 +130,8 @@ public class PostCaches implements RestCollectionModifyView<ConfigResource, Cach
}
for (CacheResource rsrc : cacheResources) {
- flushCache.apply(rsrc, null);
+ @SuppressWarnings("unused")
+ var unused = flushCache.apply(rsrc, null);
}
}
}
diff --git a/java/com/google/gerrit/server/restapi/config/ReindexIndexVersion.java b/java/com/google/gerrit/server/restapi/config/ReindexIndexVersion.java
new file mode 100644
index 0000000000..21cd1c12fd
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/ReindexIndexVersion.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.config;
+
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.index.IndexDefinition;
+import com.google.gerrit.server.config.IndexVersionResource;
+import com.google.gerrit.server.index.IndexVersionReindexer;
+import com.google.gerrit.server.restapi.config.ReindexIndexVersion.Input;
+import com.google.inject.Inject;
+
+public class ReindexIndexVersion implements RestModifyView<IndexVersionResource, Input> {
+ public static class Input {
+ public boolean reuse;
+ public boolean notifyListeners;
+ }
+
+ private final IndexVersionReindexer indexVersionReindexer;
+
+ @Inject
+ ReindexIndexVersion(IndexVersionReindexer indexVersionReindexer) {
+ this.indexVersionReindexer = indexVersionReindexer;
+ }
+
+ @Override
+ public Response<?> apply(IndexVersionResource rsrc, Input input)
+ throws ResourceNotFoundException {
+ IndexDefinition<?, ?, ?> def = rsrc.getIndexDefinition();
+ int version = rsrc.getIndex().getSchema().getVersion();
+ @SuppressWarnings("unused")
+ var unused = indexVersionReindexer.reindex(def, version, input.reuse, input.notifyListeners);
+ return Response.accepted(
+ String.format("Index %s version %d submitted for reindexing", def.getName(), version));
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/ReloadConfig.java b/java/com/google/gerrit/server/restapi/config/ReloadConfig.java
index c8f2ed6dc7..9c773fe7ff 100644
--- a/java/com/google/gerrit/server/restapi/config/ReloadConfig.java
+++ b/java/com/google/gerrit/server/restapi/config/ReloadConfig.java
@@ -16,7 +16,8 @@ package com.google.gerrit.server.restapi.config;
import static com.google.common.collect.ImmutableList.toImmutableList;
-import com.google.common.collect.Multimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ListMultimap;
import com.google.gerrit.extensions.api.config.ConfigUpdateEntryInfo;
import com.google.gerrit.extensions.common.Input;
import com.google.gerrit.extensions.restapi.Response;
@@ -52,7 +53,7 @@ public class ReloadConfig implements RestModifyView<ConfigResource, Input> {
public Response<Map<String, List<ConfigUpdateEntryInfo>>> apply(
ConfigResource resource, Input input) throws RestApiException, PermissionBackendException {
permissions.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
- Multimap<UpdateResult, ConfigUpdateEntry> updates = config.reloadConfig();
+ ListMultimap<UpdateResult, ConfigUpdateEntry> updates = config.reloadConfig();
if (updates.isEmpty()) {
return Response.ok(Collections.emptyMap());
}
@@ -64,7 +65,7 @@ public class ReloadConfig implements RestModifyView<ConfigResource, Input> {
e -> toEntryInfos(e.getValue()))));
}
- private static List<ConfigUpdateEntryInfo> toEntryInfos(
+ private static ImmutableList<ConfigUpdateEntryInfo> toEntryInfos(
Collection<ConfigUpdateEntry> updateEntries) {
return updateEntries.stream()
.map(ReloadConfig::toConfigUpdateEntryInfo)
diff --git a/java/com/google/gerrit/server/restapi/config/SnapshotIndex.java b/java/com/google/gerrit/server/restapi/config/SnapshotIndex.java
new file mode 100644
index 0000000000..c50367ff30
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/SnapshotIndex.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.config;
+
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
+
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.index.Index;
+import com.google.gerrit.index.IndexDefinition;
+import com.google.gerrit.server.config.IndexResource;
+import com.google.gerrit.server.restapi.config.SnapshotIndex.Input;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.nio.file.FileAlreadyExistsException;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+
+@RequiresCapability(MAINTAIN_SERVER)
+@Singleton
+public class SnapshotIndex implements RestModifyView<IndexResource, Input> {
+ private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss");
+
+ @Override
+ public Response<?> apply(IndexResource rsrc, Input input) throws IOException {
+ String id = input.id;
+ if (id == null) {
+ id = LocalDateTime.now(ZoneId.systemDefault()).format(formatter);
+ }
+ IndexDefinition<?, ?, ?> def = rsrc.getIndexDefinition();
+ for (Index<?, ?> index : def.getIndexCollection().getWriteIndexes()) {
+ try {
+ @SuppressWarnings("unused")
+ var unused = index.snapshot(id);
+ } catch (FileAlreadyExistsException e) {
+ return Response.withStatusCode(SC_CONFLICT, "Snapshot with same ID already exists.");
+ }
+ }
+ SnapshotInfo info = new SnapshotInfo();
+ info.id = id;
+ return Response.ok(info);
+ }
+
+ public static class Input {
+ String id;
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/SnapshotIndexVersion.java b/java/com/google/gerrit/server/restapi/config/SnapshotIndexVersion.java
new file mode 100644
index 0000000000..9f66ab3492
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/SnapshotIndexVersion.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.config;
+
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.index.Index;
+import com.google.gerrit.server.config.IndexVersionResource;
+import com.google.gerrit.server.restapi.config.SnapshotIndexVersion.Input;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+
+@RequiresCapability(MAINTAIN_SERVER)
+@Singleton
+public class SnapshotIndexVersion implements RestModifyView<IndexVersionResource, Input> {
+ private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss");
+
+ @Override
+ public Response<?> apply(IndexVersionResource rsrc, Input input)
+ throws IOException, ResourceNotFoundException {
+ String id = input.id;
+ if (id == null) {
+ id = LocalDateTime.now(ZoneId.systemDefault()).format(formatter);
+ }
+ Index<?, ?> index = rsrc.getIndex();
+ var unused = index.snapshot(id);
+ SnapshotInfo info = new SnapshotInfo();
+ info.id = id;
+ return Response.ok(info);
+ }
+
+ public static class Input {
+ String id;
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/SnapshotIndexes.java b/java/com/google/gerrit/server/restapi/config/SnapshotIndexes.java
new file mode 100644
index 0000000000..6a2c5f8043
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/SnapshotIndexes.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.config;
+
+import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.index.Index;
+import com.google.gerrit.index.IndexDefinition;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.restapi.config.SnapshotIndexes.Input;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.nio.file.FileAlreadyExistsException;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Collection;
+
+@RequiresCapability(GlobalCapability.MAINTAIN_SERVER)
+@Singleton
+public class SnapshotIndexes implements RestModifyView<ConfigResource, Input> {
+ private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss");
+
+ public static class Input {
+ String id;
+ }
+
+ private final Collection<IndexDefinition<?, ?, ?>> defs;
+
+ @Inject
+ SnapshotIndexes(Collection<IndexDefinition<?, ?, ?>> defs) {
+ this.defs = defs;
+ }
+
+ @Override
+ public Response<?> apply(ConfigResource resource, Input input) throws IOException {
+ String id = input.id;
+ if (id == null) {
+ id = LocalDateTime.now(ZoneId.systemDefault()).format(formatter);
+ }
+ for (IndexDefinition<?, ?, ?> def : defs) {
+ for (Index<?, ?> index : def.getIndexCollection().getWriteIndexes()) {
+ try {
+ @SuppressWarnings("unused")
+ var unused = index.snapshot(id);
+ } catch (FileAlreadyExistsException e) {
+ return Response.withStatusCode(SC_CONFLICT, "Snapshot with same ID already exists.");
+ }
+ }
+ }
+ SnapshotInfo info = new SnapshotInfo();
+ info.id = id;
+ return Response.ok(info);
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/SnapshotInfo.java b/java/com/google/gerrit/server/restapi/config/SnapshotInfo.java
new file mode 100644
index 0000000000..addc559f02
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/SnapshotInfo.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.config;
+
+public class SnapshotInfo {
+ public String id;
+}
diff --git a/java/com/google/gerrit/server/restapi/config/package-info.java b/java/com/google/gerrit/server/restapi/config/package-info.java
new file mode 100644
index 0000000000..ba06458996
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.restapi.config;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/restapi/group/CreateGroup.java b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
index 4110eff165..11b3788f37 100644
--- a/java/com/google/gerrit/server/restapi/group/CreateGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.restapi.group;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.entities.Account;
@@ -115,11 +116,13 @@ public class CreateGroup
this.sequences = sequences;
}
+ @CanIgnoreReturnValue
public CreateGroup addOption(ListGroupsOption o) {
json.addOption(o);
return this;
}
+ @CanIgnoreReturnValue
public CreateGroup addOptions(Collection<ListGroupsOption> o) {
json.addOptions(o);
return this;
diff --git a/java/com/google/gerrit/server/restapi/group/GroupJson.java b/java/com/google/gerrit/server/restapi/group/GroupJson.java
index 6d3fa01a8f..4edbc95c74 100644
--- a/java/com/google/gerrit/server/restapi/group/GroupJson.java
+++ b/java/com/google/gerrit/server/restapi/group/GroupJson.java
@@ -19,6 +19,7 @@ import static com.google.gerrit.extensions.client.ListGroupsOption.MEMBERS;
import com.google.common.base.Strings;
import com.google.common.base.Suppliers;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.GroupDescription;
import com.google.gerrit.extensions.client.ListGroupsOption;
@@ -65,11 +66,13 @@ public class GroupJson {
options = EnumSet.noneOf(ListGroupsOption.class);
}
+ @CanIgnoreReturnValue
public GroupJson addOption(ListGroupsOption o) {
options.add(o);
return this;
}
+ @CanIgnoreReturnValue
public GroupJson addOptions(Collection<ListGroupsOption> o) {
options.addAll(o);
return this;
diff --git a/java/com/google/gerrit/server/restapi/group/ListGroups.java b/java/com/google/gerrit/server/restapi/group/ListGroups.java
index 4d9a1e9315..3b16129e25 100644
--- a/java/com/google/gerrit/server/restapi/group/ListGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListGroups.java
@@ -20,6 +20,8 @@ import static java.util.stream.Collectors.toList;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
@@ -286,7 +288,8 @@ public class ListGroups implements RestReadView<TopLevelResource> {
if (limit > 0) {
existingGroups = existingGroups.limit(limit);
}
- List<GroupDescription.Internal> relevantGroups = existingGroups.collect(toImmutableList());
+ ImmutableList<GroupDescription.Internal> relevantGroups =
+ existingGroups.collect(toImmutableList());
List<GroupInfo> groupInfos = Lists.newArrayListWithCapacity(relevantGroups.size());
for (GroupDescription.Internal group : relevantGroups) {
groupInfos.add(json.addOptions(options).format(group));
@@ -376,7 +379,7 @@ public class ListGroups implements RestReadView<TopLevelResource> {
if (limit > 0) {
foundGroups = foundGroups.limit(limit);
}
- List<GroupDescription.Internal> ownedGroups = foundGroups.collect(toImmutableList());
+ ImmutableList<GroupDescription.Internal> ownedGroups = foundGroups.collect(toImmutableList());
List<GroupInfo> groupInfos = new ArrayList<>(ownedGroups.size());
for (GroupDescription.Internal group : ownedGroups) {
groupInfos.add(json.addOptions(options).format(group));
@@ -384,7 +387,8 @@ public class ListGroups implements RestReadView<TopLevelResource> {
return groupInfos;
}
- private Set<GroupDescription.Internal> loadGroups(Collection<AccountGroup.UUID> groupUuids) {
+ private ImmutableSet<GroupDescription.Internal> loadGroups(
+ Collection<AccountGroup.UUID> groupUuids) {
return groupCache.get(groupUuids).values().stream()
.map(InternalGroupDescription::new)
.collect(toImmutableSet());
diff --git a/java/com/google/gerrit/server/restapi/group/ListMembers.java b/java/com/google/gerrit/server/restapi/group/ListMembers.java
index 1882fc51ff..c6da92f760 100644
--- a/java/com/google/gerrit/server/restapi/group/ListMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/ListMembers.java
@@ -20,6 +20,7 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.GroupDescription;
@@ -60,6 +61,7 @@ public class ListMembers implements RestReadView<GroupResource> {
this.accountLoaderFactory = accountLoaderFactory;
}
+ @CanIgnoreReturnValue
public ListMembers setRecursive(boolean recursive) {
this.recursive = recursive;
return this;
@@ -111,7 +113,7 @@ public class ListMembers implements RestReadView<GroupResource> {
GroupDescription.Internal group, GroupControl groupControl)
throws PermissionBackendException {
checkSameGroup(group, groupControl);
- Set<Account.Id> directMembers = getDirectMemberIds(group, groupControl);
+ ImmutableSet<Account.Id> directMembers = getDirectMemberIds(group, groupControl);
return toAccountInfos(directMembers);
}
@@ -138,32 +140,32 @@ public class ListMembers implements RestReadView<GroupResource> {
GroupDescription.Internal group,
GroupControl groupControl,
HashSet<AccountGroup.UUID> seenGroups) {
- Set<Account.Id> directMembers = getDirectMemberIds(group, groupControl);
+ ImmutableSet<Account.Id> directMembers = getDirectMemberIds(group, groupControl);
if (!groupControl.canSeeGroup()) {
return directMembers;
}
- Set<Account.Id> indirectMembers = getIndirectMemberIds(group, seenGroups);
+ ImmutableSet<Account.Id> indirectMembers = getIndirectMemberIds(group, seenGroups);
return Sets.union(directMembers, indirectMembers);
}
- private static Set<Account.Id> getDirectMemberIds(
+ private static ImmutableSet<Account.Id> getDirectMemberIds(
GroupDescription.Internal group, GroupControl groupControl) {
return group.getMembers().stream().filter(groupControl::canSeeMember).collect(toImmutableSet());
}
- private Set<Account.Id> getIndirectMemberIds(
+ private ImmutableSet<Account.Id> getIndirectMemberIds(
GroupDescription.Internal group, HashSet<AccountGroup.UUID> seenGroups) {
- Set<Account.Id> indirectMembers = new HashSet<>();
- Set<AccountGroup.UUID> subgroupMembersToLoad = new HashSet<>();
+ ImmutableSet.Builder<Account.Id> indirectMembers = ImmutableSet.builder();
+ ImmutableSet.Builder<AccountGroup.UUID> subgroupMembersToLoad = ImmutableSet.builder();
for (AccountGroup.UUID subgroupUuid : group.getSubgroups()) {
if (!seenGroups.contains(subgroupUuid)) {
seenGroups.add(subgroupUuid);
subgroupMembersToLoad.add(subgroupUuid);
}
}
- groupCache.get(subgroupMembersToLoad).values().stream()
+ groupCache.get(subgroupMembersToLoad.build()).values().stream()
.map(InternalGroupDescription::new)
.forEach(
subgroup -> {
@@ -171,7 +173,7 @@ public class ListMembers implements RestReadView<GroupResource> {
indirectMembers.addAll(getTransitiveMemberIds(subgroup, subgroupControl, seenGroups));
});
- return indirectMembers;
+ return indirectMembers.build();
}
private static void checkSameGroup(GroupDescription.Internal group, GroupControl groupControl) {
diff --git a/java/com/google/gerrit/server/restapi/group/QueryGroups.java b/java/com/google/gerrit/server/restapi/group/QueryGroups.java
index fed23028e5..547ccd67e5 100644
--- a/java/com/google/gerrit/server/restapi/group/QueryGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/QueryGroups.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.restapi.group;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.extensions.client.ListGroupsOption;
@@ -115,13 +116,11 @@ public class QueryGroups implements RestReadView<TopLevelResource> {
queryProcessor.setStart(start);
}
- if (limit != 0) {
- queryProcessor.setUserProvidedLimit(limit);
- }
+ queryProcessor.setUserProvidedLimit(limit, /* applyDefaultLimit */ true);
try {
QueryResult<InternalGroup> result = queryProcessor.query(queryBuilder.parse(query));
- List<InternalGroup> groups = result.entities();
+ ImmutableList<InternalGroup> groups = result.entities();
ArrayList<GroupInfo> groupInfos = Lists.newArrayListWithCapacity(groups.size());
json.addOptions(options);
diff --git a/java/com/google/gerrit/server/restapi/group/package-info.java b/java/com/google/gerrit/server/restapi/group/package-info.java
new file mode 100644
index 0000000000..5372db26fd
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/group/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.restapi.group;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/restapi/package-info.java b/java/com/google/gerrit/server/restapi/package-info.java
new file mode 100644
index 0000000000..28b6003cd0
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.restapi;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
index 338ff0d60c..3e8002bd73 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
@@ -14,17 +14,11 @@
package com.google.gerrit.server.restapi.project;
-import static com.google.gerrit.server.project.ProjectCache.illegalState;
-import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
-
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
import com.google.gerrit.entities.AccessSection;
import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.InvalidNameException;
import com.google.gerrit.extensions.api.access.ProjectAccessInput;
import com.google.gerrit.extensions.common.ChangeInfo;
@@ -33,89 +27,35 @@ import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.Sequences;
-import com.google.gerrit.server.approval.ApprovalsUtil;
-import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ChangeJson;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.permissions.ProjectPermission;
-import com.google.gerrit.server.permissions.RefPermission;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
-import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.UpdateException;
-import com.google.gerrit.server.update.context.RefUpdateContext;
-import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
@Singleton
public class CreateAccessChange implements RestModifyView<ProjectResource, ProjectAccessInput> {
- private final PermissionBackend permissionBackend;
- private final Sequences seq;
- private final ChangeInserter.Factory changeInserterFactory;
- private final BatchUpdate.Factory updateFactory;
- private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
private final SetAccessUtil setAccess;
private final ChangeJson.Factory jsonFactory;
- private final ProjectCache projectCache;
- private final ProjectConfig.Factory projectConfigFactory;
+ private final RepoMetaDataUpdater repoMetaDataUpdater;
@Inject
CreateAccessChange(
- PermissionBackend permissionBackend,
- ChangeInserter.Factory changeInserterFactory,
- BatchUpdate.Factory updateFactory,
- Sequences seq,
- Provider<MetaDataUpdate.User> metaDataUpdateFactory,
SetAccessUtil accessUtil,
ChangeJson.Factory jsonFactory,
- ProjectCache projectCache,
- ProjectConfig.Factory projectConfigFactory) {
- this.permissionBackend = permissionBackend;
- this.seq = seq;
- this.changeInserterFactory = changeInserterFactory;
- this.updateFactory = updateFactory;
- this.metaDataUpdateFactory = metaDataUpdateFactory;
+ RepoMetaDataUpdater repoMetaDataUpdater) {
this.setAccess = accessUtil;
this.jsonFactory = jsonFactory;
- this.projectCache = projectCache;
- this.projectConfigFactory = projectConfigFactory;
+ this.repoMetaDataUpdater = repoMetaDataUpdater;
}
@Override
public Response<ChangeInfo> apply(ProjectResource rsrc, ProjectAccessInput input)
- throws PermissionBackendException, AuthException, IOException, ConfigInvalidException,
- InvalidNameException, UpdateException, RestApiException {
- PermissionBackend.ForProject forProject =
- permissionBackend.user(rsrc.getUser()).project(rsrc.getNameKey());
- if (!check(forProject, ProjectPermission.READ_CONFIG)) {
- throw new AuthException(RefNames.REFS_CONFIG + " not visible");
- }
- if (!check(forProject, ProjectPermission.WRITE_CONFIG)) {
- try {
- forProject.ref(RefNames.REFS_CONFIG).check(RefPermission.CREATE_CHANGE);
- } catch (AuthException denied) {
- throw new AuthException("cannot create change for " + RefNames.REFS_CONFIG, denied);
- }
- }
- projectCache
- .get(rsrc.getNameKey())
- .orElseThrow(illegalState(rsrc.getNameKey()))
- .checkStatePermitsWrite();
-
- MetaDataUpdate.User metaDataUpdateUser = metaDataUpdateFactory.get();
+ throws PermissionBackendException, IOException, ConfigInvalidException, InvalidNameException,
+ UpdateException, RestApiException {
ImmutableList<AccessSection> removals =
setAccess.getAccessSections(input.remove, /* rejectNonResolvableGroups= */ false);
ImmutableList<AccessSection> additions =
@@ -123,81 +63,30 @@ public class CreateAccessChange implements RestModifyView<ProjectResource, Proje
Project.NameKey newParentProjectName =
input.parent == null ? null : Project.nameKey(input.parent);
-
- try (MetaDataUpdate md = metaDataUpdateUser.create(rsrc.getNameKey())) {
- ProjectConfig config = projectConfigFactory.read(md);
- ObjectId oldCommit = config.getRevision();
- String oldCommitSha1 = oldCommit == null ? null : oldCommit.getName();
-
- setAccess.validateChanges(config, removals, additions);
- setAccess.applyChanges(config, removals, additions);
- try {
- setAccess.setParentName(
- rsrc.getUser().asIdentifiedUser(),
- config,
- rsrc.getNameKey(),
- newParentProjectName,
- false);
- } catch (AuthException e) {
- throw new IllegalStateException(e);
- }
-
- if (!Strings.isNullOrEmpty(input.message)) {
- if (!input.message.endsWith("\n")) {
- input.message += "\n";
- }
- md.setMessage(input.message);
- } else {
- md.setMessage("Review access change\n");
- }
-
- md.setInsertChangeId(true);
- Change.Id changeId = Change.id(seq.nextChangeId());
- try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
- RevCommit commit =
- config.commitToNewRef(
- md, PatchSet.id(changeId, Change.INITIAL_PATCH_SET_ID).toRefName());
-
- if (commit.name().equals(oldCommitSha1)) {
- throw new BadRequestException("no change");
- }
-
- try (ObjectInserter objInserter = md.getRepository().newObjectInserter();
- ObjectReader objReader = objInserter.newReader();
- RevWalk rw = new RevWalk(objReader);
- BatchUpdate bu =
- updateFactory.create(rsrc.getNameKey(), rsrc.getUser(), TimeUtil.now())) {
- bu.setRepository(md.getRepository(), rw, objInserter);
- ChangeInserter ins = newInserter(changeId, commit);
- bu.insertChange(ins);
- bu.execute();
- return Response.created(jsonFactory.noOptions().format(ins.getChange()));
- }
- }
+ String message = !Strings.isNullOrEmpty(input.message) ? input.message : "Review access change";
+ try {
+ Change change =
+ repoMetaDataUpdater.updateAndCreateChangeForReview(
+ rsrc.getNameKey(),
+ rsrc.getUser(),
+ message,
+ config -> {
+ setAccess.validateChanges(config, removals, additions);
+ setAccess.applyChanges(config, removals, additions);
+ try {
+ setAccess.setParentName(
+ rsrc.getUser().asIdentifiedUser(),
+ config,
+ rsrc.getNameKey(),
+ newParentProjectName,
+ false);
+ } catch (AuthException e) {
+ throw new IllegalStateException(e);
+ }
+ });
+ return Response.created(jsonFactory.noOptions().format(change));
} catch (InvalidNameException e) {
throw new BadRequestException(e.toString());
}
}
-
- // ProjectConfig doesn't currently support fusing into a BatchUpdate.
- @SuppressWarnings("deprecation")
- private ChangeInserter newInserter(Change.Id changeId, RevCommit commit) {
- return changeInserterFactory
- .create(changeId, commit, RefNames.REFS_CONFIG)
- .setMessage(
- // Same message as in ReceiveCommits.CreateRequest.
- ApprovalsUtil.renderMessageWithApprovals(1, ImmutableMap.of(), ImmutableMap.of()))
- .setValidate(false)
- .setUpdateRef(false);
- }
-
- private boolean check(PermissionBackend.ForProject perm, ProjectPermission p)
- throws PermissionBackendException {
- try {
- perm.check(p);
- return true;
- } catch (AuthException denied) {
- return false;
- }
- }
}
diff --git a/java/com/google/gerrit/server/restapi/project/CreateLabel.java b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
index ad79a14fc0..a233834f88 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateLabel.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
@@ -176,7 +176,8 @@ public class CreateLabel
if (input.copyCondition != null) {
try {
- approvalQueryBuilder.parse(input.copyCondition);
+ @SuppressWarnings("unused")
+ var unused = approvalQueryBuilder.parse(input.copyCondition);
} catch (QueryParseException e) {
throw new BadRequestException(
"unable to parse copy condition. got: " + input.copyCondition + ". " + e.getMessage(),
diff --git a/java/com/google/gerrit/server/restapi/project/CreateProject.java b/java/com/google/gerrit/server/restapi/project/CreateProject.java
index 04819d8d2b..8be96b51b6 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateProject.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateProject.java
@@ -186,7 +186,9 @@ public class CreateProject
ConfigInput in = new ConfigInput();
in.pluginConfigValues = input.pluginConfigValues;
in.description = args.projectDescription;
- putConfig.get().apply(projectState, in);
+
+ @SuppressWarnings("unused")
+ var unused = putConfig.get().apply(projectState, in);
}
return Response.created(json.format(projectState));
} finally {
diff --git a/java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java b/java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java
index 2aeba891ac..a46211c8db 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.restapi.project;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.extensions.common.SubmitRequirementInfo;
@@ -41,7 +42,6 @@ import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.List;
import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -136,7 +136,7 @@ public class CreateSubmitRequirement
.setAllowOverrideInChildProjects(input.allowOverrideInChildProjects)
.build();
- List<String> validationMessages =
+ ImmutableList<String> validationMessages =
submitRequirementExpressionsValidator.validateExpressions(submitRequirement);
if (!validationMessages.isEmpty()) {
throw new BadRequestException(
diff --git a/java/com/google/gerrit/server/restapi/project/GarbageCollect.java b/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
index 7457eb744d..b9dcc5cef2 100644
--- a/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
+++ b/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
@@ -85,7 +85,8 @@ public class GarbageCollect
new Runnable() {
@Override
public void run() {
- runGC(project, input, null);
+ @SuppressWarnings("unused")
+ var unused = runGC(project, input, null);
}
@Override
diff --git a/java/com/google/gerrit/server/restapi/project/GetHead.java b/java/com/google/gerrit/server/restapi/project/GetHead.java
index 4e0a144799..be84e6b59c 100644
--- a/java/com/google/gerrit/server/restapi/project/GetHead.java
+++ b/java/com/google/gerrit/server/restapi/project/GetHead.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.restapi.project;
import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
@@ -54,8 +55,9 @@ public class GetHead implements RestReadView<ProjectResource> {
@Override
public Response<String> apply(ProjectResource rsrc)
- throws AuthException, ResourceNotFoundException, IOException, PermissionBackendException {
- rsrc.getProjectState().statePermitsRead();
+ throws AuthException, ResourceNotFoundException, IOException, PermissionBackendException,
+ ResourceConflictException {
+ rsrc.getProjectState().checkStatePermitsRead();
try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
Ref head = repo.getRefDatabase().exactRef(Constants.HEAD);
if (head == null) {
diff --git a/java/com/google/gerrit/server/restapi/project/IndexChanges.java b/java/com/google/gerrit/server/restapi/project/IndexChanges.java
index 6ad0005330..532bd245cb 100644
--- a/java/com/google/gerrit/server/restapi/project/IndexChanges.java
+++ b/java/com/google/gerrit/server/restapi/project/IndexChanges.java
@@ -32,7 +32,6 @@ import com.google.gerrit.server.index.change.AllChangesIndexer;
import com.google.gerrit.server.index.change.ChangeIndexer;
import com.google.gerrit.server.project.ProjectResource;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.util.concurrent.Future;
import org.eclipse.jgit.util.io.NullOutputStream;
@@ -42,18 +41,18 @@ import org.eclipse.jgit.util.io.NullOutputStream;
public class IndexChanges implements RestModifyView<ProjectResource, Input> {
private final MultiProgressMonitor.Factory multiProgressMonitorFactory;
- private final Provider<AllChangesIndexer> allChangesIndexerProvider;
+ private final AllChangesIndexer.Factory allChangesIndexerFactory;
private final ChangeIndexer indexer;
private final ListeningExecutorService executor;
@Inject
IndexChanges(
MultiProgressMonitor.Factory multiProgressMonitorFactory,
- Provider<AllChangesIndexer> allChangesIndexerProvider,
+ AllChangesIndexer.Factory allChangesIndexerFactory,
ChangeIndexer indexer,
@IndexExecutor(BATCH) ListeningExecutorService executor) {
this.multiProgressMonitorFactory = multiProgressMonitorFactory;
- this.allChangesIndexerProvider = allChangesIndexerProvider;
+ this.allChangesIndexerFactory = allChangesIndexerFactory;
this.indexer = indexer;
this.executor = executor;
}
@@ -65,7 +64,7 @@ public class IndexChanges implements RestModifyView<ProjectResource, Input> {
multiProgressMonitorFactory
.create(ByteStreams.nullOutputStream(), TaskKind.INDEXING, "Reindexing project")
.beginSubTask("", MultiProgressMonitor.UNKNOWN);
- AllChangesIndexer allChangesIndexer = allChangesIndexerProvider.get();
+ AllChangesIndexer allChangesIndexer = allChangesIndexerFactory.create();
allChangesIndexer.setVerboseOut(NullOutputStream.INSTANCE);
// The REST call is just a trigger for async reindexing, so it is safe to ignore the future's
// return value.
diff --git a/java/com/google/gerrit/server/restapi/project/ListBranches.java b/java/com/google/gerrit/server/restapi/project/ListBranches.java
index 3cb412a50c..1c2ce5a5de 100644
--- a/java/com/google/gerrit/server/restapi/project/ListBranches.java
+++ b/java/com/google/gerrit/server/restapi/project/ListBranches.java
@@ -56,7 +56,6 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
-import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
@@ -232,7 +231,7 @@ public class ListBranches implements RestReadView<ProjectResource> {
throws IOException, ResourceNotFoundException {
List<Ref> refs;
try (Repository db = repoManager.openRepository(rsrc.getNameKey())) {
- Collection<Ref> heads = db.getRefDatabase().getRefsByPrefix(Constants.R_HEADS);
+ List<Ref> heads = db.getRefDatabase().getRefsByPrefix(Constants.R_HEADS);
refs = new ArrayList<>(heads.size() + 3);
refs.addAll(heads);
refs.addAll(
diff --git a/java/com/google/gerrit/server/restapi/project/ListDashboards.java b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
index 8cedd603e4..59a33a2b00 100644
--- a/java/com/google/gerrit/server/restapi/project/ListDashboards.java
+++ b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
@@ -74,7 +74,7 @@ public class ListDashboards implements RestReadView<ProjectResource> {
List<List<DashboardInfo>> all = new ArrayList<>();
boolean setDefault = true;
for (ProjectState ps : tree(rsrc)) {
- List<DashboardInfo> list = scan(ps, project, setDefault);
+ ImmutableList<DashboardInfo> list = scan(ps, project, setDefault);
for (DashboardInfo d : list) {
if (d.isDefault != null && Boolean.TRUE.equals(d.isDefault)) {
setDefault = false;
diff --git a/java/com/google/gerrit/server/restapi/project/ListLabels.java b/java/com/google/gerrit/server/restapi/project/ListLabels.java
index 56ee4cd3ce..683c107702 100644
--- a/java/com/google/gerrit/server/restapi/project/ListLabels.java
+++ b/java/com/google/gerrit/server/restapi/project/ListLabels.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.restapi.project;
+import com.google.common.collect.ImmutableCollection;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.extensions.common.LabelDefinitionInfo;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -29,7 +30,6 @@ import com.google.gerrit.server.project.ProjectState;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import org.kohsuke.args4j.Option;
@@ -80,7 +80,8 @@ public class ListLabels implements RestReadView<ProjectResource> {
}
private List<LabelDefinitionInfo> listLabels(ProjectState projectState) {
- Collection<LabelType> labelTypes = projectState.getConfig().getLabelSections().values();
+ ImmutableCollection<LabelType> labelTypes =
+ projectState.getConfig().getLabelSections().values();
List<LabelDefinitionInfo> labels = new ArrayList<>(labelTypes.size());
for (LabelType labelType : labelTypes) {
labels.add(LabelDefinitionJson.format(projectState.getNameKey(), labelType));
diff --git a/java/com/google/gerrit/server/restapi/project/ListTags.java b/java/com/google/gerrit/server/restapi/project/ListTags.java
index 83d29de075..24bbb2482c 100644
--- a/java/com/google/gerrit/server/restapi/project/ListTags.java
+++ b/java/com/google/gerrit/server/restapi/project/ListTags.java
@@ -15,12 +15,12 @@
package com.google.gerrit.server.restapi.project;
import static com.google.gerrit.entities.RefNames.isConfigRef;
-import static java.util.Comparator.comparing;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.common.ListTagSortOption;
import com.google.gerrit.extensions.common.WebLinkInfo;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -42,6 +42,7 @@ import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Constants;
@@ -58,6 +59,7 @@ public class ListTags implements RestReadView<ProjectResource> {
private final GitRepositoryManager repoManager;
private final PermissionBackend permissionBackend;
private final WebLinks links;
+ private final TagSorter tagSorter;
@Option(
name = "--limit",
@@ -78,6 +80,14 @@ public class ListTags implements RestReadView<ProjectResource> {
}
@Option(
+ name = "--descending",
+ aliases = {"-d"},
+ usage = "return the tags in descending order")
+ public void setDescendingOrder(boolean descendingOrder) {
+ this.descendingOrder = descendingOrder;
+ }
+
+ @Option(
name = "--match",
aliases = {"-m"},
metaVar = "MATCH",
@@ -95,24 +105,40 @@ public class ListTags implements RestReadView<ProjectResource> {
this.matchRegex = matchRegex;
}
+ @Option(
+ name = "--sort-by",
+ aliases = {"-sortby"},
+ usage = "sort the tags")
+ private void setSortBy(ListTagSortOption sortBy) {
+ this.sortBy = sortBy;
+ }
+
private int limit;
private int start;
+ private boolean descendingOrder;
private String matchSubstring;
private String matchRegex;
+ private ListTagSortOption sortBy = ListTagSortOption.REF;
@Inject
public ListTags(
- GitRepositoryManager repoManager, PermissionBackend permissionBackend, WebLinks webLinks) {
+ GitRepositoryManager repoManager,
+ PermissionBackend permissionBackend,
+ WebLinks webLinks,
+ TagSorter tagSorter) {
this.repoManager = repoManager;
this.permissionBackend = permissionBackend;
this.links = webLinks;
+ this.tagSorter = tagSorter;
}
public ListTags request(ListRefsRequest<TagInfo> request) {
this.setLimit(request.getLimit());
this.setStart(request.getStart());
+ this.setDescendingOrder(request.getDescendingOrder());
this.setMatchSubstring(request.getSubstring());
this.setMatchRegex(request.getRegex());
+ this.setSortBy(request.getSortBy());
return this;
}
@@ -136,7 +162,10 @@ public class ListTags implements RestReadView<ProjectResource> {
}
}
- tags.sort(comparing(t -> t.ref));
+ tagSorter.sort(sortBy, tags, descendingOrder);
+ if (descendingOrder) {
+ Collections.reverse(tags);
+ }
return Response.ok(
new RefFilter<>(Constants.R_TAGS, (TagInfo tag) -> tag.ref)
diff --git a/java/com/google/gerrit/server/restapi/project/PostLabels.java b/java/com/google/gerrit/server/restapi/project/PostLabels.java
index 6cb912ebb7..3616f4b8f3 100644
--- a/java/com/google/gerrit/server/restapi/project/PostLabels.java
+++ b/java/com/google/gerrit/server/restapi/project/PostLabels.java
@@ -112,7 +112,8 @@ public class PostLabels
if (labelInput.commitMessage != null) {
throw new BadRequestException("commit message on label definition input not supported");
}
- createLabel.createLabel(config, labelInput.name.trim(), labelInput);
+ @SuppressWarnings("unused")
+ var unused = createLabel.createLabel(config, labelInput.name.trim(), labelInput);
}
dirty = true;
}
@@ -126,9 +127,11 @@ public class PostLabels
if (e.getValue().commitMessage != null) {
throw new BadRequestException("commit message on label definition input not supported");
}
- setLabel.updateLabel(config, labelType, e.getValue());
+
+ if (setLabel.updateLabel(config, labelType, e.getValue())) {
+ dirty = true;
+ }
}
- dirty = true;
}
if (input.commitMessage != null) {
diff --git a/java/com/google/gerrit/server/restapi/project/PutConfig.java b/java/com/google/gerrit/server/restapi/project/PutConfig.java
index d4b30c2219..d5f61ceac6 100644
--- a/java/com/google/gerrit/server/restapi/project/PutConfig.java
+++ b/java/com/google/gerrit/server/restapi/project/PutConfig.java
@@ -221,7 +221,16 @@ public class PutConfig implements RestModifyView<ProjectResource, ConfigInput> {
oldValue = Joiner.on("\n").join(l);
value = Joiner.on("\n").join(v.getValue().values);
}
- if (Strings.emptyToNull(value) != null) {
+
+ String defaultValue = projectConfigEntry.getDefaultValue();
+ if (defaultValue != null && defaultValue.equals(value)) {
+ // If the value is equal to the default, unset in case it existed.
+ if (oldValue != null) {
+ validateProjectConfigEntryIsEditable(
+ projectConfigEntry, projectState, v.getKey(), pluginName);
+ projectConfig.updatePluginConfig(pluginName, cfg -> cfg.unset(v.getKey()));
+ }
+ } else if (Strings.emptyToNull(value) != null) {
if (!value.equals(oldValue)) {
validateProjectConfigEntryIsEditable(
projectConfigEntry, projectState, v.getKey(), pluginName);
diff --git a/java/com/google/gerrit/server/restapi/project/QueryProjects.java b/java/com/google/gerrit/server/restapi/project/QueryProjects.java
index dc7499db01..2ead807a82 100644
--- a/java/com/google/gerrit/server/restapi/project/QueryProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/QueryProjects.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.restapi.project;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.common.ProjectInfo;
@@ -115,15 +116,13 @@ public class QueryProjects implements RestReadView<TopLevelResource> {
queryProcessor.setStart(start);
}
- if (limit != 0) {
- queryProcessor.setUserProvidedLimit(limit);
- }
+ queryProcessor.setUserProvidedLimit(limit, /* applyDefaultLimit */ true);
try {
QueryResult<ProjectData> result =
queryProcessor.query(
!Strings.isNullOrEmpty(query) ? queryBuilder.parse(query) : Predicate.any());
- List<ProjectData> pds = result.entities();
+ ImmutableList<ProjectData> pds = result.entities();
ArrayList<ProjectInfo> projectInfos = Lists.newArrayListWithCapacity(pds.size());
for (ProjectData pd : pds) {
diff --git a/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java b/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
new file mode 100644
index 0000000000..c45a00973f
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
@@ -0,0 +1,215 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.project;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.InvalidNameException;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.CreateGroupPermissionSyncer;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.Sequences;
+import com.google.gerrit.server.approval.ApprovalsUtil;
+import com.google.gerrit.server.change.ChangeInserter;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.git.meta.MetaDataUpdate.User;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
+import com.google.gerrit.server.util.time.TimeUtil;
+import java.io.IOException;
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/** Updates repo refs/meta/config content. */
+@Singleton
+public class RepoMetaDataUpdater {
+ private final CreateGroupPermissionSyncer createGroupPermissionSyncer;
+ private final Provider<User> metaDataUpdateFactory;
+ private final ProjectConfig.Factory projectConfigFactory;
+ private final ProjectCache projectCache;
+ private final ChangeInserter.Factory changeInserterFactory;
+ private final Sequences seq;
+
+ private final BatchUpdate.Factory updateFactory;
+
+ private final PermissionBackend permissionBackend;
+
+ @Inject
+ RepoMetaDataUpdater(
+ CreateGroupPermissionSyncer createGroupPermissionSyncer,
+ Provider<User> metaDataUpdateFactory,
+ ProjectConfig.Factory projectConfigFactory,
+ ProjectCache projectCache,
+ ChangeInserter.Factory changeInserterFactory,
+ Sequences seq,
+ BatchUpdate.Factory updateFactory,
+ PermissionBackend permissionBackend) {
+ this.createGroupPermissionSyncer = createGroupPermissionSyncer;
+ this.metaDataUpdateFactory = metaDataUpdateFactory;
+ this.projectConfigFactory = projectConfigFactory;
+ this.projectCache = projectCache;
+ this.changeInserterFactory = changeInserterFactory;
+ this.seq = seq;
+ this.updateFactory = updateFactory;
+ this.permissionBackend = permissionBackend;
+ }
+
+ public Change updateAndCreateChangeForReview(
+ Project.NameKey projectName,
+ CurrentUser user,
+ String message,
+ ProjectConfigUpdater projectConfigUpdater)
+ throws ConfigInvalidException, IOException, RestApiException, UpdateException,
+ InvalidNameException, PermissionBackendException {
+ checkArgument(!message.isBlank(), "The message must not be empty");
+ message = validateMessage(message);
+
+ PermissionBackend.ForProject forProject = permissionBackend.user(user).project(projectName);
+ if (!check(forProject, ProjectPermission.READ_CONFIG)) {
+ throw new AuthException(RefNames.REFS_CONFIG + " not visible");
+ }
+ if (!check(forProject, ProjectPermission.WRITE_CONFIG)) {
+ try {
+ forProject.ref(RefNames.REFS_CONFIG).check(RefPermission.CREATE_CHANGE);
+ } catch (AuthException denied) {
+ throw new AuthException("cannot create change for " + RefNames.REFS_CONFIG, denied);
+ }
+ }
+ projectCache.get(projectName).orElseThrow(illegalState(projectName)).checkStatePermitsWrite();
+
+ try (MetaDataUpdate md = metaDataUpdateFactory.get().create(projectName)) {
+ ProjectConfig config = projectConfigFactory.read(md);
+ ObjectId oldCommit = config.getRevision();
+ String oldCommitSha1 = oldCommit == null ? null : oldCommit.getName();
+
+ projectConfigUpdater.update(config);
+ md.setMessage(message);
+ md.setInsertChangeId(true);
+
+ Change.Id changeId = Change.id(seq.nextChangeId());
+ try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+ RevCommit commit =
+ config.commitToNewRef(
+ md, PatchSet.id(changeId, Change.INITIAL_PATCH_SET_ID).toRefName());
+
+ if (commit.name().equals(oldCommitSha1)) {
+ throw new BadRequestException("no change");
+ }
+
+ try (ObjectInserter objInserter = md.getRepository().newObjectInserter();
+ ObjectReader objReader = objInserter.newReader();
+ RevWalk rw = new RevWalk(objReader);
+ BatchUpdate bu = updateFactory.create(projectName, user, TimeUtil.now())) {
+ bu.setRepository(md.getRepository(), rw, objInserter);
+ ChangeInserter ins = newInserter(changeId, commit);
+ bu.insertChange(ins);
+ bu.execute();
+ return ins.getChange();
+ }
+ }
+ }
+ }
+
+ public void updateWithoutReview(
+ Project.NameKey projectName, String message, ProjectConfigUpdater projectConfigUpdater)
+ throws ConfigInvalidException, IOException, PermissionBackendException, AuthException,
+ ResourceConflictException, InvalidNameException, BadRequestException {
+ updateWithoutReview(
+ projectName, message, /*skipPermissionsCheck=*/ false, projectConfigUpdater);
+ }
+
+ public void updateWithoutReview(
+ Project.NameKey projectName,
+ String message,
+ boolean skipPermissionsCheck,
+ ProjectConfigUpdater projectConfigUpdater)
+ throws ConfigInvalidException, IOException, PermissionBackendException, AuthException,
+ ResourceConflictException, InvalidNameException, BadRequestException {
+ message = validateMessage(message);
+ if (!skipPermissionsCheck) {
+ permissionBackend.currentUser().project(projectName).check(ProjectPermission.WRITE_CONFIG);
+ }
+
+ try (MetaDataUpdate md = metaDataUpdateFactory.get().create(projectName)) {
+ ProjectConfig config = projectConfigFactory.read(md);
+
+ projectConfigUpdater.update(config);
+ md.setMessage(message);
+ config.commit(md);
+ projectCache.evictAndReindex(config.getProject());
+ createGroupPermissionSyncer.syncIfNeeded();
+ }
+ }
+
+ private String validateMessage(String message) {
+ if (!message.endsWith("\n")) {
+ return message + "\n";
+ }
+ return message;
+ }
+
+ // ProjectConfig doesn't currently support fusing into a BatchUpdate.
+ @SuppressWarnings("deprecation")
+ private ChangeInserter newInserter(Change.Id changeId, RevCommit commit) {
+ return changeInserterFactory
+ .create(changeId, commit, RefNames.REFS_CONFIG)
+ .setMessage(
+ // Same message as in ReceiveCommits.CreateRequest.
+ ApprovalsUtil.renderMessageWithApprovals(1, ImmutableMap.of(), ImmutableMap.of()))
+ .setValidate(false)
+ .setUpdateRef(false);
+ }
+
+ private boolean check(PermissionBackend.ForProject perm, ProjectPermission p)
+ throws PermissionBackendException {
+ try {
+ perm.check(p);
+ return true;
+ } catch (AuthException denied) {
+ return false;
+ }
+ }
+
+ @FunctionalInterface
+ public interface ProjectConfigUpdater {
+ void update(ProjectConfig config)
+ throws BadRequestException, InvalidNameException, PermissionBackendException,
+ ResourceConflictException, AuthException;
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccess.java b/java/com/google/gerrit/server/restapi/project/SetAccess.java
index 6957275ec3..75fe28093f 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccess.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.restapi.project;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.gerrit.entities.AccessSection;
import com.google.gerrit.entities.Project;
@@ -28,20 +29,15 @@ import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.CreateGroupPermissionSyncer;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.RefPermission;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
-import java.util.List;
import java.util.Map;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -49,92 +45,72 @@ import org.eclipse.jgit.errors.ConfigInvalidException;
public class SetAccess implements RestModifyView<ProjectResource, ProjectAccessInput> {
protected final GroupBackend groupBackend;
private final PermissionBackend permissionBackend;
- private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
private final GetAccess getAccess;
- private final ProjectCache projectCache;
private final Provider<IdentifiedUser> identifiedUser;
private final SetAccessUtil accessUtil;
- private final CreateGroupPermissionSyncer createGroupPermissionSyncer;
- private final ProjectConfig.Factory projectConfigFactory;
+ private final RepoMetaDataUpdater repoMetaDataUpdater;
@Inject
private SetAccess(
GroupBackend groupBackend,
PermissionBackend permissionBackend,
- Provider<MetaDataUpdate.User> metaDataUpdateFactory,
- ProjectCache projectCache,
GetAccess getAccess,
Provider<IdentifiedUser> identifiedUser,
SetAccessUtil accessUtil,
- CreateGroupPermissionSyncer createGroupPermissionSyncer,
- ProjectConfig.Factory projectConfigFactory) {
+ RepoMetaDataUpdater repoMetaDataUpdater) {
this.groupBackend = groupBackend;
this.permissionBackend = permissionBackend;
- this.metaDataUpdateFactory = metaDataUpdateFactory;
this.getAccess = getAccess;
- this.projectCache = projectCache;
this.identifiedUser = identifiedUser;
this.accessUtil = accessUtil;
- this.createGroupPermissionSyncer = createGroupPermissionSyncer;
- this.projectConfigFactory = projectConfigFactory;
+ this.repoMetaDataUpdater = repoMetaDataUpdater;
}
@Override
public Response<ProjectAccessInfo> apply(ProjectResource rsrc, ProjectAccessInput input)
throws Exception {
- MetaDataUpdate.User metaDataUpdateUser = metaDataUpdateFactory.get();
-
validateInput(input);
- ProjectConfig config;
-
- List<AccessSection> removals =
+ ImmutableList<AccessSection> removals =
accessUtil.getAccessSections(input.remove, /* rejectNonResolvableGroups= */ false);
- List<AccessSection> additions =
+ ImmutableList<AccessSection> additions =
accessUtil.getAccessSections(input.add, /* rejectNonResolvableGroups= */ true);
- try (MetaDataUpdate md = metaDataUpdateUser.create(rsrc.getNameKey())) {
- config = projectConfigFactory.read(md);
-
- // Check that the user has the right permissions.
- boolean checkedAdmin = false;
- for (AccessSection section : Iterables.concat(additions, removals)) {
- boolean isGlobalCapabilities = AccessSection.GLOBAL_CAPABILITIES.equals(section.getName());
- if (isGlobalCapabilities) {
- if (!checkedAdmin) {
- permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
- checkedAdmin = true;
- }
- } else {
- permissionBackend
- .currentUser()
- .project(rsrc.getNameKey())
- .ref(section.getName())
- .check(RefPermission.WRITE_CONFIG);
- }
- }
-
- accessUtil.validateChanges(config, removals, additions);
- accessUtil.applyChanges(config, removals, additions);
-
- accessUtil.setParentName(
- identifiedUser.get(),
- config,
+ String message = !Strings.isNullOrEmpty(input.message) ? input.message : "Modify access rules";
+ try {
+ this.repoMetaDataUpdater.updateWithoutReview(
rsrc.getNameKey(),
- input.parent == null ? null : Project.nameKey(input.parent),
- !checkedAdmin);
-
- if (!Strings.isNullOrEmpty(input.message)) {
- if (!input.message.endsWith("\n")) {
- input.message += "\n";
- }
- md.setMessage(input.message);
- } else {
- md.setMessage("Modify access rules\n");
- }
-
- config.commit(md);
- projectCache.evictAndReindex(config.getProject());
- createGroupPermissionSyncer.syncIfNeeded();
+ message,
+ /*skipPermissionsCheck=*/ true,
+ config -> {
+ // Check that the user has the right permissions.
+ boolean checkedAdmin = false;
+ for (AccessSection section : Iterables.concat(additions, removals)) {
+ boolean isGlobalCapabilities =
+ AccessSection.GLOBAL_CAPABILITIES.equals(section.getName());
+ if (isGlobalCapabilities) {
+ if (!checkedAdmin) {
+ permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
+ checkedAdmin = true;
+ }
+ } else {
+ permissionBackend
+ .currentUser()
+ .project(rsrc.getNameKey())
+ .ref(section.getName())
+ .check(RefPermission.WRITE_CONFIG);
+ }
+ }
+
+ accessUtil.validateChanges(config, removals, additions);
+ accessUtil.applyChanges(config, removals, additions);
+
+ accessUtil.setParentName(
+ identifiedUser.get(),
+ config,
+ rsrc.getNameKey(),
+ input.parent == null ? null : Project.nameKey(input.parent),
+ !checkedAdmin);
+ });
} catch (InvalidNameException e) {
throw new BadRequestException(e.toString());
} catch (ConfigInvalidException e) {
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
index 547a2148ff..b685f0887b 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.restapi.project;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.entities.AccessSection;
@@ -45,7 +46,6 @@ import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.util.List;
import java.util.Map;
-import java.util.Set;
@Singleton
public class SetAccessUtil {
@@ -266,7 +266,7 @@ public class SetAccessUtil {
}
return true;
}
- Set<String> pluginPermissions =
+ ImmutableSet<String> pluginPermissions =
pluginPermissionsUtil.collectPluginProjectPermissions().keySet();
return pluginPermissions.contains(name);
}
@@ -275,7 +275,8 @@ public class SetAccessUtil {
if (GlobalCapability.isGlobalCapability(name)) {
return true;
}
- Set<String> pluginCapabilities = pluginPermissionsUtil.collectPluginCapabilities().keySet();
+ ImmutableSet<String> pluginCapabilities =
+ pluginPermissionsUtil.collectPluginCapabilities().keySet();
return pluginCapabilities.contains(name);
}
}
diff --git a/java/com/google/gerrit/server/restapi/project/SetLabel.java b/java/com/google/gerrit/server/restapi/project/SetLabel.java
index 6c4976d76f..edd165dcc0 100644
--- a/java/com/google/gerrit/server/restapi/project/SetLabel.java
+++ b/java/com/google/gerrit/server/restapi/project/SetLabel.java
@@ -190,7 +190,8 @@ public class SetLabel implements RestModifyView<LabelResource, LabelDefinitionIn
input.copyCondition = Strings.emptyToNull(input.copyCondition);
if (input.copyCondition != null) {
try {
- approvalQueryBuilder.parse(input.copyCondition);
+ @SuppressWarnings("unused")
+ var unused = approvalQueryBuilder.parse(input.copyCondition);
} catch (QueryParseException e) {
throw new BadRequestException(
"unable to parse copy condition. got: " + input.copyCondition + ". " + e.getMessage(),
diff --git a/java/com/google/gerrit/server/restapi/project/TagSorter.java b/java/com/google/gerrit/server/restapi/project/TagSorter.java
new file mode 100644
index 0000000000..4776ce102b
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/TagSorter.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.project;
+
+import static java.util.Comparator.comparing;
+
+import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.common.ListTagSortOption;
+import com.google.inject.Inject;
+import java.sql.Timestamp;
+import java.util.Comparator;
+import java.util.List;
+
+public class TagSorter {
+ @Inject
+ public TagSorter() {}
+
+ /** Sort the tags by the given sort option, in place */
+ public void sort(ListTagSortOption sortBy, List<TagInfo> tags, boolean descendingOrder) {
+ switch (sortBy) {
+ case CREATION_TIME:
+ Comparator<Timestamp> nullsComparator =
+ descendingOrder
+ ? Comparator.nullsFirst(Comparator.naturalOrder())
+ : Comparator.nullsLast(Comparator.naturalOrder());
+ tags.sort(comparing(t -> t.created, nullsComparator));
+ break;
+ case REF:
+ default:
+ tags.sort(comparing(t -> t.ref));
+ break;
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java b/java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java
index bbd617c8b8..3e1104efb6 100644
--- a/java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java
+++ b/java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.restapi.project;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.extensions.common.SubmitRequirementInfo;
@@ -38,7 +39,6 @@ import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.List;
import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -131,7 +131,7 @@ public class UpdateSubmitRequirement
.setAllowOverrideInChildProjects(input.allowOverrideInChildProjects)
.build();
- List<String> validationMessages =
+ ImmutableList<String> validationMessages =
submitRequirementExpressionsValidator.validateExpressions(submitRequirement);
if (!validationMessages.isEmpty()) {
throw new BadRequestException(
diff --git a/java/com/google/gerrit/server/restapi/project/package-info.java b/java/com/google/gerrit/server/restapi/project/package-info.java
new file mode 100644
index 0000000000..7def13dc3a
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.restapi.project;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/rules/DefaultSubmitRule.java b/java/com/google/gerrit/server/rules/DefaultSubmitRule.java
index 8cd0a58af0..1efb8a623e 100644
--- a/java/com/google/gerrit/server/rules/DefaultSubmitRule.java
+++ b/java/com/google/gerrit/server/rules/DefaultSubmitRule.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.rules;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.PatchSetApproval;
@@ -26,7 +27,6 @@ import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Singleton;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.List;
import java.util.Optional;
@@ -67,7 +67,7 @@ public final class DefaultSubmitRule implements SubmitRule {
t.getName(),
cd.getId());
- Collection<PatchSetApproval> approvalsForLabel = getApprovalsForLabel(approvals, t);
+ ImmutableList<PatchSetApproval> approvalsForLabel = getApprovalsForLabel(approvals, t);
SubmitRecord.Label label = labelFunction.check(t, approvalsForLabel);
submitRecord.labels.add(label);
@@ -87,7 +87,7 @@ public final class DefaultSubmitRule implements SubmitRule {
return Optional.of(submitRecord);
}
- private static List<PatchSetApproval> getApprovalsForLabel(
+ private static ImmutableList<PatchSetApproval> getApprovalsForLabel(
List<PatchSetApproval> approvals, LabelType t) {
return approvals.stream()
.filter(input -> input.label().equals(t.getLabelId().get()))
diff --git a/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java b/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
index 216405d99a..c915e6e2e8 100644
--- a/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
+++ b/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.rules;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.LabelType;
@@ -75,7 +76,7 @@ public class IgnoreSelfApprovalRule implements SubmitRule {
continue;
}
- Collection<PatchSetApproval> allApprovalsForLabel = filterApprovalsByLabel(approvals, t);
+ ImmutableList<PatchSetApproval> allApprovalsForLabel = filterApprovalsByLabel(approvals, t);
SubmitRecord.Label allApprovalsCheckResult = labelFunction.check(t, allApprovalsForLabel);
SubmitRecord.Label ignoreSelfApprovalCheckResult =
labelFunction.check(t, filterOutPositiveApprovalsOfUser(allApprovalsForLabel, uploader));
@@ -119,7 +120,7 @@ public class IgnoreSelfApprovalRule implements SubmitRule {
}
@VisibleForTesting
- static Collection<PatchSetApproval> filterOutPositiveApprovalsOfUser(
+ static ImmutableList<PatchSetApproval> filterOutPositiveApprovalsOfUser(
Collection<PatchSetApproval> approvals, Account.Id user) {
return approvals.stream()
.filter(input -> input.value() < 0 || !input.accountId().equals(user))
@@ -127,7 +128,7 @@ public class IgnoreSelfApprovalRule implements SubmitRule {
}
@VisibleForTesting
- static Collection<PatchSetApproval> filterApprovalsByLabel(
+ static ImmutableList<PatchSetApproval> filterApprovalsByLabel(
Collection<PatchSetApproval> approvals, LabelType t) {
return approvals.stream()
.filter(input -> input.labelId().get().equals(t.getLabelId().get()))
diff --git a/java/com/google/gerrit/server/rules/PrologSubmitRuleUtil.java b/java/com/google/gerrit/server/rules/PrologSubmitRuleUtil.java
index a94fb6e979..a568371ac3 100644
--- a/java/com/google/gerrit/server/rules/PrologSubmitRuleUtil.java
+++ b/java/com/google/gerrit/server/rules/PrologSubmitRuleUtil.java
@@ -20,22 +20,29 @@ import com.google.gerrit.server.query.change.ChangeData;
/** Provides prolog-related operations to different callers. */
public interface PrologSubmitRuleUtil {
+ /** Returns true if prolog rules are enabled for the project. */
+ boolean isProjectRulesEnabled();
/**
* Returns the submit-type of a change depending on the change data and the definition of the
* prolog rules file.
+ *
+ * <p>Must only be called when Prolog rules are enabled on the Gerrit server.
*/
SubmitTypeRecord getSubmitType(ChangeData cd);
/**
* Returns the submit-type of a change depending on the change data and the definition of the
* prolog rules file.
+ *
+ * <p>Must only be called when Prolog rules are enabled on the Gerrit server.
*/
SubmitTypeRecord getSubmitType(ChangeData cd, String ruleToTest, boolean skipFilters);
- /** Evaluates a submit rule. */
+ /**
+ * Evaluates a submit rule.
+ *
+ * <p>Must only be called when Prolog rules are enabled on the Gerrit server.
+ */
SubmitRecord evaluate(ChangeData cd, String ruleToTest, boolean skipFilters);
-
- /** Returns true if prolog rules are enabled for the project. */
- boolean isProjectRulesEnabled();
}
diff --git a/java/com/google/gerrit/server/rules/package-info.java b/java/com/google/gerrit/server/rules/package-info.java
new file mode 100644
index 0000000000..bee88f94bf
--- /dev/null
+++ b/java/com/google/gerrit/server/rules/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.rules;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/rules/prolog/BUILD b/java/com/google/gerrit/server/rules/prolog/BUILD
index 5e38d06542..620a0235dc 100644
--- a/java/com/google/gerrit/server/rules/prolog/BUILD
+++ b/java/com/google/gerrit/server/rules/prolog/BUILD
@@ -15,6 +15,7 @@ java_library(
"//lib:guava",
"//lib:jgit",
"//lib/auto:auto-value-annotations",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
"//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/server/rules/prolog/PredicateClassLoader.java b/java/com/google/gerrit/server/rules/prolog/PredicateClassLoader.java
index 67b8a6064d..28ec78fd84 100644
--- a/java/com/google/gerrit/server/rules/prolog/PredicateClassLoader.java
+++ b/java/com/google/gerrit/server/rules/prolog/PredicateClassLoader.java
@@ -17,7 +17,7 @@ package com.google.gerrit.server.rules.prolog;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.SetMultimap;
import com.google.gerrit.server.plugincontext.PluginSetContext;
-import java.util.Collection;
+import java.util.Set;
/** Loads the classes for Prolog predicates. */
class PredicateClassLoader extends ClassLoader {
@@ -38,8 +38,7 @@ class PredicateClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
- final Collection<ClassLoader> classLoaders =
- packageClassLoaderMap.get(getPackageName(className));
+ final Set<ClassLoader> classLoaders = packageClassLoaderMap.get(getPackageName(className));
for (ClassLoader cl : classLoaders) {
try {
return Class.forName(className, true, cl);
diff --git a/java/com/google/gerrit/server/rules/prolog/PrologRule.java b/java/com/google/gerrit/server/rules/prolog/PrologRule.java
index 13814bb7a2..c560fd2a1b 100644
--- a/java/com/google/gerrit/server/rules/prolog/PrologRule.java
+++ b/java/com/google/gerrit/server/rules/prolog/PrologRule.java
@@ -30,15 +30,22 @@ import java.util.Optional;
class PrologRule implements SubmitRule {
private final PrologRuleEvaluator.Factory factory;
private final ProjectCache projectCache;
+ private final boolean isProjectRulesEnabled;
@Inject
- private PrologRule(PrologRuleEvaluator.Factory factory, ProjectCache projectCache) {
+ private PrologRule(
+ PrologRuleEvaluator.Factory factory, ProjectCache projectCache, RulesCache rulesCache) {
this.factory = factory;
this.projectCache = projectCache;
+ this.isProjectRulesEnabled = rulesCache.isProjectRulesEnabled();
}
@Override
public Optional<SubmitRecord> evaluate(ChangeData cd) {
+ if (!isProjectRulesEnabled) {
+ return Optional.empty();
+ }
+
ProjectState projectState =
projectCache.get(cd.project()).orElseThrow(illegalState(cd.project()));
// We only want to run the Prolog engine if we have at least one rules.pl file to use.
@@ -49,15 +56,11 @@ class PrologRule implements SubmitRule {
return Optional.of(evaluate(cd, PrologOptions.defaultOptions()));
}
- public SubmitRecord evaluate(ChangeData cd, PrologOptions opts) {
+ SubmitRecord evaluate(ChangeData cd, PrologOptions opts) {
return getEvaluator(cd, opts).evaluate();
}
- public SubmitTypeRecord getSubmitType(ChangeData cd) {
- return getSubmitType(cd, PrologOptions.defaultOptions());
- }
-
- public SubmitTypeRecord getSubmitType(ChangeData cd, PrologOptions opts) {
+ SubmitTypeRecord getSubmitType(ChangeData cd, PrologOptions opts) {
return getEvaluator(cd, opts).getSubmitType();
}
diff --git a/java/com/google/gerrit/server/rules/prolog/PrologSubmitRuleUtilImpl.java b/java/com/google/gerrit/server/rules/prolog/PrologSubmitRuleUtilImpl.java
index 3d017e2cf1..6be71f899d 100644
--- a/java/com/google/gerrit/server/rules/prolog/PrologSubmitRuleUtilImpl.java
+++ b/java/com/google/gerrit/server/rules/prolog/PrologSubmitRuleUtilImpl.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.rules.prolog;
+import static com.google.common.base.Preconditions.checkState;
+
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.entities.SubmitTypeRecord;
import com.google.gerrit.server.query.change.ChangeData;
@@ -25,7 +27,6 @@ import javax.inject.Inject;
@Singleton
public class PrologSubmitRuleUtilImpl implements PrologSubmitRuleUtil {
private final PrologRule prologRule;
-
private final RulesCache rulesCache;
@Inject
@@ -35,22 +36,25 @@ public class PrologSubmitRuleUtilImpl implements PrologSubmitRuleUtil {
}
@Override
+ public boolean isProjectRulesEnabled() {
+ return rulesCache.isProjectRulesEnabled();
+ }
+
+ @Override
public SubmitTypeRecord getSubmitType(ChangeData cd) {
- return prologRule.getSubmitType(cd);
+ checkState(isProjectRulesEnabled(), "prolog rules disabled");
+ return prologRule.getSubmitType(cd, PrologOptions.defaultOptions());
}
@Override
public SubmitTypeRecord getSubmitType(ChangeData cd, String ruleToTest, boolean skipFilters) {
+ checkState(isProjectRulesEnabled(), "prolog rules disabled");
return prologRule.getSubmitType(cd, PrologOptions.dryRunOptions(ruleToTest, skipFilters));
}
@Override
public SubmitRecord evaluate(ChangeData cd, String ruleToTest, boolean skipFilters) {
+ checkState(isProjectRulesEnabled(), "prolog rules disabled");
return prologRule.evaluate(cd, PrologOptions.dryRunOptions(ruleToTest, skipFilters));
}
-
- @Override
- public boolean isProjectRulesEnabled() {
- return rulesCache.isProjectRulesEnabled();
- }
}
diff --git a/java/com/google/gerrit/server/rules/prolog/package-info.java b/java/com/google/gerrit/server/rules/prolog/package-info.java
new file mode 100644
index 0000000000..08595d3ab4
--- /dev/null
+++ b/java/com/google/gerrit/server/rules/prolog/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.rules.prolog;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/schema/AllProjectsInput.java b/java/com/google/gerrit/server/schema/AllProjectsInput.java
index cfb9754fb2..8db5b1ae70 100644
--- a/java/com/google/gerrit/server/schema/AllProjectsInput.java
+++ b/java/com/google/gerrit/server/schema/AllProjectsInput.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.schema;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.GroupReference;
@@ -124,6 +125,7 @@ public abstract class AllProjectsInput {
public abstract ImmutableMap.Builder<BooleanProjectConfig, InheritableBoolean>
booleanProjectConfigsBuilder();
+ @CanIgnoreReturnValue
public Builder addBooleanProjectConfig(
BooleanProjectConfig booleanProjectConfig, InheritableBoolean inheritableBoolean) {
booleanProjectConfigsBuilder().put(booleanProjectConfig, inheritableBoolean);
diff --git a/java/com/google/gerrit/server/schema/BUILD b/java/com/google/gerrit/server/schema/BUILD
index ce445e1579..b344e6d932 100644
--- a/java/com/google/gerrit/server/schema/BUILD
+++ b/java/com/google/gerrit/server/schema/BUILD
@@ -24,6 +24,7 @@ java_library(
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
"//lib/commons:dbcp",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
],
diff --git a/java/com/google/gerrit/server/schema/MigrateLabelConfigToCopyCondition.java b/java/com/google/gerrit/server/schema/MigrateLabelConfigToCopyCondition.java
index 46a6857fd0..927d3fd156 100644
--- a/java/com/google/gerrit/server/schema/MigrateLabelConfigToCopyCondition.java
+++ b/java/com/google/gerrit/server/schema/MigrateLabelConfigToCopyCondition.java
@@ -29,8 +29,8 @@ import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.git.meta.VersionedConfigFile;
import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.ProjectLevelConfig;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
@@ -127,8 +127,7 @@ public class MigrateLabelConfigToCopyCondition {
* parsed
*/
public void execute(Project.NameKey projectName) throws IOException, ConfigInvalidException {
- ProjectLevelConfig.Bare projectConfig =
- new ProjectLevelConfig.Bare(ProjectConfig.PROJECT_CONFIG);
+ VersionedConfigFile projectConfig = new VersionedConfigFile(ProjectConfig.PROJECT_CONFIG);
try (Repository repo = repoManager.openRepository(projectName);
MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, projectName, repo)) {
boolean isAlreadyMigrated = hasMigrationAlreadyRun(repo);
diff --git a/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java b/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
index 2ca79342ed..d8da13d25a 100644
--- a/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
+++ b/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
@@ -28,8 +28,8 @@ import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.git.meta.VersionedConfigFile;
import com.google.gerrit.server.project.ProjectConfig;
-import com.google.gerrit.server.project.ProjectLevelConfig;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
@@ -130,8 +130,7 @@ public class MigrateLabelFunctionsToSubmitRequirement {
ui.message(String.format("Skipping project %s because it has prolog rules", project));
return Status.HAS_PROLOG;
}
- ProjectLevelConfig.Bare projectConfig =
- new ProjectLevelConfig.Bare(ProjectConfig.PROJECT_CONFIG);
+ VersionedConfigFile projectConfig = new VersionedConfigFile(ProjectConfig.PROJECT_CONFIG);
boolean migrationPerformed = false;
try (Repository repo = repoManager.openRepository(project);
MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, project, repo)) {
@@ -275,7 +274,7 @@ public class MigrateLabelFunctionsToSubmitRequirement {
cfg.setString(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_FUNCTION, function);
}
- private void commit(ProjectLevelConfig.Bare projectConfig, MetaDataUpdate md) throws IOException {
+ private void commit(VersionedConfigFile projectConfig, MetaDataUpdate md) throws IOException {
md.getCommitBuilder().setAuthor(serverUser);
md.getCommitBuilder().setCommitter(serverUser);
md.setMessage(COMMIT_MSG);
diff --git a/java/com/google/gerrit/server/schema/package-info.java b/java/com/google/gerrit/server/schema/package-info.java
new file mode 100644
index 0000000000..2b2ff5f2cb
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.schema;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/schema/testing/BUILD b/java/com/google/gerrit/server/schema/testing/BUILD
index 77bb777f0f..8f2c2ffbed 100644
--- a/java/com/google/gerrit/server/schema/testing/BUILD
+++ b/java/com/google/gerrit/server/schema/testing/BUILD
@@ -11,6 +11,7 @@ java_library(
"//java/com/google/gerrit/server",
"//lib:guava",
"//lib:jgit",
+ "//lib/errorprone:annotations",
"//lib/truth",
],
)
diff --git a/java/com/google/gerrit/server/schema/testing/package-info.java b/java/com/google/gerrit/server/schema/testing/package-info.java
new file mode 100644
index 0000000000..d2c2885988
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.schema.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/securestore/package-info.java b/java/com/google/gerrit/server/securestore/package-info.java
new file mode 100644
index 0000000000..c281f663b8
--- /dev/null
+++ b/java/com/google/gerrit/server/securestore/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.securestore;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/securestore/testing/BUILD b/java/com/google/gerrit/server/securestore/testing/BUILD
index 9afc44acb2..814736e6ef 100644
--- a/java/com/google/gerrit/server/securestore/testing/BUILD
+++ b/java/com/google/gerrit/server/securestore/testing/BUILD
@@ -9,5 +9,6 @@ java_library(
deps = [
"//java/com/google/gerrit/server",
"//lib:jgit",
+ "//lib/errorprone:annotations",
],
)
diff --git a/java/com/google/gerrit/server/securestore/testing/package-info.java b/java/com/google/gerrit/server/securestore/testing/package-info.java
new file mode 100644
index 0000000000..8a7b2d6887
--- /dev/null
+++ b/java/com/google/gerrit/server/securestore/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.securestore.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/ssh/package-info.java b/java/com/google/gerrit/server/ssh/package-info.java
new file mode 100644
index 0000000000..116396a5c3
--- /dev/null
+++ b/java/com/google/gerrit/server/ssh/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.ssh;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/submit/EmailMerge.java b/java/com/google/gerrit/server/submit/EmailMerge.java
index 47fef1ac8c..ce265529a4 100644
--- a/java/com/google/gerrit/server/submit/EmailMerge.java
+++ b/java/com/google/gerrit/server/submit/EmailMerge.java
@@ -114,7 +114,8 @@ class EmailMerge implements Runnable, RequestContext {
} catch (Exception e) {
logger.atSevere().withCause(e).log("Cannot email merged notification for %s", change.getId());
} finally {
- requestContext.setContext(old);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(old);
}
}
diff --git a/java/com/google/gerrit/server/submit/GitModules.java b/java/com/google/gerrit/server/submit/GitModules.java
index f8f6bc4e9a..601e9ee2a9 100644
--- a/java/com/google/gerrit/server/submit/GitModules.java
+++ b/java/com/google/gerrit/server/submit/GitModules.java
@@ -27,8 +27,8 @@ import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Collections;
+import java.util.List;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.BlobBasedConfig;
@@ -89,8 +89,8 @@ public class GitModules {
}
}
- Collection<SubmoduleSubscription> subscribedTo(BranchNameKey src) {
- Collection<SubmoduleSubscription> ret = new ArrayList<>();
+ List<SubmoduleSubscription> subscribedTo(BranchNameKey src) {
+ List<SubmoduleSubscription> ret = new ArrayList<>();
for (SubmoduleSubscription s : subscriptions) {
if (s.getSubmodule().equals(src)) {
ret.add(s);
diff --git a/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java b/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
index 86d6c6746a..2e66941fdb 100644
--- a/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
+++ b/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
@@ -106,8 +106,8 @@ public class LocalMergeSuperSetComputation implements MergeSuperSetComputation {
@Override
public ChangeSet completeWithoutTopic(
MergeOpRepoManager orm, ChangeSet changeSet, CurrentUser user) throws IOException {
- Collection<ChangeData> visibleChanges = new ArrayList<>();
- Collection<ChangeData> nonVisibleChanges = new ArrayList<>();
+ List<ChangeData> visibleChanges = new ArrayList<>();
+ List<ChangeData> nonVisibleChanges = new ArrayList<>();
// For each target branch we run a separate rev walk to find open changes
// reachable from changes already in the merge super set.
@@ -194,7 +194,7 @@ public class LocalMergeSuperSetComputation implements MergeSuperSetComputation {
Set<String> nonVisibleHashes,
CurrentUser user)
throws IOException {
- List<ChangeData> potentiallyVisibleChanges =
+ ImmutableList<ChangeData> potentiallyVisibleChanges =
byCommitsOnBranchNotMerged(or, branch, visibleHashes);
List<ChangeData> invisibleChanges =
new ArrayList<>(byCommitsOnBranchNotMerged(or, branch, nonVisibleHashes));
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 2b8a66225d..eb41690bc6 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -16,16 +16,22 @@ package com.google.gerrit.server.submit;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.experiments.ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_ALWAYS_REJECT_IMPLICIT_MERGES_ON_MERGE;
+import static com.google.gerrit.server.experiments.ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_CHECK_IMPLICIT_MERGES_ON_MERGE;
+import static com.google.gerrit.server.experiments.ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_REJECT_IMPLICIT_MERGES_ON_MERGE;
+import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static com.google.gerrit.server.update.RetryableAction.ActionType.INDEX_QUERY;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.MERGE_CHANGE;
import static java.util.Comparator.comparing;
import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toSet;
import com.github.rholder.retry.Attempt;
import com.github.rholder.retry.RetryListener;
import com.google.auto.value.AutoValue;
import com.google.common.base.Joiner;
+import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -33,8 +39,11 @@ import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.SetMultimap;
+import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Change.Status;
@@ -63,6 +72,9 @@ import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.change.NotifyResolver;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.MergeTip;
import com.google.gerrit.server.git.validators.MergeValidationException;
@@ -73,6 +85,7 @@ import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.StoreSubmitRequirementsOp;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -80,6 +93,7 @@ import com.google.gerrit.server.submit.MergeOpRepoManager.OpenBranch;
import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.BatchUpdates;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.RetryHelper;
import com.google.gerrit.server.update.SubmissionExecutor;
@@ -93,23 +107,30 @@ import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.time.Instant;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
/**
* Merges changes in submission order into a single branch.
@@ -134,6 +155,8 @@ public class MergeOp implements AutoCloseable {
private final ImmutableSetMultimap<BranchNameKey, Change.Id> byBranch;
private final Map<Change.Id, CodeReviewCommit> commits;
private final ListMultimap<Change.Id, String> problems;
+ private final Set<SimpleImmutableEntry<Project.NameKey, BranchNameKey>> implicitMergeProblems;
+
private final boolean allowClosed;
private CommitStatus(ChangeSet cs, boolean allowClosed) {
@@ -147,6 +170,7 @@ public class MergeOp implements AutoCloseable {
byBranch = bb.build();
commits = new HashMap<>();
problems = MultimapBuilder.treeKeys(comparing(Change.Id::get)).arrayListValues(1).build();
+ implicitMergeProblems = new HashSet<>();
this.allowClosed = allowClosed;
}
@@ -181,8 +205,12 @@ public class MergeOp implements AutoCloseable {
problems.put(id, msg);
}
+ public void addImplicitMerge(Project.NameKey projectName, BranchNameKey branchName) {
+ implicitMergeProblems.add(new SimpleImmutableEntry<>(projectName, branchName));
+ }
+
public boolean isOk() {
- return problems.isEmpty();
+ return problems.isEmpty() && implicitMergeProblems.isEmpty();
}
public List<SubmitRecord> getSubmitRecords(Change.Id id) {
@@ -214,6 +242,21 @@ public class MergeOp implements AutoCloseable {
for (Change.Id id : problems.keySet()) {
ps.add("Change " + id + ": " + Joiner.on("; ").join(problems.get(id)));
}
+ if (ps.isEmpty()) {
+ // An implicit merge can be also detected when there are another problems with changes(e.g.
+ // the parent change is deleted). It can confuse the user if gerrit reports both the correct
+ // problem and implicit merge problem at the same time - so report implicit merge problem
+ // only if no other problems are reported.
+ for (SimpleImmutableEntry<Project.NameKey, BranchNameKey> projectBranch :
+ implicitMergeProblems) {
+ // TODO(dmfilippov): Make message more clear to the user and add the exact change id.
+ ps.add(
+ String.format(
+ "submit makes implicit merge to the branch %s of the project %s from some other "
+ + "branch",
+ projectBranch.getValue().shortName(), projectBranch.getKey().get()));
+ }
+ }
throw new ResourceConflictException(msg + Joiner.on('\n').join(ps));
}
@@ -234,6 +277,7 @@ public class MergeOp implements AutoCloseable {
private final ChangeMessagesUtil cmUtil;
private final BatchUpdate.Factory batchUpdateFactory;
+ private final BatchUpdates batchUpdates;
private final InternalUser.Factory internalUserFactory;
private final MergeSuperSet mergeSuperSet;
private final MergeValidators.Factory mergeValidatorsFactory;
@@ -252,6 +296,11 @@ public class MergeOp implements AutoCloseable {
// Changes that were updated by this MergeOp.
private final Map<Change.Id, Change> updatedChanges;
+ private final ExperimentFeatures experimentFeatures;
+
+ private final ProjectCache projectCache;
+ private final long hasImplicitMergeTimeoutSeconds;
+
private Instant ts;
private SubmissionId submissionId;
private IdentifiedUser caller;
@@ -260,7 +309,7 @@ public class MergeOp implements AutoCloseable {
private CommitStatus commitStatus;
private SubmitInput submitInput;
private NotifyResolver.Result notify;
- private Set<Project.NameKey> projects;
+ private ImmutableSet<Project.NameKey> projects;
private boolean dryrun;
private TopicMetrics topicMetrics;
@@ -268,6 +317,7 @@ public class MergeOp implements AutoCloseable {
MergeOp(
ChangeMessagesUtil cmUtil,
BatchUpdate.Factory batchUpdateFactory,
+ BatchUpdates batchUpdates,
InternalUser.Factory internalUserFactory,
MergeSuperSet mergeSuperSet,
MergeValidators.Factory mergeValidatorsFactory,
@@ -283,9 +333,13 @@ public class MergeOp implements AutoCloseable {
RetryHelper retryHelper,
ChangeData.Factory changeDataFactory,
StoreSubmitRequirementsOp.Factory storeSubmitRequirementsOpFactory,
- MergeMetrics mergeMetrics) {
+ MergeMetrics mergeMetrics,
+ ProjectCache projectCache,
+ ExperimentFeatures experimentFeatures,
+ @GerritServerConfig Config config) {
this.cmUtil = cmUtil;
this.batchUpdateFactory = batchUpdateFactory;
+ this.batchUpdates = batchUpdates;
this.internalUserFactory = internalUserFactory;
this.mergeSuperSet = mergeSuperSet;
this.mergeValidatorsFactory = mergeValidatorsFactory;
@@ -302,6 +356,12 @@ public class MergeOp implements AutoCloseable {
this.updatedChanges = new HashMap<>();
this.storeSubmitRequirementsOpFactory = storeSubmitRequirementsOpFactory;
this.mergeMetrics = mergeMetrics;
+ this.projectCache = projectCache;
+ this.experimentFeatures = experimentFeatures;
+ // Undocumented - experimental, can be removed.
+ hasImplicitMergeTimeoutSeconds =
+ ConfigUtil.getTimeUnit(
+ config, "change", null, "implicitMergeCalculationTimeout", 60, TimeUnit.SECONDS);
}
@Override
@@ -438,6 +498,7 @@ public class MergeOp implements AutoCloseable {
* @throws IOException an error occurred reading from NoteDb.
* @return the merged change
*/
+ @CanIgnoreReturnValue
public Change merge(
Change change,
IdentifiedUser caller,
@@ -500,37 +561,40 @@ public class MergeOp implements AutoCloseable {
}
SubmissionExecutor submissionExecutor =
- new SubmissionExecutor(dryrun, superprojectUpdateSubmissionListeners);
+ new SubmissionExecutor(batchUpdates, dryrun, superprojectUpdateSubmissionListeners);
RetryTracker retryTracker = new RetryTracker();
- retryHelper
- .changeUpdate(
- "integrateIntoHistory",
- updateFactory -> {
- long attempt = retryTracker.lastAttemptNumber + 1;
- boolean isRetry = attempt > 1;
- if (isRetry) {
- logger.atFine().log("Retrying, attempt #%d; skipping merged changes", attempt);
- this.ts = TimeUtil.now();
- openRepoManager();
- }
- this.commitStatus = new CommitStatus(filteredNoteDbChangeSet, isRetry);
- if (checkSubmitRules) {
- logger.atFine().log("Checking submit rules and state");
- checkSubmitRulesAndState(filteredNoteDbChangeSet, isRetry);
- } else {
- logger.atFine().log("Bypassing submit rules");
- bypassSubmitRulesAndRequirements(filteredNoteDbChangeSet);
- }
- integrateIntoHistory(
- filteredNoteDbChangeSet, submissionExecutor, checkSubmitRules);
- return null;
- })
- .listener(retryTracker)
- // Up to the entire submit operation is retried, including possibly many projects.
- // Multiply the timeout by the number of projects we're actually attempting to
- // submit. Times 2 to retry more persistently, to increase success rate.
- .defaultTimeoutMultiplier(filteredNoteDbChangeSet.projects().size() * 2)
- .call();
+ @SuppressWarnings("unused")
+ var unused =
+ retryHelper
+ .changeUpdate(
+ "integrateIntoHistory",
+ updateFactory -> {
+ long attempt = retryTracker.lastAttemptNumber + 1;
+ boolean isRetry = attempt > 1;
+ if (isRetry) {
+ logger.atFine().log(
+ "Retrying, attempt #%d; skipping merged changes", attempt);
+ this.ts = TimeUtil.now();
+ openRepoManager();
+ }
+ this.commitStatus = new CommitStatus(filteredNoteDbChangeSet, isRetry);
+ if (checkSubmitRules) {
+ logger.atFine().log("Checking submit rules and state");
+ checkSubmitRulesAndState(filteredNoteDbChangeSet, isRetry);
+ } else {
+ logger.atFine().log("Bypassing submit rules");
+ bypassSubmitRulesAndRequirements(filteredNoteDbChangeSet);
+ }
+ integrateIntoHistory(
+ filteredNoteDbChangeSet, submissionExecutor, checkSubmitRules);
+ return null;
+ })
+ .listener(retryTracker)
+ // Up to the entire submit operation is retried, including possibly many projects.
+ // Multiply the timeout by the number of projects we're actually attempting to
+ // submit. Times 2 to retry more persistently, to increase success rate.
+ .defaultTimeoutMultiplier(filteredNoteDbChangeSet.projects().size() * 2)
+ .call();
submissionExecutor.afterExecutions(orm);
if (projects > 1) {
@@ -754,7 +818,7 @@ public class MergeOp implements AutoCloseable {
boolean dryrun)
throws IntegrationConflictException, NoSuchProjectException, IOException {
List<SubmitStrategy> strategies = new ArrayList<>();
- Set<BranchNameKey> allBranches = updateOrderCalculator.getBranchesInOrder();
+ ImmutableSet<BranchNameKey> allBranches = updateOrderCalculator.getBranchesInOrder();
Set<CodeReviewCommit> allCommits =
toSubmit.values().stream().map(BranchBatch::commits).flatMap(Set::stream).collect(toSet());
@@ -767,7 +831,10 @@ public class MergeOp implements AutoCloseable {
requireNonNull(
submitting.submitType(),
String.format("null submit type for %s; expected to previously fail fast", submitting));
- Set<CodeReviewCommit> commitsToSubmit = submitting.commits();
+ ImmutableSet<CodeReviewCommit> commitsToSubmit = submitting.commits();
+ checkImplicitMerges(
+ branch, or.rw, submitting.commits(), submitting.submitType(), ob.oldTip);
+
ob.mergeTip = new MergeTip(ob.oldTip, commitsToSubmit);
SubmitStrategy strategy =
submitStrategyFactory.create(
@@ -793,6 +860,202 @@ public class MergeOp implements AutoCloseable {
return strategies;
}
+ private void checkImplicitMerges(
+ BranchNameKey branch,
+ RevWalk rw,
+ Set<CodeReviewCommit> commitsToSubmit,
+ SubmitType submitType,
+ @Nullable RevCommit branchTip)
+ throws IOException {
+ if (branchTip == null) {
+ // The branch doesn't exist.
+ return;
+ }
+ Project.NameKey project = branch.project();
+ if (!experimentFeatures.isFeatureEnabled(
+ GERRIT_BACKEND_FEATURE_CHECK_IMPLICIT_MERGES_ON_MERGE, project)) {
+ return;
+ }
+ if (submitType == SubmitType.CHERRY_PICK || submitType == SubmitType.REBASE_ALWAYS) {
+ return;
+ }
+
+ boolean projectConfigRejectImplicitMerges =
+ projectCache
+ .get(project)
+ .orElseThrow(illegalState(project))
+ .is(BooleanProjectConfig.REJECT_IMPLICIT_MERGES);
+ boolean rejectImplicitMergesOnMerges =
+ experimentFeatures.isFeatureEnabled(
+ GERRIT_BACKEND_FEATURE_REJECT_IMPLICIT_MERGES_ON_MERGE, project)
+ && (experimentFeatures.isFeatureEnabled(
+ GERRIT_BACKEND_FEATURE_ALWAYS_REJECT_IMPLICIT_MERGES_ON_MERGE, project)
+ || projectConfigRejectImplicitMerges);
+ try {
+ if (hasImplicitMerges(branch, rw, commitsToSubmit, branchTip)) {
+ if (rejectImplicitMergesOnMerges) {
+ commitStatus.addImplicitMerge(project, branch);
+ } else {
+ String allCommits =
+ commitsToSubmit.stream()
+ .map(CodeReviewCommit::getId)
+ .map(c -> ObjectId.toString(c))
+ .collect(joining(", "));
+ logger.atWarning().log(
+ "Implicit merge was detected for the branch %s of the project %s. "
+ + "Commits to be merged are: %s",
+ branch.shortName(), project, allCommits);
+ }
+ }
+ } catch (Exception e) {
+ if (rejectImplicitMergesOnMerges) {
+ throw e;
+ }
+ logger.atWarning().withCause(e).log("Error while checking for implicit merges");
+ }
+ }
+
+ private boolean isMergedInBranchAsSubmittedChange(RevCommit commit, BranchNameKey dest) {
+ List<ChangeData> changes = queryProvider.get().byBranchCommit(dest, commit.getId().getName());
+ for (ChangeData change : changes) {
+ if (change.change().isMerged()) {
+ logger.atFine().log(
+ "Dependency %s associated with merged change %s.", commit.getName(), change.getId());
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks if merging {@code commitsToSubmit} into the target branch leads to implicit merge.
+ *
+ * <p>All commits in the {@code commitsToSubmit} have {@code targetBranch} as a target. When
+ * multiple changes are submitted together, the {@code commitsToSubmit} contains transitive
+ * dependencies, not a single change (the method is never called for the cherry pick strategy
+ * because the strategy always submit a single change).
+ */
+ private boolean hasImplicitMerges(
+ BranchNameKey targetBranch,
+ RevWalk rw,
+ Set<CodeReviewCommit> commitsToSubmit,
+ RevCommit branchTip)
+ throws IOException {
+
+ // rootCommits - top level commits in chains. It is all commits which don't have children in
+ // the commitsToSubmit set (no commits have them as parents).
+ Set<CodeReviewCommit> rootCommits = new HashSet<>(commitsToSubmit);
+ Set<RevCommit> allParents = new HashSet<>();
+ for (CodeReviewCommit commit : commitsToSubmit) {
+ rw.parseBody(commit);
+ for (RevCommit parent : commit.getParents()) {
+ rootCommits.remove(parent);
+ allParents.add(parent);
+ }
+ }
+
+ // Calculate all "external" parents of commitsToSubmit - i.e. all parents which already
+ // present in the repository.
+ // targetBranchParents - all "external" parents which were merged into the targetBranch (
+ // they are reachable from the targetBranchTip).
+ Set<RevCommit> targetBranchParents = new HashSet<>();
+ int nonTargetBranchParentsCount = 0;
+ try {
+ for (RevCommit parent : Sets.difference(allParents, commitsToSubmit)) {
+ if (rw.isMergedInto(parent, branchTip)) {
+ targetBranchParents.add(parent);
+ } else {
+ // Special case: user created chain of changes and then submit first changes from the
+ // chain. It should be allowed for the user to submit remaining changes of the chain
+ // without rebasing them (otherwise votes can be lost).
+ // When a rebase... strategy is used in this scenario, submitting the first few changes of
+ // the chain creates new patchset(s), but all others changes are not rebased on top of new
+ // patchset(s). In this situation isMergedInto check is not enough and additional
+ // isMergedInBranchAsSubmittedChange check should be used.
+ if (isMergedInBranchAsSubmittedChange(parent, targetBranch)) {
+ targetBranchParents.add(parent);
+ } else {
+ nonTargetBranchParentsCount++;
+ }
+ }
+ }
+ } finally {
+ // It's unclear why resetting the RevWalk here is needed, but if we don't do this MergeSorter
+ // and RebaseSorter which are invoked later with the same RevWalk instance may fail while
+ // marking commits as uninteresting.
+ rw.reset();
+ }
+
+ if (nonTargetBranchParentsCount == 0) {
+ // All parents are in target branch, no implicit merge is possible.
+ return false;
+ }
+ // There are some parents not in the target branch.
+ if (rootCommits.size() == 1) {
+ // There is only one root commit - this is the case when a single chain of changes is
+ // submitted to the branch.
+ // If the target branch is not reachable from the root commit then it means that there is no
+ // explicit merge with the target branch and the merge operation will create an implicit merge
+ // (except if rebase is used; but for consistency between different strategies we reject
+ // merge even for rebase).
+ return targetBranchParents.isEmpty();
+ }
+ // There are multiple root commits - check that a target branch is reachable from each root
+ // commit. This situation means that multiple chain of changes are submitted (e.g. as a part
+ // of a single topic).
+ // reachableCommits contains pairs of commit: the first item in pair is always one of the root
+ // commits. The second item in pair - a commit reachable from this root (following parents).
+ // Loop implements breadth-search.
+ Deque<Entry<CodeReviewCommit, RevCommit>> reachableCommits =
+ new ArrayDeque<>(rootCommits.size());
+ rootCommits.forEach(commit -> reachableCommits.add(Map.entry(commit, commit)));
+ // Tracks all chains roots which can lead to implicit merge.
+ Set<CodeReviewCommit> implicitMergesRoots = new HashSet<>(rootCommits);
+ int iterationCount = 0;
+ Stopwatch sw = Stopwatch.createStarted();
+ while (!reachableCommits.isEmpty()) {
+ iterationCount++;
+ if (hasImplicitMergeTimeoutSeconds != 0
+ && sw.elapsed(TimeUnit.SECONDS) >= hasImplicitMergeTimeoutSeconds) {
+ String allCommits =
+ commitsToSubmit.stream()
+ .map(CodeReviewCommit::getId)
+ .map(c -> ObjectId.toString(c))
+ .collect(joining(", "));
+ logger.atWarning().log(
+ "Timeout during hasImplicitMerge calculation. Number of iterations: %s, commitsToSubmit: %s",
+ iterationCount, allCommits);
+ return true;
+ }
+ Entry<CodeReviewCommit, RevCommit> entry = reachableCommits.pop();
+ if (!implicitMergesRoots.contains(entry.getKey())) {
+ // We already know that from the given root (key in the entry) one of the
+ // targetBranchParents is reachable and this is not an implicit merge.
+ continue;
+ }
+ if (targetBranchParents.contains(entry.getValue())) {
+ // The target branch is reachable from the root. We don't need to process other items
+ // in the queue for this root.
+ implicitMergesRoots.remove(entry.getKey());
+ continue;
+ }
+ if (entry.getValue() == null) {
+ logger.atSevere().log("The entry value is null for the key %s", entry.getKey());
+ }
+ rw.parseBody(entry.getValue());
+ if (entry.getValue().getParents() == null) {
+ logger.atSevere().log(
+ "The entry value has null parents. The value is: %s", entry.getValue());
+ }
+ for (RevCommit parent : entry.getValue().getParents()) {
+ reachableCommits.push(Map.entry(entry.getKey(), parent));
+ }
+ }
+ // only commits which don't have parents in the targetBranch remains in the implicitMergesRoots.
+ // If there are at least one commit - this is an implicit merge.
+ return !implicitMergesRoots.isEmpty();
+ }
+
private Set<RevCommit> getAlreadyAccepted(OpenRepo or, CodeReviewCommit branchTip) {
Set<RevCommit> alreadyAccepted = new HashSet<>();
diff --git a/java/com/google/gerrit/server/submit/RebaseSorter.java b/java/com/google/gerrit/server/submit/RebaseSorter.java
index 3645d3fb76..d769ddd89d 100644
--- a/java/com/google/gerrit/server/submit/RebaseSorter.java
+++ b/java/com/google/gerrit/server/submit/RebaseSorter.java
@@ -31,6 +31,7 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevFlag;
@@ -65,8 +66,10 @@ public class RebaseSorter {
public List<CodeReviewCommit> sort(Collection<CodeReviewCommit> toSort) throws IOException {
final List<CodeReviewCommit> sorted = new ArrayList<>();
final Set<CodeReviewCommit> sort = new HashSet<>(toSort);
+ final Set<ObjectId> uninterestingParents = new HashSet<>();
while (!sort.isEmpty()) {
final CodeReviewCommit n = removeOne(sort);
+ collectUninterestingParents(n, uninterestingParents);
rw.resetRetain(canMergeFlag);
rw.markStart(n);
@@ -77,6 +80,8 @@ public class RebaseSorter {
CodeReviewCommit c;
final List<CodeReviewCommit> contents = new ArrayList<>();
while ((c = rw.next()) != null) {
+ collectUninterestingParents(c, uninterestingParents);
+
if (!c.has(canMergeFlag) || !incoming.contains(c)) {
if (isMergedInBranchAsSubmittedChange(c, n.change().getDest())
|| isAlreadyMergedInAnyBranch(c)) {
@@ -106,9 +111,27 @@ public class RebaseSorter {
sorted.removeAll(contents);
sorted.addAll(contents);
}
+ sorted.removeAll(uninterestingParents);
return sorted;
}
+ /**
+ * Rebasing merge commits is done by rebasing their first parent commit, i.e. the first parent is
+ * updated to the new base while the second parent stays intact. This means for chains of changes
+ * we only need to rebase changes that are reachable via the first parents. Changes that are
+ * reachable via second parents do not need to be rebased (since the second parent of merge
+ * commits stays intact) which is why we filter them out here by marking them as uninteresting.
+ */
+ private void collectUninterestingParents(CodeReviewCommit c, Set<ObjectId> uninterestingParents)
+ throws IOException {
+ if (c.getParentCount() > 0) {
+ for (int parent = 1; parent < c.getParentCount(); parent++) {
+ uninterestingParents.add(c.getParent(parent));
+ rw.markUninteresting(c.getParent(parent));
+ }
+ }
+ }
+
private boolean isAlreadyMergedInAnyBranch(CodeReviewCommit commit) throws IOException {
try (CodeReviewRevWalk mirw = CodeReviewCommit.newRevWalk(rw.getObjectReader())) {
mirw.reset();
diff --git a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
index 87de810f2f..e3d7fc48c8 100644
--- a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
@@ -15,9 +15,7 @@
package com.google.gerrit.server.submit;
import static com.google.common.base.Preconditions.checkState;
-import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.server.submit.CommitMergeStatus.EMPTY_COMMIT;
-import static com.google.gerrit.server.submit.CommitMergeStatus.SKIPPED_IDENTICAL_TREE;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.Nullable;
@@ -32,6 +30,7 @@ import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.change.RebaseChangeOp;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.MergeTip;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
@@ -41,11 +40,7 @@ import com.google.gerrit.server.update.RepoContext;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
-import java.util.Optional;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
/** This strategy covers RebaseAlways and RebaseIfNecessary ones. */
public class RebaseSubmitStrategy extends SubmitStrategy {
@@ -65,39 +60,6 @@ public class RebaseSubmitStrategy extends SubmitStrategy {
throw new StorageException("Commit sorting failed", e);
}
- // We cannot rebase merge commits. This is why we integrate merge changes into the target branch
- // the same way as if MERGE_IF_NECESSARY was the submit strategy. This means if needed we create
- // a merge commit that integrates the merge change into the target branch.
- // If we integrate a change series that consists out of a normal change and a merge change,
- // where the merge change depends on the normal change, we must skip rebasing the normal change,
- // because it already gets integrated by merging the merge change. If the rebasing of the normal
- // change is not skipped, it would appear twice in the history after the submit is done (once
- // through its rebased commit, and once through its original commit which is a parent of the
- // merge change that was merged into the target branch. To skip the rebasing of the normal
- // change, we call MergeUtil#reduceToMinimalMerge, as it excludes commits which will be
- // implicitly integrated by merging the series. Then we use the MergeIfNecessaryOp to integrate
- // the whole series.
- // If on the other hand, we integrate a change series that consists out of a merge change and a
- // normal change, where the normal change depends on the merge change, we can first integrate
- // the merge change by a merge and then integrate the normal change by a rebase. In this case we
- // do not want to call MergeUtil#reduceToMinimalMerge as we are not intending to integrate the
- // whole series by a merge, but rather do the integration of the commits one by one.
- boolean foundNonMerge = false;
- for (CodeReviewCommit c : sorted) {
- if (c.getParentCount() > 1) {
- if (!foundNonMerge) {
- // found a merge change, but it doesn't depend on a normal change, this means we are not
- // required to merge the whole series at once
- continue;
- }
- // found a merge commit that depends on a normal change, this means we are required to merge
- // the whole series at once
- sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, sorted);
- return sorted.stream().map(n -> new MergeIfNecessaryOp(n)).collect(toImmutableList());
- }
- foundNonMerge = true;
- }
-
ImmutableList.Builder<SubmitStrategyOp> ops =
ImmutableList.builderWithExpectedSize(sorted.size());
boolean first = true;
@@ -109,10 +71,8 @@ public class RebaseSubmitStrategy extends SubmitStrategy {
ops.add(new FastForwardOp(args, n));
} else if (n.getParentCount() == 0) {
ops.add(new RebaseRootOp(n));
- } else if (n.getParentCount() == 1) {
- ops.add(new RebaseOneOp(n));
} else {
- ops.add(new MergeIfNecessaryOp(n));
+ ops.add(new RebaseOneOp(n));
}
first = false;
}
@@ -144,91 +104,62 @@ public class RebaseSubmitStrategy extends SubmitStrategy {
@Override
public void updateRepoImpl(RepoContext ctx)
throws InvalidChangeOperationException, RestApiException, IOException,
- PermissionBackendException {
- if (args.mergeUtil.canFastForward(
- args.mergeSorter, args.mergeTip.getCurrentTip(), args.rw, toMerge)) {
- if (!rebaseAlways) {
- if (args.project.is(BooleanProjectConfig.REJECT_EMPTY_COMMIT)
- && toMerge.getTree().equals(toMerge.getParent(0).getTree())) {
- toMerge.setStatusCode(EMPTY_COMMIT);
- return;
- }
-
- args.mergeTip.moveTipTo(amendGitlink(toMerge), toMerge);
- toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
- acceptMergeTip(args.mergeTip);
+ PermissionBackendException, DiffNotAvailableException {
+ if (!rebaseAlways
+ && args.mergeUtil.canFastForward(
+ args.mergeSorter, args.mergeTip.getCurrentTip(), args.rw, toMerge)) {
+ if (args.project.is(BooleanProjectConfig.REJECT_EMPTY_COMMIT)
+ && toMerge.getTree().equals(toMerge.getParent(0).getTree())) {
+ toMerge.setStatusCode(EMPTY_COMMIT);
return;
}
- // RebaseAlways means we modify commit message.
- args.rw.parseBody(toMerge);
- newPatchSetId =
- ChangeUtil.nextPatchSetIdFromChangeRefs(
- ctx.getRepoView().getRefs(getId().toRefPrefix()).keySet(),
- toMerge.change().currentPatchSetId());
- RevCommit mergeTip = args.mergeTip.getCurrentTip();
- args.rw.parseBody(mergeTip);
- String cherryPickCmtMsg = args.mergeUtil.createCommitMessageOnSubmit(toMerge, mergeTip);
- PersonIdent committer =
- Optional.ofNullable(toMerge.getCommitterIdent())
- .map(ident -> ctx.newCommitterIdent(ident.getEmailAddress(), args.caller))
- .orElseGet(() -> ctx.newCommitterIdent(args.caller));
- try {
- newCommit =
- args.mergeUtil.createCherryPickFromCommit(
- ctx.getInserter(),
- ctx.getRepoView().getConfig(),
- args.mergeTip.getCurrentTip(),
- toMerge,
- committer,
- cherryPickCmtMsg,
- args.rw,
- 0,
- true,
- false);
- } catch (MergeConflictException mce) {
- // Unlike in Cherry-pick case, this should never happen.
- toMerge.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
- throw new IllegalStateException(
- "MergeConflictException on message edit must not happen", mce);
- } catch (MergeIdenticalTreeException mie) {
- // this should not happen
- toMerge.setStatusCode(SKIPPED_IDENTICAL_TREE);
- return;
- }
- ctx.addRefUpdate(ObjectId.zeroId(), newCommit, newPatchSetId.toRefName());
- } else {
- // Stale read of patch set is ok; see comments in RebaseChangeOp.
- PatchSet origPs = args.psUtil.get(toMerge.getNotes(), toMerge.getPatchsetId());
- rebaseOp =
- args.rebaseFactory
- .create(toMerge.notes(), origPs, args.mergeTip.getCurrentTip())
- .setFireRevisionCreated(false)
- // Bypass approval copier since SubmitStrategyOp copy all approvals
- // later anyway.
- .setValidate(false)
- .setCheckAddPatchSetPermission(false)
- // RebaseAlways should set always modify commit message like
- // Cherry-Pick strategy.
- .setDetailedCommitMessage(rebaseAlways)
- // Do not post message after inserting new patchset because there
- // will be one about change being merged already.
- .setPostMessage(false)
- .setSendEmail(false)
- .setMatchAuthorToCommitterDate(
- args.project.is(BooleanProjectConfig.MATCH_AUTHOR_TO_COMMITTER_DATE))
- // The votes are automatically copied and they don't count as copied votes. See
- // method's javadoc.
- .setStoreCopiedVotes(/* storeCopiedVotes = */ false);
- try {
- rebaseOp.updateRepo(ctx);
- } catch (MergeConflictException | NoSuchChangeException e) {
- toMerge.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
- throw new IntegrationConflictException(
- "Cannot rebase " + toMerge.name() + ": " + e.getMessage(), e);
- }
- newCommit = args.rw.parseCommit(rebaseOp.getRebasedCommit());
- newPatchSetId = rebaseOp.getPatchSetId();
+
+ args.mergeTip.moveTipTo(amendGitlink(toMerge), toMerge);
+ toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
+ acceptMergeTip(args.mergeTip);
+ return;
}
+
+ args.rw.parseBody(toMerge);
+ newPatchSetId =
+ ChangeUtil.nextPatchSetIdFromChangeRefs(
+ ctx.getRepoView().getRefs(getId().toRefPrefix()).keySet(),
+ toMerge.change().currentPatchSetId());
+ // Stale read of patch set is ok; see comments in RebaseChangeOp.
+ PatchSet origPs = args.psUtil.get(toMerge.getNotes(), toMerge.getPatchsetId());
+ rebaseOp =
+ args.rebaseFactory
+ .create(toMerge.notes(), origPs, args.mergeTip.getCurrentTip())
+ .setFireRevisionCreated(false)
+ // Bypass approval copier since SubmitStrategyOp copy all approvals
+ // later anyway.
+ .setValidate(false)
+ .setCheckAddPatchSetPermission(false)
+ // RebaseAlways should set always modify commit message like
+ // Cherry-Pick strategy.
+ .setDetailedCommitMessage(rebaseAlways)
+ // Do not post message after inserting new patchset because there
+ // will be one about change being merged already.
+ .setPostMessage(false)
+ .setSendEmail(false)
+ .setMatchAuthorToCommitterDate(
+ args.project.is(BooleanProjectConfig.MATCH_AUTHOR_TO_COMMITTER_DATE))
+ // The votes are automatically copied and they don't count as copied votes. See
+ // method's javadoc.
+ .setStoreCopiedVotes(/* storeCopiedVotes = */ false)
+ .setVerifyNeedsRebase(/* verifyNeedsRebase= */ !rebaseAlways);
+
+ try {
+ rebaseOp.updateRepo(ctx);
+ } catch (MergeConflictException | NoSuchChangeException e) {
+ toMerge.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
+ throw new IntegrationConflictException(
+ "Cannot rebase " + toMerge.name() + ": " + e.getMessage(), e);
+ }
+
+ newCommit = args.rw.parseCommit(rebaseOp.getRebasedCommit());
+ newPatchSetId = rebaseOp.getPatchSetId();
+
if (args.project.is(BooleanProjectConfig.REJECT_EMPTY_COMMIT)
&& newCommit.getTree().equals(newCommit.getParent(0).getTree())) {
toMerge.setStatusCode(EMPTY_COMMIT);
@@ -255,7 +186,9 @@ public class RebaseSubmitStrategy extends SubmitStrategy {
PatchSet newPs;
if (rebaseOp != null) {
- rebaseOp.updateChange(ctx);
+ @SuppressWarnings("unused")
+ var unused = rebaseOp.updateChange(ctx);
+
newPs = rebaseOp.getPatchSet();
} else {
// CherryPick
@@ -285,49 +218,6 @@ public class RebaseSubmitStrategy extends SubmitStrategy {
}
}
- private class MergeIfNecessaryOp extends SubmitStrategyOp {
- private MergeIfNecessaryOp(CodeReviewCommit toMerge) {
- super(RebaseSubmitStrategy.this.args, toMerge);
- }
-
- @Override
- public void updateRepoImpl(RepoContext ctx) throws IntegrationConflictException, IOException {
- // There are multiple parents, so this is a merge commit. We don't want
- // to rebase the merge as clients can't easily rebase their history with
- // that merge present and replaced by an equivalent merge with a different
- // first parent. So instead behave as though MERGE_IF_NECESSARY was
- // configured.
- // TODO(tandrii): this is not in spirit of RebaseAlways strategy because
- // the commit messages can not be modified in the process. It's also
- // possible to implement rebasing of merge commits. E.g., the Cherry Pick
- // REST endpoint already supports cherry-picking of merge commits.
- // For now, users of RebaseAlways strategy for whom changed commit footers
- // are important would be well advised to prohibit uploading patches with
- // merge commits.
- MergeTip mergeTip = args.mergeTip;
- if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge)
- && !args.subscriptionGraph.hasSubscription(args.destBranch)) {
- mergeTip.moveTipTo(toMerge, toMerge);
- } else {
- PersonIdent caller = ctx.newCommitterIdent();
- CodeReviewCommit newTip =
- args.mergeUtil.mergeOneCommit(
- caller,
- caller,
- args.rw,
- ctx.getInserter(),
- ctx.getRepoView().getConfig(),
- args.destBranch,
- mergeTip.getCurrentTip(),
- toMerge);
- mergeTip.moveTipTo(amendGitlink(newTip), toMerge);
- }
- args.mergeUtil.markCleanMerges(
- args.rw, args.canMergeFlag, mergeTip.getCurrentTip(), args.alreadyAccepted);
- acceptMergeTip(mergeTip);
- }
- }
-
private void acceptMergeTip(MergeTip mergeTip) {
args.alreadyAccepted.add(mergeTip.getCurrentTip());
}
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategy.java b/java/com/google/gerrit/server/submit/SubmitStrategy.java
index bdda3fc5dd..d4dd67aa5c 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategy.java
@@ -264,7 +264,7 @@ public abstract class SubmitStrategy {
* place.
*/
public final void addOps(BatchUpdate bu, Set<CodeReviewCommit> toMerge) {
- List<SubmitStrategyOp> ops = buildOps(toMerge);
+ ImmutableList<SubmitStrategyOp> ops = buildOps(toMerge);
Set<CodeReviewCommit> added = Sets.newHashSetWithExpectedSize(ops.size());
for (SubmitStrategyOp op : ops) {
diff --git a/java/com/google/gerrit/server/submit/SubmoduleCommits.java b/java/com/google/gerrit/server/submit/SubmoduleCommits.java
index 1fd3ad61db..88fb1d495d 100644
--- a/java/com/google/gerrit/server/submit/SubmoduleCommits.java
+++ b/java/com/google/gerrit/server/submit/SubmoduleCommits.java
@@ -18,6 +18,7 @@ import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.SubmoduleSubscription;
@@ -214,6 +215,7 @@ class SubmoduleCommits {
}
@Nullable
+ @CanIgnoreReturnValue
private RevCommit updateSubmodule(
DirCache dc, DirCacheEditor ed, StringBuilder msgbuf, SubmoduleSubscription s)
throws SubmoduleConflictException, IOException {
diff --git a/java/com/google/gerrit/server/submit/SubmoduleOp.java b/java/com/google/gerrit/server/submit/SubmoduleOp.java
index cebb5e3c0e..240235780d 100644
--- a/java/com/google/gerrit/server/submit/SubmoduleOp.java
+++ b/java/com/google/gerrit/server/submit/SubmoduleOp.java
@@ -25,7 +25,7 @@ import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
-import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.BatchUpdates;
import com.google.gerrit.server.update.UpdateException;
import com.google.gerrit.server.update.context.RefUpdateContext;
import com.google.inject.Inject;
@@ -39,13 +39,16 @@ public class SubmoduleOp {
@Singleton
public static class Factory {
+ private final BatchUpdates batchUpdates;
private final SubscriptionGraph.Factory subscriptionGraphFactory;
private final SubmoduleCommits.Factory submoduleCommitsFactory;
@Inject
Factory(
+ BatchUpdates batchUpdates,
SubscriptionGraph.Factory subscriptionGraphFactory,
SubmoduleCommits.Factory submoduleCommitsFactory) {
+ this.batchUpdates = batchUpdates;
this.subscriptionGraphFactory = subscriptionGraphFactory;
this.submoduleCommitsFactory = submoduleCommitsFactory;
}
@@ -54,6 +57,7 @@ public class SubmoduleOp {
Map<BranchNameKey, ReceiveCommand> updatedBranches, MergeOpRepoManager orm)
throws SubmoduleConflictException {
return new SubmoduleOp(
+ batchUpdates,
updatedBranches,
orm,
subscriptionGraphFactory.compute(updatedBranches.keySet(), orm),
@@ -61,6 +65,7 @@ public class SubmoduleOp {
}
}
+ private final BatchUpdates batchUpdates;
private final Map<BranchNameKey, ReceiveCommand> updatedBranches;
private final MergeOpRepoManager orm;
private final SubscriptionGraph subscriptionGraph;
@@ -68,10 +73,12 @@ public class SubmoduleOp {
private final UpdateOrderCalculator updateOrderCalculator;
private SubmoduleOp(
+ BatchUpdates batchUpdates,
Map<BranchNameKey, ReceiveCommand> updatedBranches,
MergeOpRepoManager orm,
SubscriptionGraph subscriptionGraph,
SubmoduleCommits submoduleCommits) {
+ this.batchUpdates = batchUpdates;
this.updatedBranches = updatedBranches;
this.orm = orm;
this.subscriptionGraph = subscriptionGraph;
@@ -107,7 +114,7 @@ public class SubmoduleOp {
}
}
try (RefUpdateContext ctx = RefUpdateContext.open(UPDATE_SUPERPROJECT)) {
- BatchUpdate.execute(
+ batchUpdates.execute(
orm.batchUpdates(superProjects, /* refLogMessage= */ "merged"),
ImmutableList.of(),
dryrun);
diff --git a/java/com/google/gerrit/server/submit/SubscriptionGraph.java b/java/com/google/gerrit/server/submit/SubscriptionGraph.java
index d434890267..711950c94e 100644
--- a/java/com/google/gerrit/server/submit/SubscriptionGraph.java
+++ b/java/com/google/gerrit/server/submit/SubscriptionGraph.java
@@ -37,7 +37,6 @@ import com.google.inject.Inject;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
@@ -283,7 +282,7 @@ public class SubscriptionGraph {
currentVisited.add(current);
try {
- Collection<SubmoduleSubscription> subscriptions =
+ List<SubmoduleSubscription> subscriptions =
superProjectSubscriptionsForSubmoduleBranch(current, branchGitModules, orm);
for (SubmoduleSubscription sub : subscriptions) {
BranchNameKey superBranch = sub.getSuperProject();
@@ -310,7 +309,7 @@ public class SubscriptionGraph {
allVisited.add(current);
}
- private Collection<BranchNameKey> getDestinationBranches(
+ private ImmutableSet<BranchNameKey> getDestinationBranches(
BranchNameKey src, SubscribeSection s, MergeOpRepoManager orm) throws IOException {
OpenRepo or;
try {
@@ -326,12 +325,12 @@ public class SubscriptionGraph {
return s.getDestinationBranches(src, refs);
}
- private Collection<SubmoduleSubscription> superProjectSubscriptionsForSubmoduleBranch(
+ private List<SubmoduleSubscription> superProjectSubscriptionsForSubmoduleBranch(
BranchNameKey srcBranch,
Map<BranchNameKey, GitModules> branchGitModules,
MergeOpRepoManager orm)
throws IOException {
- Collection<SubmoduleSubscription> ret = new ArrayList<>();
+ List<SubmoduleSubscription> ret = new ArrayList<>();
if (RefNames.isGerritRef(srcBranch.branch())) return ret;
Project.NameKey srcProject = srcBranch.project();
@@ -340,7 +339,7 @@ public class SubscriptionGraph {
.get(srcProject)
.orElseThrow(illegalState(srcProject))
.getSubscribeSections(srcBranch)) {
- Collection<BranchNameKey> branches = getDestinationBranches(srcBranch, s, orm);
+ ImmutableSet<BranchNameKey> branches = getDestinationBranches(srcBranch, s, orm);
for (BranchNameKey targetBranch : branches) {
Project.NameKey targetProject = targetBranch.project();
try {
diff --git a/java/com/google/gerrit/server/submit/UpdateOrderCalculator.java b/java/com/google/gerrit/server/submit/UpdateOrderCalculator.java
index 517c708e09..2f584aa995 100644
--- a/java/com/google/gerrit/server/submit/UpdateOrderCalculator.java
+++ b/java/com/google/gerrit/server/submit/UpdateOrderCalculator.java
@@ -18,7 +18,6 @@ import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.SubmoduleSubscription;
-import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
@@ -68,7 +67,8 @@ class UpdateOrderCalculator {
current.add(project);
Set<Project.NameKey> subprojects = new HashSet<>();
for (BranchNameKey branch : subscriptionGraph.getAffectedSuperBranches(project)) {
- Collection<SubmoduleSubscription> subscriptions = subscriptionGraph.getSubscriptions(branch);
+ ImmutableSet<SubmoduleSubscription> subscriptions =
+ subscriptionGraph.getSubscriptions(branch);
for (SubmoduleSubscription s : subscriptions) {
subprojects.add(s.getSubmodule().project());
}
diff --git a/java/com/google/gerrit/server/submit/package-info.java b/java/com/google/gerrit/server/submit/package-info.java
new file mode 100644
index 0000000000..d3dac18abd
--- /dev/null
+++ b/java/com/google/gerrit/server/submit/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.submit;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/submitrequirement/predicate/package-info.java b/java/com/google/gerrit/server/submitrequirement/predicate/package-info.java
new file mode 100644
index 0000000000..16257e5c65
--- /dev/null
+++ b/java/com/google/gerrit/server/submitrequirement/predicate/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.submitrequirement.predicate;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/tools/package-info.java b/java/com/google/gerrit/server/tools/package-info.java
new file mode 100644
index 0000000000..df5982299e
--- /dev/null
+++ b/java/com/google/gerrit/server/tools/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.tools;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/update/BatchUpdate.java b/java/com/google/gerrit/server/update/BatchUpdate.java
index 532b34521f..813246c5cf 100644
--- a/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -17,7 +17,6 @@ package com.google.gerrit.server.update;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.common.collect.ImmutableMultiset.toImmutableMultiset;
import static com.google.common.flogger.LazyArgs.lazy;
import static com.google.gerrit.common.UsedAt.Project.GOOGLE;
import static java.util.Comparator.comparing;
@@ -35,7 +34,6 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
-import com.google.common.collect.Multiset;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@@ -51,9 +49,6 @@ import com.google.gerrit.entities.ProjectChangeKey;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.CurrentUser;
@@ -73,12 +68,7 @@ import com.google.gerrit.server.logging.RequestId;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.LimitExceededException;
import com.google.gerrit.server.notedb.NoteDbUpdateManager;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.NoSuchRefException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import com.google.inject.Module;
@@ -91,10 +81,8 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.Optional;
import java.util.TreeMap;
-import java.util.function.Function;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectInserter;
@@ -103,7 +91,6 @@ import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.PushCertificate;
import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.transport.ReceiveCommand.Result;
/**
* Helper for a set of change updates that should be applied to the NoteDb database.
@@ -142,118 +129,6 @@ public class BatchUpdate implements AutoCloseable {
BatchUpdate create(Project.NameKey project, CurrentUser user, Instant when);
}
- public static void execute(
- Collection<BatchUpdate> updates, ImmutableList<BatchUpdateListener> listeners, boolean dryrun)
- throws UpdateException, RestApiException {
- requireNonNull(listeners);
- if (updates.isEmpty()) {
- return;
- }
-
- checkDifferentProject(updates);
-
- try {
- List<ListenableFuture<ChangeData>> indexFutures = new ArrayList<>();
- List<ChangesHandle> changesHandles = new ArrayList<>(updates.size());
- try {
- for (BatchUpdate u : updates) {
- u.executeUpdateRepo();
- }
- notifyAfterUpdateRepo(listeners);
- for (BatchUpdate u : updates) {
- changesHandles.add(u.executeChangeOps(listeners, dryrun));
- }
- for (ChangesHandle h : changesHandles) {
- h.execute();
- if (h.requiresReindex()) {
- indexFutures.addAll(h.startIndexFutures());
- }
- }
- notifyAfterUpdateRefs(listeners);
- notifyAfterUpdateChanges(listeners);
- } finally {
- for (ChangesHandle h : changesHandles) {
- h.close();
- }
- }
-
- Map<Change.Id, ChangeData> changeDatas =
- Futures.allAsList(indexFutures).get().stream()
- // filter out null values that were returned for change deletions
- .filter(Objects::nonNull)
- .collect(toMap(cd -> cd.change().getId(), Function.identity()));
-
- // Fire ref update events only after all mutations are finished, since callers may assume a
- // patch set ref being created means the change was created, or a branch advancing meaning
- // some changes were closed.
- updates.forEach(BatchUpdate::fireRefChangeEvents);
-
- if (!dryrun) {
- for (BatchUpdate u : updates) {
- u.executePostOps(changeDatas);
- }
- }
- } catch (Exception e) {
- wrapAndThrowException(e);
- }
- }
-
- private static void notifyAfterUpdateRepo(ImmutableList<BatchUpdateListener> listeners)
- throws Exception {
- for (BatchUpdateListener listener : listeners) {
- listener.afterUpdateRepos();
- }
- }
-
- private static void notifyAfterUpdateRefs(ImmutableList<BatchUpdateListener> listeners)
- throws Exception {
- for (BatchUpdateListener listener : listeners) {
- listener.afterUpdateRefs();
- }
- }
-
- private static void notifyAfterUpdateChanges(ImmutableList<BatchUpdateListener> listeners)
- throws Exception {
- for (BatchUpdateListener listener : listeners) {
- listener.afterUpdateChanges();
- }
- }
-
- private static void checkDifferentProject(Collection<BatchUpdate> updates) {
- Multiset<Project.NameKey> projectCounts =
- updates.stream().map(u -> u.project).collect(toImmutableMultiset());
- checkArgument(
- projectCounts.entrySet().size() == updates.size(),
- "updates must all be for different projects, got: %s",
- projectCounts);
- }
-
- private static void wrapAndThrowException(Exception e) throws UpdateException, RestApiException {
- // Convert common non-REST exception types with user-visible messages to corresponding REST
- // exception types.
- if (e instanceof InvalidChangeOperationException || e instanceof LimitExceededException) {
- throw new ResourceConflictException(e.getMessage(), e);
- } else if (e instanceof NoSuchChangeException
- || e instanceof NoSuchRefException
- || e instanceof NoSuchProjectException) {
- throw new ResourceNotFoundException(e.getMessage(), e);
- } else if (e instanceof CommentsRejectedException) {
- // SC_BAD_REQUEST is not ideal because it's not a syntactic error, but there is no better
- // status code and it's isolated in monitoring.
- throw new BadRequestException(e.getMessage(), e);
- }
-
- Throwables.throwIfUnchecked(e);
-
- // Propagate REST API exceptions thrown by operations; they commonly throw exceptions like
- // ResourceConflictException to indicate an atomic update failure.
- Throwables.throwIfInstanceOf(e, UpdateException.class);
- Throwables.throwIfInstanceOf(e, RestApiException.class);
-
- // Otherwise, wrap in a generic UpdateException, which does not include a user-visible message.
- throw new UpdateException(e);
- }
-
class ContextImpl implements Context {
private final CurrentUser contextUser;
@@ -409,6 +284,7 @@ public class BatchUpdate implements AutoCloseable {
DELETED
}
+ private final BatchUpdates batchUpdates;
private final GitRepositoryManager repoManager;
private final AccountCache accountCache;
private final ChangeData.Factory changeDataFactory;
@@ -445,6 +321,7 @@ public class BatchUpdate implements AutoCloseable {
@Inject
BatchUpdate(
+ BatchUpdates batchUpdates,
GitRepositoryManager repoManager,
@GerritPersonIdent PersonIdent serverIdent,
AccountCache accountCache,
@@ -461,6 +338,7 @@ public class BatchUpdate implements AutoCloseable {
@Assisted CurrentUser user,
@Assisted Instant when) {
this.gerritConfig = gerritConfig;
+ this.batchUpdates = batchUpdates;
this.repoManager = repoManager;
this.accountCache = accountCache;
this.changeDataFactory = changeDataFactory;
@@ -484,29 +362,35 @@ public class BatchUpdate implements AutoCloseable {
}
}
- public void execute(BatchUpdateListener listener) throws UpdateException, RestApiException {
- execute(ImmutableList.of(this), ImmutableList.of(listener), false);
+ @CanIgnoreReturnValue
+ public BatchUpdates.Result execute(BatchUpdateListener listener)
+ throws UpdateException, RestApiException {
+ return batchUpdates.execute(ImmutableList.of(this), ImmutableList.of(listener), false);
}
- public void execute() throws UpdateException, RestApiException {
- execute(ImmutableList.of(this), ImmutableList.of(), false);
+ @CanIgnoreReturnValue
+ public BatchUpdates.Result execute() throws UpdateException, RestApiException {
+ return batchUpdates.execute(ImmutableList.of(this), ImmutableList.of(), false);
}
public boolean isExecuted() {
return executed;
}
+ @CanIgnoreReturnValue
public BatchUpdate setRepository(Repository repo, RevWalk revWalk, ObjectInserter inserter) {
checkState(this.repoView == null, "repo already set");
repoView = new RepoView(repo, revWalk, inserter);
return this;
}
+ @CanIgnoreReturnValue
public BatchUpdate setPushCertificate(@Nullable PushCertificate pushCert) {
this.pushCert = pushCert;
return this;
}
+ @CanIgnoreReturnValue
public BatchUpdate setRefLogMessage(@Nullable String refLogMessage) {
this.refLogMessage = refLogMessage;
return this;
@@ -518,6 +402,7 @@ public class BatchUpdate implements AutoCloseable {
* @param notify notification settings.
* @return this.
*/
+ @CanIgnoreReturnValue
public BatchUpdate setNotify(NotifyResolver.Result notify) {
this.notify = requireNonNull(notify);
return this;
@@ -533,6 +418,7 @@ public class BatchUpdate implements AutoCloseable {
* @param notifyHandling notify handling.
* @return this.
*/
+ @CanIgnoreReturnValue
public BatchUpdate setNotifyHandling(Change.Id changeId, NotifyHandling notifyHandling) {
this.perChangeNotifyHandling.put(changeId, requireNonNull(notifyHandling));
return this;
@@ -542,6 +428,7 @@ public class BatchUpdate implements AutoCloseable {
* Add a validation step for intended ref operations, which will be performed at the end of {@link
* RepoOnlyOp#updateRepo(RepoContext)} step.
*/
+ @CanIgnoreReturnValue
public BatchUpdate setOnSubmitValidators(OnSubmitValidators onSubmitValidators) {
this.onSubmitValidators = onSubmitValidators;
return this;
@@ -578,7 +465,7 @@ public class BatchUpdate implements AutoCloseable {
*/
public Map<BranchNameKey, ReceiveCommand> getSuccessfullyUpdatedBranches(boolean dryrun) {
return getRefUpdates().entrySet().stream()
- .filter(entry -> dryrun || entry.getValue().getResult() == Result.OK)
+ .filter(entry -> dryrun || entry.getValue().getResult() == ReceiveCommand.Result.OK)
.collect(
toMap(entry -> BranchNameKey.create(project, entry.getKey()), Map.Entry::getValue));
}
@@ -636,7 +523,7 @@ public class BatchUpdate implements AutoCloseable {
return this;
}
- private void executeUpdateRepo() throws UpdateException, RestApiException {
+ void executeUpdateRepo() throws UpdateException, RestApiException {
try {
logDebug("Executing updateRepo on %d ops", ops.size());
for (Map.Entry<Change.Id, OpData<BatchUpdateOp>> e : ops.entries()) {
@@ -678,7 +565,7 @@ public class BatchUpdate implements AutoCloseable {
&& gerritConfig.getBoolean("index", "indexChangesAsync", false);
}
- private void fireRefChangeEvents() {
+ void fireRefChangeEvents() {
batchRefUpdate.forEach(
(projectName, bru) -> gitRefUpdated.fire(projectName, bru, getAccount().orElse(null)));
}
@@ -695,7 +582,7 @@ public class BatchUpdate implements AutoCloseable {
}
}
- private class ChangesHandle implements AutoCloseable {
+ class ChangesHandle implements AutoCloseable {
private final NoteDbUpdateManager manager;
private final boolean dryrun;
private final Map<Change.Id, ChangeResult> results;
@@ -780,7 +667,7 @@ public class BatchUpdate implements AutoCloseable {
}
}
- private ChangesHandle executeChangeOps(
+ ChangesHandle executeChangeOps(
ImmutableList<BatchUpdateListener> batchUpdateListeners, boolean dryrun) throws Exception {
logDebug("Executing change ops");
initRepository();
@@ -821,9 +708,11 @@ public class BatchUpdate implements AutoCloseable {
ctx.distinctUpdates.values().forEach(changeUpdates::add);
ctx = newChangeContext(opData.user(), id);
}
+ Class<? extends BatchUpdateOp> opClass = opData.op().getClass();
try (TraceContext.TraceTimer ignored =
TraceContext.newTimer(
- opData.getClass().getSimpleName() + "#updateChange",
+ (opClass.isAnonymousClass() ? opClass.getName() : opClass.getSimpleName())
+ + "#updateChange",
Metadata.builder().projectName(project.get()).changeId(id.get()).build())) {
dirty |= opData.op().updateChange(ctx);
deleted |= ctx.deleted;
@@ -908,7 +797,7 @@ public class BatchUpdate implements AutoCloseable {
return new ChangeContextImpl(contextUser, notes);
}
- private void executePostOps(Map<Change.Id, ChangeData> changeDatas) throws Exception {
+ void executePostOps(Map<Change.Id, ChangeData> changeDatas) throws Exception {
for (OpData<BatchUpdateOp> opData : ops.values()) {
PostUpdateContextImpl ctx = new PostUpdateContextImpl(opData.user(), changeDatas);
try (TraceContext.TraceTimer ignored =
diff --git a/java/com/google/gerrit/server/update/BatchUpdates.java b/java/com/google/gerrit/server/update/BatchUpdates.java
new file mode 100644
index 0000000000..2f9ef84744
--- /dev/null
+++ b/java/com/google/gerrit/server/update/BatchUpdates.java
@@ -0,0 +1,199 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.update;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableMultiset.toImmutableMultiset;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toMap;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Multiset;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.notedb.LimitExceededException;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.NoSuchRefException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.update.BatchUpdate.ChangesHandle;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+
+@Singleton
+public class BatchUpdates {
+ public class Result {
+ private final Map<Change.Id, ChangeData> changeDatas;
+
+ private Result() {
+ this(new HashMap<>());
+ }
+
+ private Result(Map<Change.Id, ChangeData> changeDatas) {
+ this.changeDatas = changeDatas;
+ }
+
+ /**
+ * Returns the updated {@link ChangeData} for the given project and change ID.
+ *
+ * <p>If the requested {@link ChangeData} was already loaded after the {@link BatchUpdate} has
+ * been executed the cached {@link ChangeData} instance is returned, otherwise the requested
+ * {@link ChangeData} is loaded and put into the cache.
+ */
+ public ChangeData getChangeData(Project.NameKey projectName, Change.Id changeId) {
+ return changeDatas.computeIfAbsent(
+ changeId, id -> changeDataFactory.create(projectName, changeId));
+ }
+ }
+
+ private final ChangeData.Factory changeDataFactory;
+
+ @Inject
+ BatchUpdates(ChangeData.Factory changeDataFactory) {
+ this.changeDataFactory = changeDataFactory;
+ }
+
+ @CanIgnoreReturnValue
+ public Result execute(
+ Collection<BatchUpdate> updates, ImmutableList<BatchUpdateListener> listeners, boolean dryrun)
+ throws UpdateException, RestApiException {
+ requireNonNull(listeners);
+ if (updates.isEmpty()) {
+ return new Result();
+ }
+
+ checkDifferentProject(updates);
+
+ try {
+ List<ListenableFuture<ChangeData>> indexFutures = new ArrayList<>();
+ List<ChangesHandle> changesHandles = new ArrayList<>(updates.size());
+ try {
+ for (BatchUpdate u : updates) {
+ u.executeUpdateRepo();
+ }
+ notifyAfterUpdateRepo(listeners);
+ for (BatchUpdate u : updates) {
+ changesHandles.add(u.executeChangeOps(listeners, dryrun));
+ }
+ for (ChangesHandle h : changesHandles) {
+ h.execute();
+ if (h.requiresReindex()) {
+ indexFutures.addAll(h.startIndexFutures());
+ }
+ }
+ notifyAfterUpdateRefs(listeners);
+ notifyAfterUpdateChanges(listeners);
+ } finally {
+ for (ChangesHandle h : changesHandles) {
+ h.close();
+ }
+ }
+
+ Map<Change.Id, ChangeData> changeDatas =
+ Futures.allAsList(indexFutures).get().stream()
+ // filter out null values that were returned for change deletions
+ .filter(Objects::nonNull)
+ .collect(toMap(cd -> cd.change().getId(), Function.identity()));
+
+ // Fire ref update events only after all mutations are finished, since callers may assume a
+ // patch set ref being created means the change was created, or a branch advancing meaning
+ // some changes were closed.
+ updates.forEach(BatchUpdate::fireRefChangeEvents);
+
+ if (!dryrun) {
+ for (BatchUpdate u : updates) {
+ u.executePostOps(changeDatas);
+ }
+ }
+
+ return new Result(changeDatas);
+ } catch (Exception e) {
+ wrapAndThrowException(e);
+ return new Result();
+ }
+ }
+
+ private static void notifyAfterUpdateRepo(ImmutableList<BatchUpdateListener> listeners)
+ throws Exception {
+ for (BatchUpdateListener listener : listeners) {
+ listener.afterUpdateRepos();
+ }
+ }
+
+ private static void notifyAfterUpdateRefs(ImmutableList<BatchUpdateListener> listeners)
+ throws Exception {
+ for (BatchUpdateListener listener : listeners) {
+ listener.afterUpdateRefs();
+ }
+ }
+
+ private static void notifyAfterUpdateChanges(ImmutableList<BatchUpdateListener> listeners)
+ throws Exception {
+ for (BatchUpdateListener listener : listeners) {
+ listener.afterUpdateChanges();
+ }
+ }
+
+ private static void checkDifferentProject(Collection<BatchUpdate> updates) {
+ Multiset<Project.NameKey> projectCounts =
+ updates.stream().map(u -> u.getProject()).collect(toImmutableMultiset());
+ checkArgument(
+ projectCounts.entrySet().size() == updates.size(),
+ "updates must all be for different projects, got: %s",
+ projectCounts);
+ }
+
+ private static void wrapAndThrowException(Exception e) throws UpdateException, RestApiException {
+ // Convert common non-REST exception types with user-visible messages to corresponding REST
+ // exception types.
+ if (e instanceof InvalidChangeOperationException || e instanceof LimitExceededException) {
+ throw new ResourceConflictException(e.getMessage(), e);
+ } else if (e instanceof NoSuchChangeException
+ || e instanceof NoSuchRefException
+ || e instanceof NoSuchProjectException) {
+ throw new ResourceNotFoundException(e.getMessage(), e);
+ } else if (e instanceof CommentsRejectedException) {
+ // SC_BAD_REQUEST is not ideal because it's not a syntactic error, but there is no better
+ // status code and it's isolated in monitoring.
+ throw new BadRequestException(e.getMessage(), e);
+ }
+
+ Throwables.throwIfUnchecked(e);
+
+ // Propagate REST API exceptions thrown by operations; they commonly throw exceptions like
+ // ResourceConflictException to indicate an atomic update failure.
+ Throwables.throwIfInstanceOf(e, UpdateException.class);
+ Throwables.throwIfInstanceOf(e, RestApiException.class);
+
+ // Otherwise, wrap in a generic UpdateException, which does not include a user-visible message.
+ throw new UpdateException(e);
+ }
+}
diff --git a/java/com/google/gerrit/server/update/RepoView.java b/java/com/google/gerrit/server/update/RepoView.java
index da9b083217..7e861b6798 100644
--- a/java/com/google/gerrit/server/update/RepoView.java
+++ b/java/com/google/gerrit/server/update/RepoView.java
@@ -227,5 +227,10 @@ public class RepoView {
public void close() {
// Do nothing; the delegate is closed separately.
}
+
+ @Override
+ public String toString() {
+ return String.format("%s (wrapped inserter: %s)", super.toString(), delegate.toString());
+ }
}
}
diff --git a/java/com/google/gerrit/server/update/RetryHelper.java b/java/com/google/gerrit/server/update/RetryHelper.java
index 7e6974cf30..c5621ed21c 100644
--- a/java/com/google/gerrit/server/update/RetryHelper.java
+++ b/java/com/google/gerrit/server/update/RetryHelper.java
@@ -45,6 +45,7 @@ import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.query.group.InternalGroupQuery;
import com.google.gerrit.server.update.RetryableAction.Action;
import com.google.gerrit.server.update.RetryableAction.ActionType;
import com.google.gerrit.server.update.RetryableChangeAction.ChangeAction;
@@ -188,6 +189,7 @@ public class RetryHelper {
private final BatchUpdate.Factory updateFactory;
private final Provider<InternalAccountQuery> internalAccountQuery;
private final Provider<InternalChangeQuery> internalChangeQuery;
+ private final Provider<InternalGroupQuery> internalGroupQuery;
private final PluginSetContext<ExceptionHook> exceptionHooks;
private final Duration defaultTimeout;
private final Map<String, Duration> defaultTimeouts;
@@ -202,13 +204,15 @@ public class RetryHelper {
PluginSetContext<ExceptionHook> exceptionHooks,
BatchUpdate.Factory updateFactory,
Provider<InternalAccountQuery> internalAccountQuery,
- Provider<InternalChangeQuery> internalChangeQuery) {
+ Provider<InternalChangeQuery> internalChangeQuery,
+ Provider<InternalGroupQuery> internalGroupQuery) {
this(
cfg,
metrics,
updateFactory,
internalAccountQuery,
internalChangeQuery,
+ internalGroupQuery,
exceptionHooks,
null);
}
@@ -220,6 +224,7 @@ public class RetryHelper {
BatchUpdate.Factory updateFactory,
Provider<InternalAccountQuery> internalAccountQuery,
Provider<InternalChangeQuery> internalChangeQuery,
+ Provider<InternalGroupQuery> internalGroupQuery,
PluginSetContext<ExceptionHook> exceptionHooks,
@Nullable Consumer<RetryerBuilder<?>> overwriteDefaultRetryerStrategySetup) {
this.cfg = cfg;
@@ -227,6 +232,7 @@ public class RetryHelper {
this.updateFactory = updateFactory;
this.internalAccountQuery = internalAccountQuery;
this.internalChangeQuery = internalChangeQuery;
+ this.internalGroupQuery = internalGroupQuery;
this.exceptionHooks = exceptionHooks;
this.defaultTimeout =
Duration.ofMillis(
@@ -386,6 +392,22 @@ public class RetryHelper {
}
/**
+ * Creates an action for querying the group index that is executed with retrying when called.
+ *
+ * <p>The index query action gets a {@link InternalGroupQuery} provided that can be used to query
+ * the account index.
+ *
+ * @param actionName the name of the action, used as metric bucket
+ * @param indexQueryAction the action that should be executed
+ * @return the retryable action, callers need to call {@link RetryableIndexQueryAction#call()} to
+ * execute the action
+ */
+ public <T> RetryableIndexQueryAction<InternalGroupQuery, T> groupIndexQuery(
+ String actionName, IndexQueryAction<T, InternalGroupQuery> indexQueryAction) {
+ return new RetryableIndexQueryAction<>(this, internalGroupQuery, actionName, indexQueryAction);
+ }
+
+ /**
* Returns the default timeout for an action type.
*
* <p>The default timeout for an action type is defined by the 'retry.<action-type>.timeout'
@@ -454,17 +476,30 @@ public class RetryHelper {
actionType,
opts,
t -> {
+ String actionName = opts.actionName().orElse("N/A");
+ String cause = formatCause(t);
+
+ // Do not retry if retrying was already done and failed.
+ if (Throwables.getCausalChain(t).stream()
+ .anyMatch(RetryException.class::isInstance)) {
+ return false;
+ }
+
// exceptionPredicate checks for temporary errors for which the operation should be
// retried (e.g. LockFailure). The retry has good chances to succeed.
if (exceptionPredicate.test(t)) {
+ logger.atFine().withCause(t).log(
+ "Retry: %s failed with possibly temporary error (cause = %s)",
+ actionName, cause);
return true;
}
- String actionName = opts.actionName().orElse("N/A");
-
// Exception hooks may identify additional exceptions for retry.
if (exceptionHooks.stream()
.anyMatch(h -> h.shouldRetry(actionType, actionName, t))) {
+ logger.atFine().withCause(t).log(
+ "Retry: %s failed with possibly temporary error (cause = %s)",
+ actionName, cause);
return true;
}
@@ -480,7 +515,6 @@ public class RetryHelper {
return false;
}
- String cause = formatCause(t);
if (!TraceContext.isTracing()) {
String traceId = "retry-on-failure-" + new RequestId();
traceContext.addTag(RequestId.Type.TRACE_ID, traceId).forceLogging();
@@ -568,6 +602,9 @@ public class RetryHelper {
actionType,
opts.actionName().orElse("N/A"),
listener.getOriginalCause().map(this::formatCause).orElse("_unknown"));
+
+ // Re-throw the RetryException so that retrying is not re-attempted on an outer level.
+ throw e;
}
if (e.getCause() != null) {
Throwables.throwIfUnchecked(e.getCause());
diff --git a/java/com/google/gerrit/server/update/RetryableAction.java b/java/com/google/gerrit/server/update/RetryableAction.java
index f79a849e5a..1a713d22ce 100644
--- a/java/com/google/gerrit/server/update/RetryableAction.java
+++ b/java/com/google/gerrit/server/update/RetryableAction.java
@@ -18,7 +18,11 @@ import static java.util.Objects.requireNonNull;
import com.github.rholder.retry.RetryListener;
import com.google.common.base.Throwables;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.server.ExceptionHook;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
@@ -71,6 +75,8 @@ public class RetryableAction<T> {
private final RetryHelper.Options.Builder options = RetryHelper.options();
private final List<Predicate<Throwable>> exceptionPredicates = new ArrayList<>();
+ private int numberOfCalls;
+
RetryableAction(
RetryHelper retryHelper, ActionType actionType, String actionName, Action<T> action) {
this(retryHelper, requireNonNull(actionType, "actionType").name(), actionName, action);
@@ -79,7 +85,15 @@ public class RetryableAction<T> {
RetryableAction(RetryHelper retryHelper, String actionType, String actionName, Action<T> action) {
this.retryHelper = requireNonNull(retryHelper, "retryHelper");
this.actionType = requireNonNull(actionType, "actionType");
- this.action = requireNonNull(action, "action");
+ this.action =
+ () -> {
+ numberOfCalls++;
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ actionName, Metadata.builder().attempt(numberOfCalls).build())) {
+ return requireNonNull(action, "action").call();
+ }
+ };
options.actionName(requireNonNull(actionName, "actionName"));
}
@@ -97,6 +111,7 @@ public class RetryableAction<T> {
* exception
* @return this instance to enable chaining of calls
*/
+ @CanIgnoreReturnValue
public RetryableAction<T> retryOn(Predicate<Throwable> exceptionPredicate) {
exceptionPredicates.add(exceptionPredicate);
return this;
@@ -117,6 +132,7 @@ public class RetryableAction<T> {
* for a given exception
* @return this instance to enable chaining of calls
*/
+ @CanIgnoreReturnValue
public RetryableAction<T> retryWithTrace(Predicate<Throwable> exceptionPredicate) {
options.retryWithTrace(exceptionPredicate);
return this;
@@ -132,6 +148,7 @@ public class RetryableAction<T> {
* @param traceIdConsumer trace ID consumer
* @return this instance to enable chaining of calls
*/
+ @CanIgnoreReturnValue
public RetryableAction<T> onAutoTrace(Consumer<String> traceIdConsumer) {
options.onAutoTrace(traceIdConsumer);
return this;
@@ -145,6 +162,7 @@ public class RetryableAction<T> {
* @param retryListener retry listener
* @return this instance to enable chaining of calls
*/
+ @CanIgnoreReturnValue
public RetryableAction<T> listener(RetryListener retryListener) {
options.listener(retryListener);
return this;
@@ -158,6 +176,7 @@ public class RetryableAction<T> {
* @param multiplier multiplier for the default timeout
* @return this instance to enable chaining of calls
*/
+ @CanIgnoreReturnValue
public RetryableAction<T> defaultTimeoutMultiplier(int multiplier) {
options.timeout(retryHelper.getDefaultTimeout(actionType).multipliedBy(multiplier));
return this;
diff --git a/java/com/google/gerrit/server/update/SubmissionExecutor.java b/java/com/google/gerrit/server/update/SubmissionExecutor.java
index 39eda58e17..762de57af1 100644
--- a/java/com/google/gerrit/server/update/SubmissionExecutor.java
+++ b/java/com/google/gerrit/server/update/SubmissionExecutor.java
@@ -22,12 +22,16 @@ import java.util.Optional;
import java.util.stream.Collectors;
public class SubmissionExecutor {
-
+ private final BatchUpdates batchUpdates;
private final ImmutableList<SubmissionListener> submissionListeners;
private final boolean dryrun;
private ImmutableList<BatchUpdateListener> additionalListeners = ImmutableList.of();
- public SubmissionExecutor(boolean dryrun, ImmutableList<SubmissionListener> submissionListeners) {
+ public SubmissionExecutor(
+ BatchUpdates batchUpdates,
+ boolean dryrun,
+ ImmutableList<SubmissionListener> submissionListeners) {
+ this.batchUpdates = batchUpdates;
this.dryrun = dryrun;
this.submissionListeners = submissionListeners;
if (dryrun) {
@@ -58,7 +62,7 @@ public class SubmissionExecutor {
.map(Optional::get)
.collect(Collectors.toList()))
.build();
- BatchUpdate.execute(updates, listeners, dryrun);
+ batchUpdates.execute(updates, listeners, dryrun);
}
/**
diff --git a/java/com/google/gerrit/server/update/context/package-info.java b/java/com/google/gerrit/server/update/context/package-info.java
new file mode 100644
index 0000000000..2637a62bc4
--- /dev/null
+++ b/java/com/google/gerrit/server/update/context/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.update.context;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/update/package-info.java b/java/com/google/gerrit/server/update/package-info.java
new file mode 100644
index 0000000000..b80bffddc7
--- /dev/null
+++ b/java/com/google/gerrit/server/update/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.update;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/util/AttentionSetEmail.java b/java/com/google/gerrit/server/util/AttentionSetEmail.java
index 82e5bd17a7..95fc246541 100644
--- a/java/com/google/gerrit/server/util/AttentionSetEmail.java
+++ b/java/com/google/gerrit/server/util/AttentionSetEmail.java
@@ -179,7 +179,8 @@ public class AttentionSetEmail {
} catch (Exception e) {
logger.atSevere().withCause(e).log("Cannot email update for change %s", changeId);
} finally {
- requestContext.setContext(old);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(old);
}
}
diff --git a/java/com/google/gerrit/server/util/LabelVote.java b/java/com/google/gerrit/server/util/LabelVote.java
index fbcf3ce929..a227d7a48d 100644
--- a/java/com/google/gerrit/server/util/LabelVote.java
+++ b/java/com/google/gerrit/server/util/LabelVote.java
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.PatchSetApproval;
@@ -56,6 +57,7 @@ public abstract class LabelVote {
return create(text.substring(0, e), Short.parseShort(text.substring(e + 1)));
}
+ @CanIgnoreReturnValue
public static StringBuilder appendTo(StringBuilder sb, String label, short value) {
if (value == (short) 0) {
return sb.append('-').append(label);
diff --git a/java/com/google/gerrit/server/util/ManualRequestContext.java b/java/com/google/gerrit/server/util/ManualRequestContext.java
index 7790b5ff74..2873c57da7 100644
--- a/java/com/google/gerrit/server/util/ManualRequestContext.java
+++ b/java/com/google/gerrit/server/util/ManualRequestContext.java
@@ -42,6 +42,7 @@ public class ManualRequestContext implements RequestContext, AutoCloseable {
@Override
public void close() {
- requestContext.setContext(old);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(old);
}
}
diff --git a/java/com/google/gerrit/server/util/RequestScopePropagator.java b/java/com/google/gerrit/server/util/RequestScopePropagator.java
index 2f03b07c6c..acdafee92e 100644
--- a/java/com/google/gerrit/server/util/RequestScopePropagator.java
+++ b/java/com/google/gerrit/server/util/RequestScopePropagator.java
@@ -173,7 +173,8 @@ public abstract class RequestScopePropagator {
try {
return callable.call();
} finally {
- local.setContext(old);
+ @SuppressWarnings("unused")
+ var unused = local.setContext(old);
}
};
}
diff --git a/java/com/google/gerrit/server/util/git/BUILD b/java/com/google/gerrit/server/util/git/BUILD
index 83a230d226..70463825bb 100644
--- a/java/com/google/gerrit/server/util/git/BUILD
+++ b/java/com/google/gerrit/server/util/git/BUILD
@@ -8,6 +8,7 @@ java_library(
"//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/entities",
"//lib:jgit",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
],
)
diff --git a/java/com/google/gerrit/server/util/git/package-info.java b/java/com/google/gerrit/server/util/git/package-info.java
new file mode 100644
index 0000000000..3b98bfa59a
--- /dev/null
+++ b/java/com/google/gerrit/server/util/git/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.util.git;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/util/package-info.java b/java/com/google/gerrit/server/util/package-info.java
new file mode 100644
index 0000000000..ae74688134
--- /dev/null
+++ b/java/com/google/gerrit/server/util/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.util;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/util/time/BUILD b/java/com/google/gerrit/server/util/time/BUILD
index f3e00914a2..7fd6ecfe8b 100644
--- a/java/com/google/gerrit/server/util/time/BUILD
+++ b/java/com/google/gerrit/server/util/time/BUILD
@@ -8,5 +8,6 @@ java_library(
"//java/com/google/gerrit/server/util/git",
"//lib:guava",
"//lib:jgit",
+ "//lib/errorprone:annotations",
],
)
diff --git a/java/com/google/gerrit/server/util/time/package-info.java b/java/com/google/gerrit/server/util/time/package-info.java
new file mode 100644
index 0000000000..a0ac845dc9
--- /dev/null
+++ b/java/com/google/gerrit/server/util/time/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.util.time;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/validators/package-info.java b/java/com/google/gerrit/server/validators/package-info.java
new file mode 100644
index 0000000000..f677a65f78
--- /dev/null
+++ b/java/com/google/gerrit/server/validators/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.validators;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/version/BUILD b/java/com/google/gerrit/server/version/BUILD
index c7f659ca14..1176e33e22 100644
--- a/java/com/google/gerrit/server/version/BUILD
+++ b/java/com/google/gerrit/server/version/BUILD
@@ -12,6 +12,7 @@ java_library(
"//java/com/google/gerrit/index/project",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server/schema",
+ "//lib/errorprone:annotations",
"@guice-library//jar",
],
)
diff --git a/java/com/google/gerrit/server/version/package-info.java b/java/com/google/gerrit/server/version/package-info.java
new file mode 100644
index 0000000000..fd5d18ccf6
--- /dev/null
+++ b/java/com/google/gerrit/server/version/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.server.version;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/sshd/BUILD b/java/com/google/gerrit/sshd/BUILD
index 7a11131cfd..0a975498a9 100644
--- a/java/com/google/gerrit/sshd/BUILD
+++ b/java/com/google/gerrit/sshd/BUILD
@@ -37,6 +37,7 @@ java_library(
"//lib/auto:auto-value-annotations",
"//lib/bouncycastle:bcprov-neverlink",
"//lib/dropwizard:dropwizard-core",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
"//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/sshd/CommandModule.java b/java/com/google/gerrit/sshd/CommandModule.java
index 4242c71780..7becc2b34f 100644
--- a/java/com/google/gerrit/sshd/CommandModule.java
+++ b/java/com/google/gerrit/sshd/CommandModule.java
@@ -21,7 +21,11 @@ import org.apache.sshd.server.command.Command;
/** Module to register commands in the SSH daemon. */
public abstract class CommandModule extends LifecycleModule {
- protected boolean slaveMode;
+ protected final boolean slaveMode;
+
+ protected CommandModule(boolean slaveMode) {
+ this.slaveMode = slaveMode;
+ }
/**
* Configure a command to be invoked by name.
diff --git a/java/com/google/gerrit/sshd/PluginCommandModule.java b/java/com/google/gerrit/sshd/PluginCommandModule.java
index f0dc17ab01..9fd068cd67 100644
--- a/java/com/google/gerrit/sshd/PluginCommandModule.java
+++ b/java/com/google/gerrit/sshd/PluginCommandModule.java
@@ -14,24 +14,19 @@
package com.google.gerrit.sshd;
-import static com.google.common.base.Preconditions.checkState;
-
-import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.inject.Inject;
import com.google.inject.binder.LinkedBindingBuilder;
import org.apache.sshd.server.command.Command;
public abstract class PluginCommandModule extends CommandModule {
- private CommandName command;
+ private final CommandName command;
- @Inject
- void setPluginName(@PluginName String name) {
- this.command = Commands.named(name);
+ public PluginCommandModule(String pluginName) {
+ super(/* slaveMode= */ false);
+ this.command = Commands.named(pluginName);
}
@Override
protected final void configure() {
- checkState(command != null, "@PluginName must be provided");
bind(Commands.key(command)).toProvider(new DispatchCommandProvider(command));
configureCommands();
}
diff --git a/java/com/google/gerrit/sshd/SingleCommandPluginModule.java b/java/com/google/gerrit/sshd/SingleCommandPluginModule.java
index edc797c7f4..4e16fbcc68 100644
--- a/java/com/google/gerrit/sshd/SingleCommandPluginModule.java
+++ b/java/com/google/gerrit/sshd/SingleCommandPluginModule.java
@@ -14,10 +14,6 @@
package com.google.gerrit.sshd;
-import static com.google.common.base.Preconditions.checkState;
-
-import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.inject.Inject;
import com.google.inject.binder.LinkedBindingBuilder;
import org.apache.sshd.server.command.Command;
@@ -27,16 +23,15 @@ import org.apache.sshd.server.command.Command;
* <p>Cannot be combined with {@link PluginCommandModule}.
*/
public abstract class SingleCommandPluginModule extends CommandModule {
- private CommandName command;
+ private final CommandName command;
- @Inject
- void setPluginName(@PluginName String name) {
- this.command = Commands.named(name);
+ public SingleCommandPluginModule(String pluginName) {
+ super(/* slaveMode= */ false);
+ this.command = Commands.named(pluginName);
}
@Override
protected final void configure() {
- checkState(command != null, "@PluginName must be provided");
configure(bind(Commands.key(command)));
}
diff --git a/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java b/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
index 7adcd24095..8c37ca3d86 100644
--- a/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
+++ b/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
@@ -31,6 +31,7 @@ import java.util.HashMap;
import java.util.Map;
import org.apache.sshd.server.command.Command;
+@SuppressWarnings("MutableGuiceModule")
class SshAutoRegisterModuleGenerator extends AbstractModule implements ModuleGenerator {
private final Map<String, Class<Command>> commands = new HashMap<>();
private final ListMultimap<TypeLiteral<?>, Class<?>> listeners = LinkedListMultimap.create();
diff --git a/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index 58e331bb65..ff452a689b 100644
--- a/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -154,7 +154,7 @@ public class SshKeyCacheImpl implements SshKeyCache {
} else {
markInvalid(k);
}
- } catch (Throwable e) {
+ } catch (Exception e) {
markInvalid(k);
}
}
diff --git a/java/com/google/gerrit/sshd/SshModule.java b/java/com/google/gerrit/sshd/SshModule.java
index ca452c1c13..ca15ba7261 100644
--- a/java/com/google/gerrit/sshd/SshModule.java
+++ b/java/com/google/gerrit/sshd/SshModule.java
@@ -19,6 +19,7 @@ import static com.google.inject.Scopes.SINGLETON;
import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableMap;
import com.google.gerrit.extensions.events.AccountActivationListener;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.registration.DynamicSet;
@@ -39,7 +40,6 @@ import com.google.inject.Inject;
import com.google.inject.internal.UniqueAnnotations;
import com.google.inject.servlet.RequestScoped;
import java.net.SocketAddress;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledThreadPoolExecutor;
@@ -50,14 +50,15 @@ import org.eclipse.jgit.lib.Config;
/** Configures standard dependencies for {@link SshDaemon}. */
public class SshModule extends LifecycleModule {
- private final Map<String, String> aliases;
+ private final ImmutableMap<String, String> aliases;
@Inject
SshModule(@GerritServerConfig Config cfg) {
- aliases = new HashMap<>();
+ ImmutableMap.Builder<String, String> aliasesBuilder = ImmutableMap.builder();
for (String name : cfg.getNames("ssh-alias", true)) {
- aliases.put(name, cfg.getString("ssh-alias", null, name));
+ aliasesBuilder.put(name, cfg.getString("ssh-alias", null, name));
}
+ this.aliases = aliasesBuilder.build();
}
@Override
diff --git a/java/com/google/gerrit/sshd/SshScope.java b/java/com/google/gerrit/sshd/SshScope.java
index f19c3957ae..9ab63a4f8e 100644
--- a/java/com/google/gerrit/sshd/SshScope.java
+++ b/java/com/google/gerrit/sshd/SshScope.java
@@ -214,7 +214,10 @@ public class SshScope {
Context set(Context ctx) {
Context old = current.get();
current.set(ctx);
- local.setContext(ctx);
+
+ @SuppressWarnings("unused")
+ var unused = local.setContext(ctx);
+
return old;
}
diff --git a/java/com/google/gerrit/sshd/commands/CheckProjectAccessCommand.java b/java/com/google/gerrit/sshd/commands/CheckProjectAccessCommand.java
index 39f9ef2a7b..76406ef89c 100644
--- a/java/com/google/gerrit/sshd/commands/CheckProjectAccessCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CheckProjectAccessCommand.java
@@ -14,8 +14,10 @@
package com.google.gerrit.sshd.commands;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -31,8 +33,6 @@ import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
-import java.util.Set;
-import java.util.stream.Collectors;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.kohsuke.args4j.Option;
@@ -96,14 +96,14 @@ public class CheckProjectAccessCommand extends SshCommand {
}
}
- private Set<IdentifiedUser> getUserList(String userName)
+ private ImmutableSet<IdentifiedUser> getUserList(String userName)
throws ConfigInvalidException, IOException, ResourceNotFoundException {
- return getIdList(userName).stream().map(userFactory::create).collect(Collectors.toSet());
+ return getIdList(userName).stream().map(userFactory::create).collect(toImmutableSet());
}
- private Set<Account.Id> getIdList(String userName)
+ private ImmutableSet<Account.Id> getIdList(String userName)
throws ConfigInvalidException, IOException, ResourceNotFoundException {
- Set<Account.Id> idList = accountResolver.resolve(userName).asIdSet();
+ ImmutableSet<Account.Id> idList = accountResolver.resolve(userName).asIdSet();
if (idList.isEmpty()) {
throw new ResourceNotFoundException(
"No accounts found for your query: \""
diff --git a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index 2e292036a9..660b0df738 100644
--- a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -82,7 +82,9 @@ final class CreateAccountCommand extends SshCommand {
input.httpPassword = httpPassword;
input.groups = Lists.transform(groups, AccountGroup.Id::toString);
try {
- createAccount.apply(TopLevelResource.INSTANCE, IdString.fromDecoded(username), input);
+ @SuppressWarnings("unused")
+ var unused =
+ createAccount.apply(TopLevelResource.INSTANCE, IdString.fromDecoded(username), input);
} catch (RestApiException e) {
throw die(e.getMessage());
}
diff --git a/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 5fd22978e1..1f65c560f9 100644
--- a/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -142,7 +142,9 @@ final class CreateGroupCommand extends SshCommand {
AddMembers.Input input =
AddMembers.Input.fromMembers(
initialMembers.stream().map(Object::toString).collect(toList()));
- addMembers.apply(rsrc, input);
+
+ @SuppressWarnings("unused")
+ var unused = addMembers.apply(rsrc, input);
}
private void addSubgroups(GroupResource rsrc)
@@ -150,6 +152,8 @@ final class CreateGroupCommand extends SshCommand {
AddSubgroups.Input input =
AddSubgroups.Input.fromGroups(
initialGroups.stream().map(AccountGroup.UUID::get).collect(toList()));
- addSubgroups.apply(rsrc, input);
+
+ @SuppressWarnings("unused")
+ var unused = addSubgroups.apply(rsrc, input);
}
}
diff --git a/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index 42e7c0f0a2..ce8c26532b 100644
--- a/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -29,8 +29,8 @@ public class DefaultCommandModule extends CommandModule {
private final LfsPluginAuthCommandModule lfsPluginAuthModule;
public DefaultCommandModule(
- boolean slave, DownloadConfig downloadCfg, LfsPluginAuthCommandModule module) {
- slaveMode = slave;
+ boolean slaveMode, DownloadConfig downloadCfg, LfsPluginAuthCommandModule module) {
+ super(slaveMode);
downloadConfig = downloadCfg;
lfsPluginAuthModule = module;
}
diff --git a/java/com/google/gerrit/sshd/commands/ExternalIdCommandsModule.java b/java/com/google/gerrit/sshd/commands/ExternalIdCommandsModule.java
index 8b025d3d48..b0ecdfc743 100644
--- a/java/com/google/gerrit/sshd/commands/ExternalIdCommandsModule.java
+++ b/java/com/google/gerrit/sshd/commands/ExternalIdCommandsModule.java
@@ -24,6 +24,9 @@ import com.google.inject.Singleton;
import java.util.concurrent.ExecutorService;
public class ExternalIdCommandsModule extends CommandModule {
+ public ExternalIdCommandsModule() {
+ super(/* slaveMode= */ false);
+ }
@Override
protected void configure() {
diff --git a/java/com/google/gerrit/sshd/commands/FlushCaches.java b/java/com/google/gerrit/sshd/commands/FlushCaches.java
index fe2a8972f8..bfd64f6690 100644
--- a/java/com/google/gerrit/sshd/commands/FlushCaches.java
+++ b/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -75,9 +75,11 @@ final class FlushCaches extends SshCommand {
}
if (all) {
- postCaches.apply(new ConfigResource(), new PostCaches.Input(FLUSH_ALL));
+ @SuppressWarnings("unused")
+ var unused = postCaches.apply(new ConfigResource(), new PostCaches.Input(FLUSH_ALL));
} else {
- postCaches.apply(new ConfigResource(), new PostCaches.Input(FLUSH, caches));
+ @SuppressWarnings("unused")
+ var unused = postCaches.apply(new ConfigResource(), new PostCaches.Input(FLUSH, caches));
}
} catch (RestApiException e) {
throw die(e.getMessage());
diff --git a/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java b/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
index 1fb0e13a3a..40d8af33f6 100644
--- a/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
+++ b/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
@@ -56,7 +56,8 @@ final class IndexChangesCommand extends SshCommand {
boolean ok = true;
for (ChangeResource rsrc : changes.values()) {
try {
- index.apply(rsrc, new Input());
+ @SuppressWarnings("unused")
+ var unused = index.apply(rsrc, new Input());
} catch (Exception e) {
ok = false;
writeError(
diff --git a/java/com/google/gerrit/sshd/commands/IndexChangesInProjectCommand.java b/java/com/google/gerrit/sshd/commands/IndexChangesInProjectCommand.java
index 168dc19755..c7a03c4cf1 100644
--- a/java/com/google/gerrit/sshd/commands/IndexChangesInProjectCommand.java
+++ b/java/com/google/gerrit/sshd/commands/IndexChangesInProjectCommand.java
@@ -52,7 +52,8 @@ final class IndexChangesInProjectCommand extends SshCommand {
private void index(ProjectState projectState) {
try {
- index.apply(new ProjectResource(projectState, user), null);
+ @SuppressWarnings("unused")
+ var unused = index.apply(new ProjectResource(projectState, user), null);
} catch (Exception e) {
writeError(
"error", String.format("Unable to index %s: %s", projectState.getName(), e.getMessage()));
diff --git a/java/com/google/gerrit/sshd/commands/IndexCommandsModule.java b/java/com/google/gerrit/sshd/commands/IndexCommandsModule.java
index 332ed69eb3..b3268c53ec 100644
--- a/java/com/google/gerrit/sshd/commands/IndexCommandsModule.java
+++ b/java/com/google/gerrit/sshd/commands/IndexCommandsModule.java
@@ -27,6 +27,7 @@ public class IndexCommandsModule extends CommandModule {
private final Injector injector;
public IndexCommandsModule(Injector injector) {
+ super(/* slaveMode= */ false);
this.injector = injector;
}
diff --git a/java/com/google/gerrit/sshd/commands/KillCommand.java b/java/com/google/gerrit/sshd/commands/KillCommand.java
index a633a8a932..ec63d9d434 100644
--- a/java/com/google/gerrit/sshd/commands/KillCommand.java
+++ b/java/com/google/gerrit/sshd/commands/KillCommand.java
@@ -52,7 +52,9 @@ final class KillCommand extends SshCommand {
for (String id : taskIds) {
try {
TaskResource taskRsrc = tasksCollection.parse(cfgRsrc, IdString.fromDecoded(id));
- deleteTask.apply(taskRsrc, null);
+
+ @SuppressWarnings("unused")
+ var unused = deleteTask.apply(taskRsrc, null);
} catch (AuthException
| ResourceNotFoundException
| ResourceConflictException
diff --git a/java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java b/java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java
index 742536cfe6..3111c28940 100644
--- a/java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java
@@ -16,11 +16,12 @@ package com.google.gerrit.sshd.commands;
import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
-import java.util.Collections;
import java.util.Map;
import java.util.TreeMap;
import org.apache.log4j.LogManager;
@@ -52,7 +53,7 @@ public class ListLoggingLevelCommand extends SshCommand {
}
@SuppressWarnings({"unchecked", "JdkObsolete"})
- private static Iterable<Logger> getCurrentLoggers() {
- return Collections.list(LogManager.getCurrentLoggers());
+ private static ImmutableList<Logger> getCurrentLoggers() {
+ return ImmutableList.copyOf(Iterators.forEnumeration(LogManager.getCurrentLoggers()));
}
}
diff --git a/java/com/google/gerrit/sshd/commands/ReloadConfig.java b/java/com/google/gerrit/sshd/commands/ReloadConfig.java
index eeb48bb864..73b262b76f 100644
--- a/java/com/google/gerrit/sshd/commands/ReloadConfig.java
+++ b/java/com/google/gerrit/sshd/commands/ReloadConfig.java
@@ -16,7 +16,7 @@ package com.google.gerrit.sshd.commands;
import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
-import com.google.common.collect.Multimap;
+import com.google.common.collect.ListMultimap;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.server.config.ConfigUpdatedEvent.ConfigUpdateEntry;
@@ -39,7 +39,8 @@ public class ReloadConfig extends SshCommand {
@Override
protected void run() throws Failure {
enableGracefulStop();
- Multimap<UpdateResult, ConfigUpdateEntry> updates = gerritServerConfigReloader.reloadConfig();
+ ListMultimap<UpdateResult, ConfigUpdateEntry> updates =
+ gerritServerConfigReloader.reloadConfig();
if (updates.isEmpty()) {
stdout.println("No config entries updated!");
return;
diff --git a/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java b/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
index 976e7bd789..23bec1d421 100644
--- a/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
+++ b/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
@@ -51,7 +51,9 @@ public class RenameGroupCommand extends SshCommand {
GroupResource rsrc = groups.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(groupName));
NameInput input = new NameInput();
input.name = newGroupName;
- putName.apply(rsrc, input);
+
+ @SuppressWarnings("unused")
+ var unused = putName.apply(rsrc, input);
} catch (RestApiException | IOException | ConfigInvalidException e) {
throw die(e);
}
diff --git a/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index c27167610e..b8c84069cc 100644
--- a/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -29,11 +29,13 @@ import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.changes.AbandonInput;
import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.Changes;
import com.google.gerrit.extensions.api.changes.MoveInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.RestoreInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
+import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.json.OutputFormat;
import com.google.gerrit.server.DynamicOptions;
@@ -56,11 +58,13 @@ import java.lang.reflect.AnnotatedElement;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
+import java.util.stream.Collectors;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.Option;
@@ -249,18 +253,40 @@ public class ReviewCommand extends SshCommand {
}
private void applyReview(PatchSet patchSet, ReviewInput review) throws Exception {
- retryHelper
- .action(
- ActionType.CHANGE_UPDATE,
- "applyReview",
- () -> {
- gApi.changes()
- .id(patchSet.id().changeId().get())
- .revision(patchSet.number())
- .review(review);
- return null;
- })
- .call();
+ Changes changesApi = gApi.changes();
+ int changeNumber = patchSet.id().changeId().get();
+ String projectName;
+ if (projectState == null) {
+ logger.atWarning().log(
+ "Deprecated usage of review command: missing project for change number %d, patchset %d",
+ changeNumber, patchSet.number());
+ List<ChangeInfo> changeInfos = changesApi.query("change: " + changeNumber).get();
+ if (changeInfos.size() > 1) {
+ throw die(
+ String.format(
+ "Multiple changes (%d) found for change number %d in projects: %s",
+ changeInfos.size(),
+ changeNumber,
+ changeInfos.stream().map(ci -> ci.project).collect(Collectors.joining(", "))));
+ }
+ projectName = changeInfos.get(0).project;
+ } else {
+ projectName = projectState.getProject().getName();
+ }
+ @SuppressWarnings("unused")
+ var unused =
+ retryHelper
+ .action(
+ ActionType.CHANGE_UPDATE,
+ "applyReview",
+ () -> {
+ changesApi
+ .id(projectName, changeNumber)
+ .revision(patchSet.number())
+ .review(review);
+ return null;
+ })
+ .call();
}
private ReviewInput reviewFromJson() throws UnloggedFailure {
diff --git a/java/com/google/gerrit/sshd/commands/SequenceCommandsModule.java b/java/com/google/gerrit/sshd/commands/SequenceCommandsModule.java
index e71624042b..48147a502f 100644
--- a/java/com/google/gerrit/sshd/commands/SequenceCommandsModule.java
+++ b/java/com/google/gerrit/sshd/commands/SequenceCommandsModule.java
@@ -20,6 +20,9 @@ import com.google.gerrit.sshd.Commands;
import com.google.gerrit.sshd.DispatchCommandProvider;
public class SequenceCommandsModule extends CommandModule {
+ public SequenceCommandsModule() {
+ super(/* slaveMode= */ false);
+ }
@Override
protected void configure() {
diff --git a/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index 0c286cad4e..4b8b8d1909 100644
--- a/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -247,7 +247,8 @@ final class SetAccountCommand extends SshCommand {
if (fullName != null) {
NameInput in = new NameInput();
in.name = fullName;
- putName.apply(rsrc, in);
+ @SuppressWarnings("unused")
+ var unused = putName.apply(rsrc, in);
}
if (httpPassword != null || clearHttpPassword || generateHttpPassword) {
@@ -263,10 +264,12 @@ final class SetAccountCommand extends SshCommand {
}
if (active) {
- putActive.apply(rsrc, null);
+ @SuppressWarnings("unused")
+ var unused = putActive.apply(rsrc, null);
} else if (inactive) {
try {
- deleteActive.apply(rsrc, null);
+ @SuppressWarnings("unused")
+ var unused = deleteActive.apply(rsrc, null);
} catch (ResourceNotFoundException e) {
// user is already inactive
}
@@ -297,7 +300,9 @@ final class SetAccountCommand extends SshCommand {
for (String sshKey : sshKeys) {
SshKeyInput in = new SshKeyInput();
in.raw = RawInputUtil.create(sshKey.getBytes(UTF_8), "text/plain");
- addSshKey.apply(rsrc, in);
+
+ @SuppressWarnings("unused")
+ var unused = addSshKey.apply(rsrc, in);
}
}
@@ -322,7 +327,10 @@ final class SetAccountCommand extends SshCommand {
throws AuthException, RepositoryNotFoundException, IOException, ConfigInvalidException,
PermissionBackendException {
AccountSshKey sshKey = AccountSshKey.create(user.getAccountId(), i.seq, i.sshPublicKey);
- deleteSshKey.apply(new AccountResource.SshKey(user.asIdentifiedUser(), sshKey), null);
+
+ @SuppressWarnings("unused")
+ var unused =
+ deleteSshKey.apply(new AccountResource.SshKey(user.asIdentifiedUser(), sshKey), null);
}
private void addEmail(String email)
@@ -332,7 +340,8 @@ final class SetAccountCommand extends SshCommand {
in.email = email;
in.noConfirmation = true;
try {
- createEmail.apply(rsrc, IdString.fromDecoded(email), in);
+ @SuppressWarnings("unused")
+ var unused = createEmail.apply(rsrc, IdString.fromDecoded(email), in);
} catch (EmailException e) {
throw die(e.getMessage());
}
@@ -342,17 +351,24 @@ final class SetAccountCommand extends SshCommand {
if (email.equals("ALL")) {
List<EmailInfo> emails = getEmails.apply(rsrc).value();
for (EmailInfo e : emails) {
- deleteEmail.apply(new AccountResource.Email(user.asIdentifiedUser(), e.email), new Input());
+ @SuppressWarnings("unused")
+ var unused =
+ deleteEmail.apply(
+ new AccountResource.Email(user.asIdentifiedUser(), e.email), new Input());
}
} else {
- deleteEmail.apply(new AccountResource.Email(user.asIdentifiedUser(), email), new Input());
+ @SuppressWarnings("unused")
+ var unused =
+ deleteEmail.apply(new AccountResource.Email(user.asIdentifiedUser(), email), new Input());
}
}
private void putPreferred(String email) throws Exception {
for (EmailInfo e : getEmails.apply(rsrc).value()) {
if (e.email.equals(email)) {
- putPreferred.apply(new AccountResource.Email(user.asIdentifiedUser(), email), null);
+ @SuppressWarnings("unused")
+ var unused =
+ putPreferred.apply(new AccountResource.Email(user.asIdentifiedUser(), email), null);
return;
}
}
@@ -390,6 +406,8 @@ final class SetAccountCommand extends SshCommand {
} else {
ids = Collections.singletonList(externalId);
}
- deleteExternalIds.apply(rsrc, ids);
+
+ @SuppressWarnings("unused")
+ var unused = deleteExternalIds.apply(rsrc, ids);
}
}
diff --git a/java/com/google/gerrit/sshd/commands/SetHeadCommand.java b/java/com/google/gerrit/sshd/commands/SetHeadCommand.java
index b6d283eb02..5624252d60 100644
--- a/java/com/google/gerrit/sshd/commands/SetHeadCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetHeadCommand.java
@@ -47,7 +47,8 @@ public class SetHeadCommand extends SshCommand {
HeadInput input = new HeadInput();
input.ref = newHead;
try {
- setHead.apply(new ProjectResource(project, user), input);
+ @SuppressWarnings("unused")
+ var unused = setHead.apply(new ProjectResource(project, user), input);
} catch (UnprocessableEntityException e) {
throw die(e);
}
diff --git a/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java b/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
index 4d16da6d5d..8395772476 100644
--- a/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
@@ -17,13 +17,14 @@ package com.google.gerrit.sshd.commands;
import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
import java.net.MalformedURLException;
import java.net.URL;
-import java.util.Collections;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
@@ -86,7 +87,7 @@ public class SetLoggingLevelCommand extends SshCommand {
}
@SuppressWarnings({"unchecked", "JdkObsolete"})
- private static Iterable<Logger> getCurrentLoggers() {
- return Collections.list(LogManager.getCurrentLoggers());
+ private static ImmutableList<Logger> getCurrentLoggers() {
+ return ImmutableList.copyOf(Iterators.forEnumeration(LogManager.getCurrentLoggers()));
}
}
diff --git a/java/com/google/gerrit/sshd/commands/SetMembersCommand.java b/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
index f788f14d48..a8894ebb46 100644
--- a/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
@@ -108,19 +108,27 @@ public class SetMembersCommand extends SshCommand {
GroupResource resource =
groupsCollection.parse(TopLevelResource.INSTANCE, IdString.fromUrl(groupUuid.get()));
if (!accountsToRemove.isEmpty()) {
- deleteMembers.apply(resource, fromMembers(accountsToRemove));
+ @SuppressWarnings("unused")
+ var unused = deleteMembers.apply(resource, fromMembers(accountsToRemove));
+
reportMembersAction("removed from", resource, accountsToRemove);
}
if (!groupsToRemove.isEmpty()) {
- deleteSubgroups.apply(resource, fromGroups(groupsToRemove));
+ @SuppressWarnings("unused")
+ var unused = deleteSubgroups.apply(resource, fromGroups(groupsToRemove));
+
reportGroupsAction("excluded from", resource, groupsToRemove);
}
if (!accountsToAdd.isEmpty()) {
- addMembers.apply(resource, fromMembers(accountsToAdd));
+ @SuppressWarnings("unused")
+ var unused = addMembers.apply(resource, fromMembers(accountsToAdd));
+
reportMembersAction("added to", resource, accountsToAdd);
}
if (!groupsToInclude.isEmpty()) {
- addSubgroups.apply(resource, fromGroups(groupsToInclude));
+ @SuppressWarnings("unused")
+ var unused = addSubgroups.apply(resource, fromGroups(groupsToInclude));
+
reportGroupsAction("included to", resource, groupsToInclude);
}
}
diff --git a/java/com/google/gerrit/sshd/commands/SetParentCommand.java b/java/com/google/gerrit/sshd/commands/SetParentCommand.java
index d23f7fa218..712715e4b5 100644
--- a/java/com/google/gerrit/sshd/commands/SetParentCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetParentCommand.java
@@ -122,7 +122,9 @@ final class SetParentCommand extends SshCommand {
final String name = nameKey.get();
ProjectState project = projectCache.get(nameKey).orElseThrow(illegalState(nameKey));
try {
- setParent.apply(new ProjectResource(project, user), parentInput(newParentKey.get()));
+ @SuppressWarnings("unused")
+ var unused =
+ setParent.apply(new ProjectResource(project, user), parentInput(newParentKey.get()));
} catch (AuthException e) {
err.append("error: insuffient access rights to change parent of '")
.append(name)
diff --git a/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
index 9866c4e4e8..188d2cd4a7 100644
--- a/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -150,7 +150,8 @@ final class SetProjectCommand extends SshCommand {
}
try {
- putConfig.apply(new ProjectResource(projectState, user), configInput);
+ @SuppressWarnings("unused")
+ var unused = putConfig.apply(new ProjectResource(projectState, user), configInput);
} catch (RestApiException | PermissionBackendException e) {
throw die(e);
}
diff --git a/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index f42eb5cb53..b1745b4c3c 100644
--- a/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -125,15 +125,18 @@ public class SetReviewersCommand extends SshCommand {
ReviewerResource rsrc = reviewerFactory.create(changeRsrc, reviewer);
String error = null;
try {
- retryHelper
- .action(
- RetryableAction.ActionType.CHANGE_UPDATE,
- "removeReviewers",
- () -> {
- deleteReviewer.apply(rsrc, new DeleteReviewerInput());
- return null;
- })
- .call();
+ @SuppressWarnings("unused")
+ var unused =
+ retryHelper
+ .action(
+ RetryableAction.ActionType.CHANGE_UPDATE,
+ "removeReviewers",
+ () -> {
+ @SuppressWarnings("unused")
+ var unused2 = deleteReviewer.apply(rsrc, new DeleteReviewerInput());
+ return null;
+ })
+ .call();
} catch (ResourceNotFoundException e) {
error = String.format("could not remove %s: not found", reviewer);
} catch (Exception e) {
@@ -156,15 +159,17 @@ public class SetReviewersCommand extends SshCommand {
String value;
};
try {
- retryHelper
- .action(
- RetryableAction.ActionType.CHANGE_UPDATE,
- "applyReview",
- () -> {
- error.value = postReviewers.apply(changeRsrc, input).value().error;
- return null;
- })
- .call();
+ @SuppressWarnings("unused")
+ var unused =
+ retryHelper
+ .action(
+ RetryableAction.ActionType.CHANGE_UPDATE,
+ "applyReview",
+ () -> {
+ error.value = postReviewers.apply(changeRsrc, input).value().error;
+ return null;
+ })
+ .call();
} catch (Exception e) {
error.value = String.format("could not add %s: %s", reviewer, e.getMessage());
}
diff --git a/java/com/google/gerrit/sshd/commands/SetTopicCommand.java b/java/com/google/gerrit/sshd/commands/SetTopicCommand.java
index b5907404ba..6ff56862b9 100644
--- a/java/com/google/gerrit/sshd/commands/SetTopicCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetTopicCommand.java
@@ -82,7 +82,8 @@ public class SetTopicCommand extends SshCommand {
TopicInput input = new TopicInput();
input.topic = topic;
try {
- putTopic.apply(r, input);
+ @SuppressWarnings("unused")
+ var unused = putTopic.apply(r, input);
} catch (ResourceNotFoundException e) {
ok = false;
writeError(
diff --git a/java/com/google/gerrit/sshd/plugin/LfsPluginAuthCommand.java b/java/com/google/gerrit/sshd/plugin/LfsPluginAuthCommand.java
index 8ba673a4c3..197354cb6f 100644
--- a/java/com/google/gerrit/sshd/plugin/LfsPluginAuthCommand.java
+++ b/java/com/google/gerrit/sshd/plugin/LfsPluginAuthCommand.java
@@ -41,6 +41,7 @@ public class LfsPluginAuthCommand extends SshCommand {
@Inject
LfsPluginAuthCommandModule(@GerritServerConfig Config cfg) {
+ super(/* slaveMode= */ false);
pluginProvided = cfg.getString("lfs", null, "plugin") != null;
}
diff --git a/java/com/google/gerrit/sshd/plugin/package-info.java b/java/com/google/gerrit/sshd/plugin/package-info.java
new file mode 100644
index 0000000000..189effebcb
--- /dev/null
+++ b/java/com/google/gerrit/sshd/plugin/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.sshd.plugin;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index 95c9b13d47..33f866830e 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -67,6 +67,9 @@ java_library(
name = "gerrit-junit",
srcs = ["GerritJUnit.java"],
visibility = ["//visibility:public"],
+ deps = [
+ "//lib/errorprone:annotations",
+ ],
)
java_library(
diff --git a/java/com/google/gerrit/testing/FakeAccountCache.java b/java/com/google/gerrit/testing/FakeAccountCache.java
index 2c01548a60..330b602df9 100644
--- a/java/com/google/gerrit/testing/FakeAccountCache.java
+++ b/java/com/google/gerrit/testing/FakeAccountCache.java
@@ -43,6 +43,7 @@ public class FakeAccountCache implements AccountCache {
return newState(
Account.builder(accountId, TimeUtil.now())
.setMetaId("1234567812345678123456781234567812345678")
+ .setUniqueTag("1234567812345678123456781234567812345678")
.build());
}
diff --git a/java/com/google/gerrit/testing/FakeEmailSender.java b/java/com/google/gerrit/testing/FakeEmailSender.java
index a2ebc888c9..42ffec26bd 100644
--- a/java/com/google/gerrit/testing/FakeEmailSender.java
+++ b/java/com/google/gerrit/testing/FakeEmailSender.java
@@ -14,6 +14,7 @@
package com.google.gerrit.testing;
+import static com.google.gerrit.common.UsedAt.Project.GOOGLE;
import static java.util.stream.Collectors.toList;
import com.google.auto.value.AutoValue;
@@ -21,6 +22,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.Address;
import com.google.gerrit.entities.EmailHeader;
import com.google.gerrit.exceptions.EmailException;
@@ -59,7 +61,8 @@ public class FakeEmailSender implements EmailSender {
@AutoValue
public abstract static class Message {
- private static Message create(
+ @UsedAt(GOOGLE)
+ public static Message create(
Address from,
Collection<Address> rcpt,
Map<String, EmailHeader> headers,
@@ -183,7 +186,8 @@ public class FakeEmailSender implements EmailSender {
for (WorkQueue.Task<?> task : workQueue.getTasks()) {
if (task.toString().contains("send-email")) {
try {
- task.get();
+ @SuppressWarnings("unused")
+ var unused = task.get();
} catch (ExecutionException | InterruptedException e) {
logger.atWarning().withCause(e).log("error finishing email task");
}
diff --git a/java/com/google/gerrit/testing/GerritJUnit.java b/java/com/google/gerrit/testing/GerritJUnit.java
index e80afa968c..87a736cbe7 100644
--- a/java/com/google/gerrit/testing/GerritJUnit.java
+++ b/java/com/google/gerrit/testing/GerritJUnit.java
@@ -14,6 +14,8 @@
package com.google.gerrit.testing;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
/** Static JUnit utility methods. */
public class GerritJUnit {
/**
@@ -36,6 +38,7 @@ public class GerritJUnit {
* @param runnable runnable containing arbitrary code.
* @return exception that was thrown.
*/
+ @CanIgnoreReturnValue
public static <T extends Throwable> T assertThrows(
Class<T> throwableClass, ThrowingRunnable runnable) {
try {
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index 3f0f8b7639..b0045e31f2 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -16,11 +16,11 @@ package com.google.gerrit.testing;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService;
-import static com.google.gerrit.server.Sequence.LightweightGroups;
import static com.google.inject.Scopes.SINGLETON;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
import com.google.gerrit.acceptance.testsuite.group.GroupOperationsImpl;
@@ -46,6 +46,7 @@ import com.google.gerrit.server.GerritPersonIdentProvider;
import com.google.gerrit.server.LibModuleType;
import com.google.gerrit.server.PluginUser;
import com.google.gerrit.server.Sequence;
+import com.google.gerrit.server.Sequence.LightweightGroups;
import com.google.gerrit.server.account.AccountCacheImpl;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.storage.notedb.AccountNoteDbReadStorageModule;
@@ -101,6 +102,8 @@ import com.google.gerrit.server.index.group.GroupIndexCollection;
import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
import com.google.gerrit.server.mail.EmailModule;
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier.SignedTokenEmailTokenVerifierModule;
+import com.google.gerrit.server.notedb.NoteDbDraftCommentsModule;
+import com.google.gerrit.server.notedb.NoteDbStarredChangesModule;
import com.google.gerrit.server.notedb.RepoSequence.DisabledGitRefUpdatedRepoGroupsSequenceProvider;
import com.google.gerrit.server.notedb.RepoSequence.RepoSequenceModule;
import com.google.gerrit.server.patch.DiffExecutor;
@@ -132,7 +135,6 @@ import com.google.inject.util.Providers;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
@@ -199,10 +201,14 @@ public class InMemoryModule extends FactoryModule {
bind(MetricMaker.class).to(DisabledMetricMaker.class);
install(cfgInjector.getInstance(AccountCacheImpl.AccountCacheModule.class));
+ install(cfgInjector.getInstance(AccountCacheImpl.AccountCacheBindingModule.class));
+
install(cfgInjector.getInstance(GerritGlobalModule.class));
install(new AccountNoteDbWriteStorageModule());
install(new AccountNoteDbReadStorageModule());
install(new RepoSequenceModule());
+ install(new NoteDbDraftCommentsModule());
+ install(new NoteDbStarredChangesModule());
AuthConfig authConfig = cfgInjector.getInstance(AuthConfig.class);
install(new AuthModule(authConfig));
@@ -223,7 +229,7 @@ public class InMemoryModule extends FactoryModule {
// It would be nice to use Jimfs for the SitePath, but the biggest blocker is that JGit does not
// support Path-based Configs, only FileBasedConfig.
- bind(Path.class).annotatedWith(SitePath.class).toInstance(Paths.get("."));
+ bind(Path.class).annotatedWith(SitePath.class).toInstance(Path.of("."));
bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(cfg);
bind(GerritOptions.class).toInstance(new GerritOptions(false, false));
bind(AllProjectsConfigProvider.class).to(FileBasedAllProjectsConfigProvider.class);
@@ -381,7 +387,8 @@ public class InMemoryModule extends FactoryModule {
try {
Class<?> clazz = Class.forName(moduleClassName);
Method m =
- clazz.getMethod("singleVersionWithExplicitVersions", Map.class, int.class, boolean.class);
+ clazz.getMethod(
+ "singleVersionWithExplicitVersions", ImmutableMap.class, int.class, boolean.class);
return (Module) m.invoke(null, getSingleSchemaVersions(), 0, ReplicaUtil.isReplica(cfg));
} catch (ClassNotFoundException
| SecurityException
@@ -394,13 +401,13 @@ public class InMemoryModule extends FactoryModule {
}
}
- private Map<String, Integer> getSingleSchemaVersions() {
+ private ImmutableMap<String, Integer> getSingleSchemaVersions() {
Map<String, Integer> singleVersions = new HashMap<>();
putSchemaVersion(singleVersions, AccountSchemaDefinitions.INSTANCE);
putSchemaVersion(singleVersions, ChangeSchemaDefinitions.INSTANCE);
putSchemaVersion(singleVersions, GroupSchemaDefinitions.INSTANCE);
putSchemaVersion(singleVersions, ProjectSchemaDefinitions.INSTANCE);
- return singleVersions;
+ return ImmutableMap.copyOf(singleVersions);
}
private void putSchemaVersion(
diff --git a/java/com/google/gerrit/testing/InMemoryRepositoryManager.java b/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
index 8c87405a37..2c00acd5f7 100644
--- a/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
+++ b/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
@@ -107,7 +107,7 @@ public class InMemoryRepositoryManager implements GitRepositoryManager {
.addSpecialRef(RefNames::isSequenceRef, REPO_SEQ)
.addSpecialRef(RefNames.HEAD::equals, HEAD_MODIFICATION)
.addSpecialRef(RefNames::isRefsChanges, CHANGE_MODIFICATION, MERGE_CHANGE)
- .addSpecialRef(RefNames::isAutoMergeRef, CHANGE_MODIFICATION)
+ .addSpecialRef(RefNames::isAutoMergeRef, CHANGE_MODIFICATION, MERGE_CHANGE)
.addSpecialRef(RefNames::isRefsEdit, CHANGE_MODIFICATION, MERGE_CHANGE)
.addSpecialRef(RefNames::isTagRef, TAG_MODIFICATION)
.addSpecialRef(RefNames::isRejectCommitsRef, BAN_COMMIT)
@@ -253,7 +253,8 @@ public class InMemoryRepositoryManager implements GitRepositoryManager {
@Override
public synchronized Status getRepositoryStatus(NameKey name) {
try {
- get(name);
+ @SuppressWarnings("unused")
+ var unused = get(name);
return Status.ACTIVE;
} catch (RepositoryNotFoundException e) {
return Status.NON_EXISTENT;
diff --git a/java/com/google/gerrit/testing/InMemoryTestEnvironment.java b/java/com/google/gerrit/testing/InMemoryTestEnvironment.java
index 77df46ce0f..ef65b61e81 100644
--- a/java/com/google/gerrit/testing/InMemoryTestEnvironment.java
+++ b/java/com/google/gerrit/testing/InMemoryTestEnvironment.java
@@ -84,7 +84,9 @@ public final class InMemoryTestEnvironment implements MethodRule {
public void setApiUser(Account.Id id) {
IdentifiedUser user = userFactory.create(id);
- requestContext.setContext(() -> user);
+
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(() -> user);
}
private void setUp(Object target) throws Exception {
@@ -113,7 +115,8 @@ public final class InMemoryTestEnvironment implements MethodRule {
lifecycle.stop();
}
if (requestContext != null) {
- requestContext.setContext(null);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(null);
}
}
}
diff --git a/java/com/google/gerrit/testing/TestCommentHelper.java b/java/com/google/gerrit/testing/TestCommentHelper.java
index 400b559a0c..7db26e3e95 100644
--- a/java/com/google/gerrit/testing/TestCommentHelper.java
+++ b/java/com/google/gerrit/testing/TestCommentHelper.java
@@ -21,6 +21,7 @@ import com.google.gerrit.entities.Change;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
import com.google.gerrit.extensions.client.Comment;
import com.google.gerrit.extensions.client.Comment.Range;
@@ -143,6 +144,22 @@ public class TestCommentHelper {
return in;
}
+ public static CommentInput createCommentInputWithMandatoryFields(String path) {
+ CommentInput in = new CommentInput();
+ in.message = "nit: trailing whitespace";
+ in.path = path;
+ return in;
+ }
+
+ public static CommentInput createCommentInput(
+ String path, FixSuggestionInfo... fixSuggestionInfos) {
+ CommentInput in = new CommentInput();
+ in.message = "nit: trailing whitespace";
+ in.path = path;
+ in.fixSuggestions = Arrays.asList(fixSuggestionInfos);
+ return in;
+ }
+
public void addRobotComment(String targetChangeId, RobotCommentInput robotCommentInput)
throws Exception {
addRobotComment(targetChangeId, robotCommentInput, "robot comment test");
@@ -174,4 +191,23 @@ public class TestCommentHelper {
reviewInput.tag = ChangeMessagesUtil.AUTOGENERATED_TAG_PREFIX;
return reviewInput;
}
+
+ public void addComment(String targetChangeId, CommentInput commentInput) throws Exception {
+ addComment(targetChangeId, commentInput, "comment test");
+ }
+
+ public void addComment(String targetChangeId, CommentInput commentInput, String message)
+ throws Exception {
+ ReviewInput reviewInput = createReviewInput(commentInput, message);
+ gApi.changes().id(targetChangeId).current().review(reviewInput);
+ }
+
+ private ReviewInput createReviewInput(CommentInput commentInput, String message) {
+ ReviewInput reviewInput = new ReviewInput();
+ reviewInput.comments =
+ Collections.singletonMap(commentInput.path, ImmutableList.of(commentInput));
+ reviewInput.message = message;
+ reviewInput.tag = ChangeMessagesUtil.AUTOGENERATED_TAG_PREFIX;
+ return reviewInput;
+ }
}
diff --git a/java/com/google/gerrit/testing/package-info.java b/java/com/google/gerrit/testing/package-info.java
new file mode 100644
index 0000000000..2b83996387
--- /dev/null
+++ b/java/com/google/gerrit/testing/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.testing;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/truth/BUILD b/java/com/google/gerrit/truth/BUILD
index b0dfef0cb1..2f3411dda4 100644
--- a/java/com/google/gerrit/truth/BUILD
+++ b/java/com/google/gerrit/truth/BUILD
@@ -9,6 +9,7 @@ java_library(
"//java/com/google/gerrit/common:annotations",
"//lib:guava",
"//lib:jgit",
+ "//lib/errorprone:annotations",
"//lib/truth",
],
)
diff --git a/java/com/google/gerrit/truth/package-info.java b/java/com/google/gerrit/truth/package-info.java
new file mode 100644
index 0000000000..7afe587c20
--- /dev/null
+++ b/java/com/google/gerrit/truth/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.truth;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/util/cli/BUILD b/java/com/google/gerrit/util/cli/BUILD
index b464f32d2f..f62aea90cd 100644
--- a/java/com/google/gerrit/util/cli/BUILD
+++ b/java/com/google/gerrit/util/cli/BUILD
@@ -12,6 +12,7 @@ java_library(
"//lib:args4j",
"//lib:guava",
"//lib/auto:auto-value-annotations",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
"//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/util/cli/CmdLineParser.java b/java/com/google/gerrit/util/cli/CmdLineParser.java
index a37c0273ae..da8d5d99cb 100644
--- a/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -44,6 +44,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -462,6 +463,7 @@ public class CmdLineParser {
ensureOptionsInitialized();
}
+ @CanIgnoreReturnValue
public int addOptionsWithMetRequirements() {
int count = 0;
for (Iterator<Map.Entry<String, QueuedOption>> it = queuedOptionsByName.entrySet().iterator();
diff --git a/java/com/google/gerrit/util/cli/package-info.java b/java/com/google/gerrit/util/cli/package-info.java
new file mode 100644
index 0000000000..585ea00329
--- /dev/null
+++ b/java/com/google/gerrit/util/cli/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.util.cli;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/util/http/BUILD b/java/com/google/gerrit/util/http/BUILD
index afb4e25024..1edc46d1b6 100644
--- a/java/com/google/gerrit/util/http/BUILD
+++ b/java/com/google/gerrit/util/http/BUILD
@@ -7,5 +7,6 @@ java_library(
deps = [
"//lib:guava",
"//lib:servlet-api",
+ "//lib/errorprone:annotations",
],
)
diff --git a/java/com/google/gerrit/util/http/package-info.java b/java/com/google/gerrit/util/http/package-info.java
new file mode 100644
index 0000000000..e0b8f62fb4
--- /dev/null
+++ b/java/com/google/gerrit/util/http/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.util.http;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/util/logging/BUILD b/java/com/google/gerrit/util/logging/BUILD
index ee598a436f..328f0e3379 100644
--- a/java/com/google/gerrit/util/logging/BUILD
+++ b/java/com/google/gerrit/util/logging/BUILD
@@ -8,6 +8,7 @@ java_library(
visibility = ["//visibility:public"],
deps = [
"//lib:gson",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/log:log4j",
],
diff --git a/java/com/google/gerrit/util/logging/package-info.java b/java/com/google/gerrit/util/logging/package-info.java
new file mode 100644
index 0000000000..d79e8bb17b
--- /dev/null
+++ b/java/com/google/gerrit/util/logging/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.util.logging;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/util/ssl/BUILD b/java/com/google/gerrit/util/ssl/BUILD
index e0641c73c0..b7d9f85af1 100644
--- a/java/com/google/gerrit/util/ssl/BUILD
+++ b/java/com/google/gerrit/util/ssl/BUILD
@@ -4,4 +4,7 @@ java_library(
name = "ssl",
srcs = glob(["**/*.java"]),
visibility = ["//visibility:public"],
+ deps = [
+ "//lib/errorprone:annotations",
+ ],
)
diff --git a/java/com/google/gerrit/util/ssl/package-info.java b/java/com/google/gerrit/util/ssl/package-info.java
new file mode 100644
index 0000000000..161b7702b8
--- /dev/null
+++ b/java/com/google/gerrit/util/ssl/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package com.google.gerrit.util.ssl;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/gerrit/BUILD b/java/gerrit/BUILD
index 6923c3df46..b6d83259a7 100644
--- a/java/gerrit/BUILD
+++ b/java/gerrit/BUILD
@@ -11,6 +11,7 @@ java_library(
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server/rules/prolog",
"//lib:jgit",
+ "//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/prolog:runtime",
"@guava//jar",
diff --git a/java/gerrit/package-info.java b/java/gerrit/package-info.java
new file mode 100644
index 0000000000..254a10e956
--- /dev/null
+++ b/java/gerrit/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package gerrit;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/org/apache/commons/net/BUILD b/java/org/apache/commons/net/BUILD
index d83d8ec7f8..c5bf47be06 100644
--- a/java/org/apache/commons/net/BUILD
+++ b/java/org/apache/commons/net/BUILD
@@ -8,5 +8,6 @@ java_library(
"//java/com/google/gerrit/util/ssl",
"//lib:guava",
"//lib/commons:net",
+ "//lib/errorprone:annotations",
],
)
diff --git a/java/org/apache/commons/net/smtp/package-info.java b/java/org/apache/commons/net/smtp/package-info.java
new file mode 100644
index 0000000000..f3d4db1644
--- /dev/null
+++ b/java/org/apache/commons/net/smtp/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+@CheckReturnValue
+package org.apache.commons.net.smtp;
+
+import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java b/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
index 33e6692f77..e85a032864 100644
--- a/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
+++ b/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
@@ -21,6 +21,7 @@ import static org.mockito.Mockito.only;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Project;
@@ -79,7 +80,7 @@ public class ProjectResetterTest {
Ref matchingRef = createRef("refs/any/test");
try (ProjectResetter resetProject =
- builder().build(new ProjectResetter.Config().reset(project))) {
+ builder().build(new ProjectResetter.Config.Builder().reset(project).build())) {
updateRef(matchingRef);
}
@@ -97,8 +98,9 @@ public class ProjectResetterTest {
try (ProjectResetter resetProject =
builder()
.build(
- new ProjectResetter.Config()
- .reset(project, "refs/match/*", "refs/another-match/*"))) {
+ new ProjectResetter.Config.Builder()
+ .reset(project, "refs/match/*", "refs/another-match/*")
+ .build())) {
updateRef(matchingRef);
updateRef(anotherMatchingRef);
updatedNonMatchingRef = updateRef(nonMatchingRef);
@@ -120,8 +122,9 @@ public class ProjectResetterTest {
try (ProjectResetter resetProject =
builder()
.build(
- new ProjectResetter.Config()
- .reset(project, "refs/match/*", "refs/another-match/*"))) {
+ new ProjectResetter.Config.Builder()
+ .reset(project, "refs/match/*", "refs/another-match/*")
+ .build())) {
matchingRef = createRef("refs/match/test");
anotherMatchingRef = createRef("refs/another-match/test");
nonMatchingRef = createRef("refs/no-match/test");
@@ -151,9 +154,10 @@ public class ProjectResetterTest {
try (ProjectResetter resetProject =
builder()
.build(
- new ProjectResetter.Config()
+ new ProjectResetter.Config.Builder()
.reset(project, "refs/foo/*")
- .reset(project2, "refs/bar/*"))) {
+ .reset(project2, "refs/bar/*")
+ .build())) {
updateRef(matchingRefProject1);
updatedNonMatchingRefProject1 = updateRef(nonMatchingRefProject1);
@@ -182,9 +186,10 @@ public class ProjectResetterTest {
try (ProjectResetter resetProject =
builder()
.build(
- new ProjectResetter.Config()
+ new ProjectResetter.Config.Builder()
.reset(project, "refs/foo/*")
- .reset(project2, "refs/bar/*"))) {
+ .reset(project2, "refs/bar/*")
+ .build())) {
matchingRefProject1 = createRef("refs/foo/test");
nonMatchingRefProject1 = createRef("refs/bar/test");
@@ -207,7 +212,9 @@ public class ProjectResetterTest {
try (ProjectResetter resetProject =
builder()
.build(
- new ProjectResetter.Config().reset(project, "refs/match/*", "refs/match/test"))) {
+ new ProjectResetter.Config.Builder()
+ .reset(project, "refs/match/*", "refs/match/test")
+ .build())) {
// This ref matches 2 ref pattern, ProjectResetter should try to delete it only once.
matchingRef = createRef("refs/match/test");
}
@@ -228,7 +235,7 @@ public class ProjectResetterTest {
try (ProjectResetter resetProject =
builder(null, null, null, null, null, null, projectCache)
- .build(new ProjectResetter.Config().reset(project).reset(project2))) {
+ .build(new ProjectResetter.Config.Builder().reset(project).reset(project2).build())) {
updateRef(nonMetaConfig);
updateRef(repo2, metaConfig);
}
@@ -245,7 +252,7 @@ public class ProjectResetterTest {
try (ProjectResetter resetProject =
builder(null, null, null, null, null, null, projectCache)
- .build(new ProjectResetter.Config().reset(project).reset(project2))) {
+ .build(new ProjectResetter.Config.Builder().reset(project).reset(project2).build())) {
createRef("refs/heads/master");
createRef(repo2, RefNames.REFS_CONFIG);
}
@@ -269,7 +276,7 @@ public class ProjectResetterTest {
Ref ref2 = createRef(allUsersRepo, RefNames.refsGroups(uuid2));
try (ProjectResetter resetProject =
builder(null, null, null, cache, includeCache, indexer, null)
- .build(new ProjectResetter.Config().reset(project).reset(allUsers))) {
+ .build(new ProjectResetter.Config.Builder().reset(project).reset(allUsers).build())) {
updateRef(allUsersRepo, ref2);
createRef(allUsersRepo, RefNames.refsGroups(uuid3));
}
@@ -283,10 +290,12 @@ public class ProjectResetterTest {
verifyNoMoreInteractions(cache, indexer, includeCache);
}
+ @CanIgnoreReturnValue
private Ref createRef(String ref) throws IOException {
return createRef(repo, ref);
}
+ @CanIgnoreReturnValue
private Ref createRef(Repository repo, String ref) throws IOException {
try (RefUpdateContext ctx = openTestRefUpdateContext()) {
try (ObjectInserter oi = repo.newObjectInserter();
@@ -301,10 +310,12 @@ public class ProjectResetterTest {
}
}
+ @CanIgnoreReturnValue
private Ref updateRef(Ref ref) throws IOException {
return updateRef(repo, ref);
}
+ @CanIgnoreReturnValue
private Ref updateRef(Repository repo, Ref ref) throws IOException {
try (RefUpdateContext ctx = openTestRefUpdateContext()) {
try (ObjectInserter oi = repo.newObjectInserter();
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index f3bb15b094..fdf7457626 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -15,10 +15,10 @@
package com.google.gerrit.acceptance.api.accounts;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.GitUtil.deleteRef;
import static com.google.gerrit.acceptance.GitUtil.fetch;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
@@ -36,7 +36,6 @@ import static com.google.gerrit.server.account.AccountProperties.ACCOUNT;
import static com.google.gerrit.server.account.AccountProperties.ACCOUNT_CONFIG;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
-import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
@@ -54,9 +53,11 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
+import com.github.rholder.retry.RetryException;
import com.github.rholder.retry.StopStrategies;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Iterables;
@@ -145,7 +146,9 @@ import com.google.gerrit.server.account.GroupMembership;
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
+import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdFactoryNoteDbImpl;
import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdsNoteDbImpl;
@@ -237,26 +240,26 @@ public class AccountIT extends AbstractDaemonTest {
@Inject protected ProjectOperations projectOperations;
@Inject protected Emails emails;
+ @Inject protected ExtensionRegistry extensionRegistry;
+ @Inject protected RequestScopeOperations requestScopeOperations;
@Inject protected GroupOperations groupOperations;
@Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
@Inject private AccountIndexer accountIndexer;
@Inject private ExternalIdNotes.Factory extIdNotesFactory;
- @Inject private ExternalIdsNoteDbImpl externalIds;
+ @Inject private ExternalIdsNoteDbImpl externalIdsNoteDbImpl;
@Inject private GitReferenceUpdated gitReferenceUpdated;
@Inject private Provider<InternalAccountQuery> accountQueryProvider;
@Inject private Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory;
@Inject private Provider<PublicKeyStore> publicKeyStoreProvider;
- @Inject private RequestScopeOperations requestScopeOperations;
@Inject private RetryHelper.Metrics retryMetrics;
@Inject private Sequences seq;
@Inject private StalenessChecker stalenessChecker;
@Inject private VersionedAuthorizedKeys.Accessor authorizedKeys;
- @Inject private ExtensionRegistry extensionRegistry;
@Inject private PluginSetContext<ExceptionHook> exceptionHooks;
@Inject private ExternalIdKeyFactory externalIdKeyFactory;
- @Inject private ExternalIdFactoryNoteDbImpl externalIdFactory;
+ @Inject private ExternalIdFactoryNoteDbImpl externalIdFactoryNoteDbImpl;
@Inject private AuthConfig authConfig;
@Inject private AccountControl.Factory accountControlFactory;
@Inject private AccountOperations accountOperations;
@@ -327,7 +330,7 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void createByAccountCreator() throws Exception {
- RefUpdateCounter refUpdateCounter = new RefUpdateCounter();
+ RefUpdateCounter refUpdateCounter = createRefUpdateCounter();
try (Registration registration = extensionRegistry.newRegistration().add(refUpdateCounter)) {
Account.Id accountId = createByAccountCreator(1);
refUpdateCounter.assertRefUpdateFor(
@@ -353,14 +356,23 @@ public class AccountIT extends AbstractDaemonTest {
}
@UsedAt(UsedAt.Project.GOOGLE)
+ protected AccountIndexedCounter getAccountIndexedCounter() {
+ return new AccountIndexedCounter();
+ }
+
+ @UsedAt(UsedAt.Project.GOOGLE)
protected Account.Id createByAccountCreator(int expectedAccountReindexCalls) throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
String name = "foo";
TestAccount foo = accountCreator.create(name);
AccountInfo info = gApi.accounts().id(foo.id().get()).get();
- assertThat(info.username).isEqualTo(name);
+ if (server.isUsernameSupported()) {
+ assertThat(info.username).isEqualTo(name);
+ } else {
+ assertThat(info.email).isEqualTo(foo.email());
+ }
assertThat(info.name).isEqualTo(name);
accountIndexedCounter.assertReindexOf(foo, expectedAccountReindexCalls);
assertUserBranch(foo.id(), name, null);
@@ -370,7 +382,7 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void createAnonymousCowardByAccountCreator() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
TestAccount anonymousCoward = accountCreator.create();
@@ -381,7 +393,7 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void create() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
AccountInput input = new AccountInput();
@@ -398,10 +410,10 @@ public class AccountIT extends AbstractDaemonTest {
Account.Id accountId = Account.id(accountInfo._accountId);
accountIndexedCounter.assertReindexOf(accountId, 1);
- assertThat(externalIds.byAccount(accountId))
+ assertThat(getExternalIdsReader().byAccount(accountId))
.containsExactly(
- externalIdFactory.createUsername(input.username, accountId, null),
- externalIdFactory.createEmail(accountId, input.email));
+ getExternalIdFactory().createUsername(input.username, accountId, null),
+ getExternalIdFactory().createEmail(accountId, input.email));
}
}
@@ -454,7 +466,7 @@ public class AccountIT extends AbstractDaemonTest {
public void createAtomically() throws Exception {
Account.Id accountId = Account.id(seq.nextAccountId());
String fullName = "Foo";
- ExternalId extId = externalIdFactory.createEmail(accountId, "foo@example.com");
+ ExternalId extId = getExternalIdFactory().createEmail(accountId, "foo@example.com");
AccountState accountState =
accountsUpdateProvider
.get()
@@ -546,23 +558,25 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void get() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
- AccountInfo info = gApi.accounts().id("admin").get();
+ AccountInfo info = gApi.accounts().id(admin.id().get()).get();
assertThat(info.name).isEqualTo("Administrator");
assertThat(info.email).isEqualTo("admin@example.com");
- assertThat(info.username).isEqualTo("admin");
+ if (server.isUsernameSupported()) {
+ assertThat(info.username).isEqualTo("admin");
+ }
accountIndexedCounter.assertNoReindex();
}
}
@Test
public void getByIntId() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
- AccountInfo info = gApi.accounts().id("admin").get();
+ AccountInfo info = gApi.accounts().id(admin.id().get()).get();
AccountInfo infoByIntId = gApi.accounts().id(info._accountId).get();
assertThat(info.name).isEqualTo(infoByIntId.name);
accountIndexedCounter.assertNoReindex();
@@ -571,13 +585,13 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void self() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
AccountInfo info = gApi.accounts().self().get();
assertUser(info, admin);
- info = gApi.accounts().id("self").get();
+ info = gApi.accounts().self().get();
assertUser(info, admin);
accountIndexedCounter.assertNoReindex();
}
@@ -585,22 +599,22 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void active() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
- int id = gApi.accounts().id(user.username()).get()._accountId;
- assertThat(gApi.accounts().id(user.username()).getActive()).isTrue();
- gApi.accounts().id(user.username()).setActive(false);
+ int id = gApi.accounts().id(user.id().get()).get()._accountId;
+ assertThat(gApi.accounts().id(user.id().get()).getActive()).isTrue();
+ gApi.accounts().id(user.id().get()).setActive(false);
accountIndexedCounter.assertReindexOf(user);
// Inactive users may only be resolved by ID.
ResourceNotFoundException thrown =
- assertThrows(ResourceNotFoundException.class, () -> gApi.accounts().id(user.username()));
+ assertThrows(ResourceNotFoundException.class, () -> gApi.accounts().id(user.email()));
assertThat(thrown)
.hasMessageThat()
.isEqualTo(
"Account '"
- + user.username()
+ + user.email()
+ "' only matches inactive accounts. To use an inactive account, retry"
+ " with one of the following exact account IDs:\n"
+ id
@@ -608,35 +622,41 @@ public class AccountIT extends AbstractDaemonTest {
assertThat(gApi.accounts().id(id).getActive()).isFalse();
gApi.accounts().id(id).setActive(true);
- assertThat(gApi.accounts().id(user.username()).getActive()).isTrue();
+ assertThat(gApi.accounts().id(user.id().get()).getActive()).isTrue();
accountIndexedCounter.assertReindexOf(user);
}
}
@Test
public void shouldAllowQueryByEmailForInactiveUser() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
Account.Id activatableAccountId =
accountOperations.newAccount().inactive().preferredEmail("foo@activatable.com").create();
- accountIndexedCounter.assertReindexOf(activatableAccountId, 1);
+ // Implementation of the accountOperations can create an account in several steps,
+ // with more than one reindexing.
+ accountIndexedCounter.assertReindexAtLeastOnceOf(activatableAccountId);
}
- gApi.changes().query("owner:foo@activatable.com").get();
+ @SuppressWarnings("unused")
+ var unused = gApi.changes().query("owner:foo@activatable.com").get();
}
@Test
public void shouldAllowQueryByUserNameForInactiveUser() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
Account.Id activatableAccountId =
accountOperations.newAccount().inactive().username("foo").create();
- accountIndexedCounter.assertReindexOf(activatableAccountId, 1);
+ // Implementation of the accountOperations can create an account in several steps,
+ // with more than one reindexing.
+ accountIndexedCounter.assertReindexAtLeastOnceOf(activatableAccountId);
}
- gApi.changes().query("owner:foo").get();
+ @SuppressWarnings("unused")
+ var unused = gApi.changes().query("owner:foo").get();
}
@Test
@@ -778,9 +798,9 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void deactivateNotActive() throws Exception {
- int id = gApi.accounts().id(user.username()).get()._accountId;
- assertThat(gApi.accounts().id(user.username()).getActive()).isTrue();
- gApi.accounts().id(user.username()).setActive(false);
+ int id = gApi.accounts().id(user.id().get()).get()._accountId;
+ assertThat(gApi.accounts().id(user.id().get()).getActive()).isTrue();
+ gApi.accounts().id(user.id().get()).setActive(false);
assertThat(gApi.accounts().id(id).getActive()).isFalse();
ResourceConflictException thrown =
assertThrows(
@@ -791,8 +811,8 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void starUnstarChange() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
- RefUpdateCounter refUpdateCounter = new RefUpdateCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
+ RefUpdateCounter refUpdateCounter = createRefUpdateCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter).add(refUpdateCounter)) {
PushOneCommit.Result r = createChange();
@@ -828,7 +848,7 @@ public class AccountIT extends AbstractDaemonTest {
reviewerInput.reviewer = user.email();
input.reviewers.add(reviewerInput);
gApi.changes().id(r.getChangeId()).current().review(input);
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message message = messages.get(0);
assertThat(message.rcpt()).containsExactly(user.getNameEmail());
@@ -849,7 +869,7 @@ public class AccountIT extends AbstractDaemonTest {
input2.reviewers.add(reviewerInput3);
gApi.changes().id(r.getChangeId()).current().review(input2);
- List<Message> messages2 = sender.getMessages();
+ ImmutableList<Message> messages2 = sender.getMessages();
assertThat(messages2).hasSize(1);
Message message2 = messages2.get(0);
assertThat(message2.rcpt()).containsExactly(user2.getNameEmail());
@@ -869,7 +889,7 @@ public class AccountIT extends AbstractDaemonTest {
input3.reviewers.add(reviewerInput5);
gApi.changes().id(r.getChangeId()).current().review(input3);
- List<Message> messages3 = sender.getMessages();
+ ImmutableList<Message> messages3 = sender.getMessages();
assertThat(messages3).isEmpty();
}
@@ -881,7 +901,7 @@ public class AccountIT extends AbstractDaemonTest {
ReviewerInput reviewerInput = new ReviewerInput();
reviewerInput.reviewer = user.email();
gApi.changes().id(r.getChangeId()).addReviewer(reviewerInput);
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message message = messages.get(0);
assertThat(message.rcpt()).containsExactly(user.getNameEmail());
@@ -894,7 +914,7 @@ public class AccountIT extends AbstractDaemonTest {
ReviewerInput reviewerInput2 = new ReviewerInput();
reviewerInput2.reviewer = user2.email();
gApi.changes().id(r.getChangeId()).addReviewer(reviewerInput2);
- List<Message> messages2 = sender.getMessages();
+ ImmutableList<Message> messages2 = sender.getMessages();
assertThat(messages2).hasSize(1);
Message message2 = messages2.get(0);
assertThat(message2.rcpt()).containsExactly(user2.getNameEmail());
@@ -906,19 +926,22 @@ public class AccountIT extends AbstractDaemonTest {
ReviewerInput reviewerInput3 = new ReviewerInput();
reviewerInput3.reviewer = user2.email();
gApi.changes().id(r.getChangeId()).addReviewer(reviewerInput3);
- List<Message> messages3 = sender.getMessages();
+ ImmutableList<Message> messages3 = sender.getMessages();
assertThat(messages3).isEmpty();
}
@Test
public void suggestAccounts() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
String adminUsername = "admin";
List<AccountInfo> result = gApi.accounts().suggestAccounts().withQuery(adminUsername).get();
assertThat(result).hasSize(1);
- assertThat(result.get(0).username).isEqualTo(adminUsername);
+ if (server.isUsernameSupported()) {
+ assertThat(result.get(0).username).isEqualTo(adminUsername);
+ }
+ assertThat(result.get(0).email).isEqualTo("admin@example.com");
List<AccountInfo> resultShortcutApi = gApi.accounts().suggestAccounts(adminUsername).get();
assertThat(resultShortcutApi).hasSize(result.size());
@@ -946,7 +969,9 @@ public class AccountIT extends AbstractDaemonTest {
AccountDetailInfo detail = gApi.accounts().id(foo.id().get()).detail();
assertThat(detail._accountId).isEqualTo(foo.id().get());
assertThat(detail.name).isEqualTo(name);
- assertThat(detail.username).isEqualTo(username);
+ if (server.isUsernameSupported()) {
+ assertThat(detail.username).isEqualTo(username);
+ }
assertThat(detail.email).isEqualTo(email);
assertThat(detail.secondaryEmails).containsExactly(secondaryEmail);
assertThat(detail.status).isEqualTo(status);
@@ -1028,11 +1053,12 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void addEmail() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
- List<String> emails = ImmutableList.of("new.email@example.com", "new.email@example.systems");
- Set<String> currentEmails = getEmails();
+ ImmutableList<String> emails =
+ ImmutableList.of("new.email@example.com", "new.email@example.systems");
+ ImmutableSet<String> currentEmails = getEmails();
for (String email : emails) {
assertThat(currentEmails).doesNotContain(email);
EmailInput input = newEmailInput(email);
@@ -1047,7 +1073,7 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void addInvalidEmail() throws Exception {
- List<String> emails =
+ ImmutableList<String> emails =
ImmutableList.of(
// Missing domain part
"new.email",
@@ -1060,7 +1086,7 @@ public class AccountIT extends AbstractDaemonTest {
// Non-supported TLD (see tlds-alpha-by-domain.txt)
"new.email@example.africa");
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
for (String email : emails) {
@@ -1078,7 +1104,7 @@ public class AccountIT extends AbstractDaemonTest {
TestAccount account = accountCreator.create(name("user"));
EmailInput input = newEmailInput("test@example.com");
requestScopeOperations.setApiUser(user.id());
- assertThrows(AuthException.class, () -> gApi.accounts().id(account.username()).addEmail(input));
+ assertThrows(AuthException.class, () -> gApi.accounts().id(account.id().get()).addEmail(input));
}
@Test
@@ -1089,7 +1115,7 @@ public class AccountIT extends AbstractDaemonTest {
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class,
- () -> gApi.accounts().id(user.username()).addEmail(input));
+ () -> gApi.accounts().id(user.id().get()).addEmail(input));
assertThat(thrown)
.hasMessageThat()
.contains("Identity 'mailto:" + email + "' in use by another account");
@@ -1150,7 +1176,7 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void addEmailAndSetPreferred() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
String email = "foo.bar@example.com";
@@ -1171,7 +1197,7 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void deleteEmail() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
String email = "foo.bar@example.com";
@@ -1192,7 +1218,7 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void deletePreferredEmail() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
ImmutableSet<String> previous = getEmails();
@@ -1229,7 +1255,7 @@ public class AccountIT extends AbstractDaemonTest {
gApi.accounts().self().addEmail(input);
requestScopeOperations.resetCurrentApiUser();
- Set<String> allEmails = getEmails();
+ ImmutableSet<String> allEmails = getEmails();
assertThat(allEmails).hasSize(2);
for (String email : allEmails) {
@@ -1243,7 +1269,7 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void deleteEmailFromCustomExternalIdSchemes() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
String email = "foo.bar@example.com";
@@ -1256,11 +1282,13 @@ public class AccountIT extends AbstractDaemonTest {
admin.id(),
u ->
u.addExternalId(
- externalIdFactory.createWithEmail(
- externalIdKeyFactory.parse(extId1), admin.id(), email))
+ getExternalIdFactory()
+ .createWithEmail(
+ externalIdKeyFactory.parse(extId1), admin.id(), email))
.addExternalId(
- externalIdFactory.createWithEmail(
- externalIdKeyFactory.parse(extId2), admin.id(), email)));
+ getExternalIdFactory()
+ .createWithEmail(
+ externalIdKeyFactory.parse(extId2), admin.id(), email)));
accountIndexedCounter.assertReindexOf(admin);
assertThat(
gApi.accounts().self().getExternalIds().stream()
@@ -1296,8 +1324,9 @@ public class AccountIT extends AbstractDaemonTest {
admin.id(),
u ->
u.addExternalId(
- externalIdFactory.createWithEmail(
- externalIdKeyFactory.parse(ldapExternalId), admin.id(), ldapEmail)));
+ getExternalIdFactory()
+ .createWithEmail(
+ externalIdKeyFactory.parse(ldapExternalId), admin.id(), ldapEmail)));
assertThat(
gApi.accounts().self().getExternalIds().stream()
.map(e -> e.identity)
@@ -1335,13 +1364,17 @@ public class AccountIT extends AbstractDaemonTest {
admin.id(),
u ->
u.addExternalId(
- externalIdFactory.createWithEmail(
- externalIdKeyFactory.parse(nonLdapExternalId),
- admin.id(),
- nonLdapEMail))
+ getExternalIdFactory()
+ .createWithEmail(
+ externalIdKeyFactory.parse(nonLdapExternalId),
+ admin.id(),
+ nonLdapEMail))
.addExternalId(
- externalIdFactory.createWithEmail(
- externalIdKeyFactory.parse(ldapExternalId), admin.id(), ldapEmail)));
+ getExternalIdFactory()
+ .createWithEmail(
+ externalIdKeyFactory.parse(ldapExternalId),
+ admin.id(),
+ ldapEmail)));
assertThat(
gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
.containsAtLeast(ldapExternalId, nonLdapExternalId);
@@ -1360,7 +1393,7 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void deleteEmailOfOtherUser() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
String email = "foo.bar@example.com";
@@ -1404,8 +1437,9 @@ public class AccountIT extends AbstractDaemonTest {
admin.id(),
u ->
u.addExternalId(
- externalIdFactory.createWithEmail(
- externalIdKeyFactory.parse("foo:bar"), admin.id(), email)));
+ getExternalIdFactory()
+ .createWithEmail(
+ externalIdKeyFactory.parse("foo:bar"), admin.id(), email)));
assertEmail(emails.getAccountFor(email), admin);
// wrong case doesn't match
@@ -1451,9 +1485,9 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void putStatus() throws Exception {
- List<String> statuses = ImmutableList.of("OOO", "Busy");
+ ImmutableList<String> statuses = ImmutableList.of("OOO", "Busy");
AccountInfo info;
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
for (String status : statuses) {
@@ -1478,8 +1512,8 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void adminCanSetNameOfOtherUser() throws Exception {
- gApi.accounts().id(user.username()).setName("User McUserface");
- assertThat(gApi.accounts().id(user.username()).get().name).isEqualTo("User McUserface");
+ gApi.accounts().id(user.id().get()).setName("User McUserface");
+ assertThat(gApi.accounts().id(user.id().get()).get().name).isEqualTo("User McUserface");
}
@Test
@@ -1487,7 +1521,7 @@ public class AccountIT extends AbstractDaemonTest {
requestScopeOperations.setApiUser(user.id());
assertThrows(
AuthException.class,
- () -> gApi.accounts().id(admin.username()).setName("Admin McAdminface"));
+ () -> gApi.accounts().id(admin.id().get()).setName("Admin McAdminface"));
}
@Test
@@ -1497,13 +1531,13 @@ public class AccountIT extends AbstractDaemonTest {
.allProjectsForUpdate()
.add(allowCapability(GlobalCapability.MODIFY_ACCOUNT).group(REGISTERED_USERS))
.update();
- gApi.accounts().id(admin.username()).setName("Admin McAdminface");
- assertThat(gApi.accounts().id(admin.username()).get().name).isEqualTo("Admin McAdminface");
+ gApi.accounts().id(admin.id().get()).setName("Admin McAdminface");
+ assertThat(gApi.accounts().id(admin.id().get()).get().name).isEqualTo("Admin McAdminface");
}
@Test
public void fetchUserBranch() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
requestScopeOperations.setApiUser(user.id());
@@ -1710,7 +1744,7 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void addOtherUsersGpgKey_Conflict() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
// Both users have a matching external ID for this key.
@@ -1721,7 +1755,7 @@ public class AccountIT extends AbstractDaemonTest {
.update(
"Add External ID",
user.id(),
- u -> u.addExternalId(externalIdFactory.create("foo", "myId", user.id())));
+ u -> u.addExternalId(getExternalIdFactory().create("foo", "myId", user.id())));
accountIndexedCounter.assertReindexOf(user);
TestKey key = validKeyWithSecondUserId();
@@ -1739,10 +1773,10 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void listGpgKeys() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
- List<TestKey> keys = allValidKeys();
+ ImmutableList<TestKey> keys = allValidKeys();
List<String> toAdd = new ArrayList<>(keys.size());
for (TestKey key : keys) {
addExternalIdEmail(
@@ -1758,7 +1792,7 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void deleteGpgKey() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
TestKey key = validKeyWithoutExpiration();
@@ -1784,7 +1818,7 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void addAndRemoveGpgKeys() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
for (TestKey key : allValidKeys()) {
@@ -1843,7 +1877,7 @@ public class AccountIT extends AbstractDaemonTest {
@Test
@UseSsh
public void sshKeys() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
// The test account should initially have exactly one ssh key
@@ -1916,7 +1950,7 @@ public class AccountIT extends AbstractDaemonTest {
@Test
@UseSsh
public void adminCanAddOrRemoveSshKeyOnOtherAccount() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
// The test account should initially have exactly one ssh key
@@ -1932,8 +1966,8 @@ public class AccountIT extends AbstractDaemonTest {
// Add a new key
sender.clear();
String newKey = TestSshKeys.publicKey(SshSessionFactory.genSshKey(), user.email());
- gApi.accounts().id(user.username()).addSshKey(newKey);
- info = gApi.accounts().id(user.username()).listSshKeys();
+ gApi.accounts().id(user.id().get()).addSshKey(newKey);
+ info = gApi.accounts().id(user.id().get()).listSshKeys();
assertThat(info).hasSize(2);
assertSequenceNumbers(info);
accountIndexedCounter.assertReindexOf(user);
@@ -1945,8 +1979,8 @@ public class AccountIT extends AbstractDaemonTest {
// Delete key
sender.clear();
- gApi.accounts().id(user.username()).deleteSshKey(1);
- info = gApi.accounts().id(user.username()).listSshKeys();
+ gApi.accounts().id(user.id().get()).deleteSshKey(1);
+ info = gApi.accounts().id(user.id().get()).listSshKeys();
assertThat(info).hasSize(1);
accountIndexedCounter.assertReindexOf(user);
@@ -1962,7 +1996,7 @@ public class AccountIT extends AbstractDaemonTest {
public void userCannotAddSshKeyToOtherAccount() throws Exception {
String newKey = TestSshKeys.publicKey(SshSessionFactory.genSshKey(), admin.email());
requestScopeOperations.setApiUser(user.id());
- assertThrows(AuthException.class, () -> gApi.accounts().id(admin.username()).addSshKey(newKey));
+ assertThrows(AuthException.class, () -> gApi.accounts().id(admin.id().get()).addSshKey(newKey));
}
@Test
@@ -1971,18 +2005,18 @@ public class AccountIT extends AbstractDaemonTest {
requestScopeOperations.setApiUser(user.id());
assertThrows(
ResourceNotFoundException.class,
- () -> gApi.accounts().id(admin.username()).deleteSshKey(0));
+ () -> gApi.accounts().id(admin.id().get()).deleteSshKey(0));
}
// reindex is tested by {@link AbstractQueryAccountsTest#reindex}
@Test
public void reindexPermissions() throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
// admin can reindex any account
requestScopeOperations.setApiUser(admin.id());
- gApi.accounts().id(user.username()).index();
+ gApi.accounts().id(user.id().get()).index();
accountIndexedCounter.assertReindexOf(user);
// user can reindex own account
@@ -1992,7 +2026,7 @@ public class AccountIT extends AbstractDaemonTest {
// user cannot reindex any account
AuthException thrown =
- assertThrows(AuthException.class, () -> gApi.accounts().id(admin.username()).index());
+ assertThrows(AuthException.class, () -> gApi.accounts().id(admin.id().get()).index());
assertThat(thrown).hasMessageThat().contains("modify account not permitted");
}
}
@@ -2024,7 +2058,7 @@ public class AccountIT extends AbstractDaemonTest {
.update(
"Delete External ID",
account.id(),
- u -> u.deleteExternalId(externalIdFactory.createEmail(account.id(), email)));
+ u -> u.deleteExternalId(getExternalIdFactory().createEmail(account.id(), email)));
expectedProblems.add(
new ConsistencyProblemInfo(
ConsistencyProblemInfo.Status.ERROR,
@@ -2045,33 +2079,40 @@ public class AccountIT extends AbstractDaemonTest {
assertThat(accountQueryProvider.get().byDefault(name, true)).isEmpty();
TestAccount foo1 = accountCreator.create(name + "-1");
- assertThat(gApi.accounts().id(foo1.username()).getActive()).isTrue();
+ assertThat(gApi.accounts().id(foo1.id().get()).getActive()).isTrue();
TestAccount foo2 = accountCreator.create(name + "-2");
- gApi.accounts().id(foo2.username()).setActive(false);
+ gApi.accounts().id(foo2.id().get()).setActive(false);
assertThat(gApi.accounts().id(foo2.id().get()).getActive()).isFalse();
assertThat(accountQueryProvider.get().byDefault(name, true)).hasSize(2);
}
@Test
- public void checkMetaId() throws Exception {
- // metaId is set when account is loaded
+ public void checkMetaIdAndUniqueTag() throws Exception {
+ // In open-source Gerrit, the uniqueTag and metaId are always the same. Check them together
+ // in this test.
+ // metaId and uniqueTag are set when account is loaded
assertThat(accounts.get(admin.id()).get().account().metaId()).isEqualTo(getMetaId(admin.id()));
+ assertThat(accounts.get(admin.id()).get().account().uniqueTag())
+ .isEqualTo(getMetaId(admin.id()));
- // metaId is set when account is created
+ // metaId and uniqueTag are set when account is created
AccountsUpdate au = accountsUpdateProvider.get();
Account.Id accountId = Account.id(seq.nextAccountId());
AccountState accountState = au.insert("Create Test Account", accountId, u -> {});
assertThat(accountState.account().metaId()).isEqualTo(getMetaId(accountId));
+ assertThat(accountState.account().uniqueTag()).isEqualTo(getMetaId(accountId));
- // metaId is set when account is updated
+ // metaId and uniqueTag are set when account is updated
Optional<AccountState> updatedAccountState =
au.update("Set Full Name", accountId, u -> u.setFullName("foo"));
assertThat(updatedAccountState).isPresent();
Account updatedAccount = updatedAccountState.get().account();
assertThat(accountState.account().metaId()).isNotEqualTo(updatedAccount.metaId());
+ assertThat(accountState.account().uniqueTag()).isNotEqualTo(updatedAccount.uniqueTag());
assertThat(updatedAccount.metaId()).isEqualTo(getMetaId(accountId));
+ assertThat(updatedAccount.uniqueTag()).isEqualTo(getMetaId(accountId));
}
private EmailInput newEmailInput(String email, boolean noConfirmation) {
@@ -2081,7 +2122,7 @@ public class AccountIT extends AbstractDaemonTest {
return input;
}
- private EmailInput newEmailInput(String email) {
+ protected EmailInput newEmailInput(String email) {
return newEmailInput(email, true);
}
@@ -2097,7 +2138,7 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void allGroupsForAnAdminAccountCanBeRetrieved() throws Exception {
- List<GroupInfo> groups = gApi.accounts().id(admin.username()).getGroups();
+ List<GroupInfo> groups = gApi.accounts().id(admin.id().get()).getGroups();
assertThat(groups)
.comparingElementsUsing(getGroupToNameCorrespondence())
.containsAtLeast("Anonymous Users", "Registered Users", "Administrators");
@@ -2135,13 +2176,13 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void allGroupsForAUserAccountCanBeRetrieved() throws Exception {
String username = name("user1");
- accountOperations.newAccount().username(username).create();
+ Account.Id id = accountOperations.newAccount().username(username).create();
AccountGroup.UUID groupID = groupOperations.newGroup().name("group").create();
String group = groupOperations.group(groupID).get().name();
gApi.groups().id(group).addMembers(username);
- List<GroupInfo> allGroups = gApi.accounts().id(username).getGroups();
+ List<GroupInfo> allGroups = gApi.accounts().id(id.get()).getGroups();
assertThat(allGroups)
.comparingElementsUsing(getGroupToNameCorrespondence())
.containsExactly("Anonymous Users", "Registered Users", group);
@@ -2161,7 +2202,10 @@ public class AccountIT extends AbstractDaemonTest {
assertLabelPermission(
allUsers, groupRef(REGISTERED_USERS), userRef, true, "Code-Review", -2, 2);
+ }
+ @Test
+ public void defaultPermissionsOnUserDefaultBranches() throws Exception {
assertPermissions(
allUsers,
adminGroupRef(),
@@ -2178,7 +2222,7 @@ public class AccountIT extends AbstractDaemonTest {
String fullName = "Foo";
AtomicBoolean doneBgUpdate = new AtomicBoolean(false);
AccountsUpdate update =
- getAccountsUpdate(
+ getAccountsUpdateWithRunnables(
() -> {
if (!doneBgUpdate.getAndSet(true)) {
try {
@@ -2212,11 +2256,11 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void failAfterRetryerGivesUp() throws Exception {
- List<String> status = ImmutableList.of("foo", "bar", "baz");
+ ImmutableList<String> status = ImmutableList.of("foo", "bar", "baz");
String fullName = "Foo";
AtomicInteger bgCounter = new AtomicInteger(0);
AccountsUpdate update =
- getAccountsUpdate(
+ getAccountsUpdateWithRunnables(
() -> {
try {
accountsUpdateProvider
@@ -2236,6 +2280,7 @@ public class AccountIT extends AbstractDaemonTest {
null,
null,
null,
+ null,
exceptionHooks,
r ->
r.withStopStrategy(StopStrategies.stopAfterAttempt(status.size()))
@@ -2245,9 +2290,12 @@ public class AccountIT extends AbstractDaemonTest {
assertThat(accountInfo.status).isNull();
assertThat(accountInfo.name).isNotEqualTo(fullName);
- assertThrows(
- LockFailureException.class,
- () -> update.update("Set Full Name", admin.id(), u -> u.setFullName(fullName)));
+ StorageException exception =
+ assertThrows(
+ StorageException.class,
+ () -> update.update("Set Full Name", admin.id(), u -> u.setFullName(fullName)));
+ assertThat(exception).hasCauseThat().isInstanceOf(RetryException.class);
+ assertThat(exception.getCause()).hasCauseThat().isInstanceOf(LockFailureException.class);
assertThat(bgCounter.get()).isEqualTo(status.size());
Account updatedAccount = accounts.get(admin.id()).get().account();
@@ -2260,15 +2308,19 @@ public class AccountIT extends AbstractDaemonTest {
}
@Test
- public void atomicReadMofifyWrite() throws Exception {
+ public void atomicReadModifyWrite() throws Exception {
gApi.accounts().id(admin.id().get()).setStatus("A-1");
- AtomicInteger bgCounterA1 = new AtomicInteger(0);
- AtomicInteger bgCounterA2 = new AtomicInteger(0);
+ AtomicBoolean bgIndicatorA1ToA2 = new AtomicBoolean(false);
AccountsUpdate update =
- getAccountsUpdate(
+ getAccountsUpdateWithRunnables(
Runnables.doNothing(),
() -> {
+ if (bgIndicatorA1ToA2.get()) {
+ // In the Google architecture, this runnable might be called multiple times. Only
+ // do the replacement once.
+ return;
+ }
try {
accountsUpdateProvider
.get()
@@ -2276,29 +2328,31 @@ public class AccountIT extends AbstractDaemonTest {
} catch (IOException | ConfigInvalidException | StorageException e) {
// Ignore, the expected exception is asserted later
}
+ bgIndicatorA1ToA2.set(true);
});
- assertThat(bgCounterA1.get()).isEqualTo(0);
- assertThat(bgCounterA2.get()).isEqualTo(0);
+
assertThat(gApi.accounts().id(admin.id().get()).get().status).isEqualTo("A-1");
+ AtomicBoolean bgIndicatorA1ToB1 = new AtomicBoolean(false);
+ AtomicBoolean bgIndicatorA2ToB2 = new AtomicBoolean(false);
Optional<AccountState> updatedAccountState =
update.update(
"Set Status",
admin.id(),
(a, u) -> {
if ("A-1".equals(a.account().status())) {
- bgCounterA1.getAndIncrement();
+ bgIndicatorA1ToB1.set(true);
u.setStatus("B-1");
}
if ("A-2".equals(a.account().status())) {
- bgCounterA2.getAndIncrement();
+ bgIndicatorA2ToB2.set(true);
u.setStatus("B-2");
}
});
- assertThat(bgCounterA1.get()).isEqualTo(1);
- assertThat(bgCounterA2.get()).isEqualTo(1);
+ assertThat(bgIndicatorA1ToB1.get()).isTrue();
+ assertThat(bgIndicatorA2ToB2.get()).isTrue();
assertThat(updatedAccountState).isPresent();
assertThat(updatedAccountState.get().account().status()).isEqualTo("B-2");
@@ -2307,64 +2361,70 @@ public class AccountIT extends AbstractDaemonTest {
}
@Test
- public void atomicReadMofifyWriteExternalIds() throws Exception {
+ public void atomicReadModifyWriteExternalIds() throws Exception {
projectOperations
.allProjectsForUpdate()
.add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
.update();
Account.Id accountId = Account.id(seq.nextAccountId());
- ExternalId extIdA1 = externalIdFactory.create("foo", "A-1", accountId);
+ ExternalId extIdA1 = getExternalIdFactory().create("foo", "A-1", accountId);
accountsUpdateProvider
.get()
.insert("Create Test Account", accountId, u -> u.addExternalId(extIdA1));
- AtomicInteger bgCounterA1 = new AtomicInteger(0);
- AtomicInteger bgCounterA2 = new AtomicInteger(0);
- ExternalId extIdA2 = externalIdFactory.create("foo", "A-2", accountId);
+ ExternalId extIdA2 = getExternalIdFactory().create("foo", "A-2", accountId);
+ AtomicBoolean bgIndicatorA1ToA2 = new AtomicBoolean(false);
AccountsUpdate update =
- getAccountsUpdate(
+ getAccountsUpdateWithRunnables(
Runnables.doNothing(),
() -> {
+ if (bgIndicatorA1ToA2.get()) {
+ // In the Google architecture, this runnable might be called multiple times. Only
+ // do the replacement once.
+ return;
+ }
try {
accountsUpdateProvider
.get()
.update(
- "Update External ID",
+ "Update External ID A1->A2",
accountId,
u -> u.replaceExternalId(extIdA1, extIdA2));
} catch (IOException | ConfigInvalidException | StorageException e) {
// Ignore, the expected exception is asserted later
}
+ bgIndicatorA1ToA2.set(true);
});
- assertThat(bgCounterA1.get()).isEqualTo(0);
- assertThat(bgCounterA2.get()).isEqualTo(0);
+
assertThat(
gApi.accounts().id(accountId.get()).getExternalIds().stream()
.map(i -> i.identity)
.collect(toSet()))
.containsExactly(extIdA1.key().get());
- ExternalId extIdB1 = externalIdFactory.create("foo", "B-1", accountId);
- ExternalId extIdB2 = externalIdFactory.create("foo", "B-2", accountId);
+ ExternalId extIdB1 = getExternalIdFactory().create("foo", "B-1", accountId);
+ ExternalId extIdB2 = getExternalIdFactory().create("foo", "B-2", accountId);
+ AtomicBoolean bgIndicatorA1ToB1 = new AtomicBoolean(false);
+ AtomicBoolean bgIndicatorA2ToB2 = new AtomicBoolean(false);
Optional<AccountState> updatedAccount =
update.update(
- "Update External ID",
+ "Conditionally update External IDs: A1->B1, A2->B2",
accountId,
(a, u) -> {
if (a.externalIds().contains(extIdA1)) {
- bgCounterA1.getAndIncrement();
+ bgIndicatorA1ToB1.set(true);
u.replaceExternalId(extIdA1, extIdB1);
}
if (a.externalIds().contains(extIdA2)) {
- bgCounterA2.getAndIncrement();
+ bgIndicatorA2ToB2.set(true);
u.replaceExternalId(extIdA2, extIdB2);
}
});
- assertThat(bgCounterA1.get()).isEqualTo(1);
- assertThat(bgCounterA2.get()).isEqualTo(1);
+ assertThat(bgIndicatorA1ToB1.get()).isTrue();
+ assertThat(bgIndicatorA2ToB2.get()).isTrue();
assertThat(updatedAccount).isPresent();
assertThat(updatedAccount.get().externalIds()).containsExactly(extIdB2);
@@ -2414,38 +2474,24 @@ public class AccountIT extends AbstractDaemonTest {
try (Repository repo = repoManager.openRepository(allUsers)) {
testRefAction(
() -> {
- ExternalIdNotes extIdNotes =
- ExternalIdNotes.load(
- allUsers,
- repo,
- externalIdFactory,
- authConfig.isUserNameCaseInsensitiveMigrationMode());
+ ExternalIdNotes extIdNotes = getExternalIdNotes(repo);
ExternalId.Key key = externalIdKeyFactory.create("foo", "foo");
- extIdNotes.insert(externalIdFactory.create(key, accountId));
+ extIdNotes.insert(getExternalIdFactory().create(key, accountId));
try (MetaDataUpdate update = metaDataUpdateFactory.create(allUsers)) {
extIdNotes.commit(update);
}
assertStaleAccountAndReindex(accountId);
- extIdNotes =
- ExternalIdNotes.load(
- allUsers,
- repo,
- externalIdFactory,
- authConfig.isUserNameCaseInsensitiveMigrationMode());
- extIdNotes.upsert(externalIdFactory.createWithEmail(key, accountId, "foo@example.com"));
+ extIdNotes = getExternalIdNotes(repo);
+ extIdNotes.upsert(
+ getExternalIdFactory().createWithEmail(key, accountId, "foo@example.com"));
try (MetaDataUpdate update = metaDataUpdateFactory.create(allUsers)) {
extIdNotes.commit(update);
}
assertStaleAccountAndReindex(accountId);
- extIdNotes =
- ExternalIdNotes.load(
- allUsers,
- repo,
- externalIdFactory,
- authConfig.isUserNameCaseInsensitiveMigrationMode());
+ extIdNotes = getExternalIdNotes(repo);
extIdNotes.delete(accountId, key);
try (MetaDataUpdate update = metaDataUpdateFactory.create(allUsers)) {
extIdNotes.commit(update);
@@ -2629,7 +2675,7 @@ public class AccountIT extends AbstractDaemonTest {
public void adminCanGenerateNewHttpPasswordForUser() throws Exception {
requestScopeOperations.setApiUser(admin.id());
sender.clear();
- String newPassword = gApi.accounts().id(user.username()).generateHttpPassword();
+ String newPassword = gApi.accounts().id(user.id().get()).generateHttpPassword();
assertThat(newPassword).isNotNull();
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
@@ -2639,7 +2685,7 @@ public class AccountIT extends AbstractDaemonTest {
public void userCannotGenerateNewHttpPasswordForOtherUser() throws Exception {
requestScopeOperations.setApiUser(user.id());
assertThrows(
- AuthException.class, () -> gApi.accounts().id(admin.username()).generateHttpPassword());
+ AuthException.class, () -> gApi.accounts().id(admin.id().get()).generateHttpPassword());
}
@Test
@@ -2654,7 +2700,7 @@ public class AccountIT extends AbstractDaemonTest {
requestScopeOperations.setApiUser(user.id());
assertThrows(
AuthException.class,
- () -> gApi.accounts().id(admin.username()).setHttpPassword("my-new-password"));
+ () -> gApi.accounts().id(admin.id().get()).setHttpPassword("my-new-password"));
}
@Test
@@ -2670,7 +2716,7 @@ public class AccountIT extends AbstractDaemonTest {
public void userCannotRemoveHttpPasswordForOtherUser() throws Exception {
requestScopeOperations.setApiUser(user.id());
assertThrows(
- AuthException.class, () -> gApi.accounts().id(admin.username()).setHttpPassword(null));
+ AuthException.class, () -> gApi.accounts().id(admin.id().get()).setHttpPassword(null));
}
@Test
@@ -2678,7 +2724,7 @@ public class AccountIT extends AbstractDaemonTest {
requestScopeOperations.setApiUser(admin.id());
String httpPassword = "new-password-for-user";
sender.clear();
- assertThat(gApi.accounts().id(user.username()).setHttpPassword(httpPassword))
+ assertThat(gApi.accounts().id(user.id().get()).setHttpPassword(httpPassword))
.isEqualTo(httpPassword);
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
@@ -2688,7 +2734,7 @@ public class AccountIT extends AbstractDaemonTest {
public void adminCanRemoveHttpPasswordForUser() throws Exception {
requestScopeOperations.setApiUser(admin.id());
sender.clear();
- assertThat(gApi.accounts().id(user.username()).setHttpPassword(null)).isNull();
+ assertThat(gApi.accounts().id(user.id().get()).setHttpPassword(null)).isNull();
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("HTTP password was deleted");
}
@@ -2710,17 +2756,13 @@ public class AccountIT extends AbstractDaemonTest {
String extId1String = "foo:bar";
String extId2String = "foo:baz";
ExternalId extId1 =
- externalIdFactory.createWithEmail(
- externalIdKeyFactory.parse(extId1String), admin.id(), "1@foo.com");
+ getExternalIdFactory()
+ .createWithEmail(externalIdKeyFactory.parse(extId1String), admin.id(), "1@foo.com");
ExternalId extId2 =
- externalIdFactory.createWithEmail(
- externalIdKeyFactory.parse(extId2String), user.id(), "2@foo.com");
-
- ObjectId revBefore;
- try (Repository repo = repoManager.openRepository(allUsers)) {
- revBefore = repo.exactRef(RefNames.REFS_EXTERNAL_IDS).getObjectId();
- }
+ getExternalIdFactory()
+ .createWithEmail(externalIdKeyFactory.parse(extId2String), user.id(), "2@foo.com");
+ int initialCommits = countExternalIdsCommits();
AccountsUpdate.UpdateArguments ua1 =
new AccountsUpdate.UpdateArguments(
"Add External ID", admin.id(), (a, u) -> u.addExternalId(extId1));
@@ -2744,21 +2786,28 @@ public class AccountIT extends AbstractDaemonTest {
.contains(extId2String);
// Ensure that we only applied one single commit.
- try (Repository repo = repoManager.openRepository(allUsers);
- RevWalk rw = new RevWalk(repo)) {
- RevCommit after = rw.parseCommit(repo.exactRef(RefNames.REFS_EXTERNAL_IDS).getObjectId());
- assertThat(after.getParent(0).toObjectId()).isEqualTo(revBefore);
+ int afterUpdateCommits = countExternalIdsCommits();
+ assertThat(afterUpdateCommits).isEqualTo(initialCommits + 1);
+ }
+
+ @UsedAt(UsedAt.Project.GOOGLE)
+ protected int countExternalIdsCommits() throws Exception {
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ Git git = new Git(allUsersRepo)) {
+ ObjectId refsMetaExternalIdsHead =
+ allUsersRepo.exactRef(RefNames.REFS_EXTERNAL_IDS).getObjectId();
+ return Iterables.size(git.log().add(refsMetaExternalIdsHead).call());
}
}
@Test
public void externalIdBatchUpdates_fail_sameAccount() {
ExternalId extId1 =
- externalIdFactory.createWithEmail(
- externalIdKeyFactory.parse("foo:bar"), admin.id(), "1@foo.com");
+ getExternalIdFactory()
+ .createWithEmail(externalIdKeyFactory.parse("foo:bar"), admin.id(), "1@foo.com");
ExternalId extId2 =
- externalIdFactory.createWithEmail(
- externalIdKeyFactory.parse("foo:baz"), user.id(), "2@foo.com");
+ getExternalIdFactory()
+ .createWithEmail(externalIdKeyFactory.parse("foo:baz"), user.id(), "2@foo.com");
AccountsUpdate.UpdateArguments ua1 =
new AccountsUpdate.UpdateArguments(
@@ -2777,11 +2826,11 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void externalIdBatchUpdates_fail_duplicateKey() {
ExternalId extIdAdmin =
- externalIdFactory.createWithEmail(
- externalIdKeyFactory.parse("foo:bar"), admin.id(), "1@foo.com");
+ getExternalIdFactory()
+ .createWithEmail(externalIdKeyFactory.parse("foo:bar"), admin.id(), "1@foo.com");
ExternalId extIdUser =
- externalIdFactory.createWithEmail(
- externalIdKeyFactory.parse("foo:bar"), user.id(), "2@foo.com");
+ getExternalIdFactory()
+ .createWithEmail(externalIdKeyFactory.parse("foo:bar"), user.id(), "2@foo.com");
AccountsUpdate.UpdateArguments ua1 =
new AccountsUpdate.UpdateArguments(
@@ -2799,11 +2848,11 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void externalIdBatchUpdates_commitMsg_multipleAccounts() throws Exception {
ExternalId extId1 =
- externalIdFactory.createWithEmail(
- externalIdKeyFactory.parse("foo:bar"), admin.id(), "1@foo.com");
+ getExternalIdFactory()
+ .createWithEmail(externalIdKeyFactory.parse("foo:bar"), admin.id(), "1@foo.com");
ExternalId extId2 =
- externalIdFactory.createWithEmail(
- externalIdKeyFactory.parse("foo:baz"), user.id(), "2@foo.com");
+ getExternalIdFactory()
+ .createWithEmail(externalIdKeyFactory.parse("foo:baz"), user.id(), "2@foo.com");
AccountsUpdate.UpdateArguments ua1 =
new AccountsUpdate.UpdateArguments(
@@ -2825,8 +2874,8 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void externalIdBatchUpdates_commitMsg_singleAccount() throws Exception {
ExternalId extId =
- externalIdFactory.createWithEmail(
- externalIdKeyFactory.parse("foo:bar"), admin.id(), "1@foo.com");
+ getExternalIdFactory()
+ .createWithEmail(externalIdKeyFactory.parse("foo:bar"), admin.id(), "1@foo.com");
accountsUpdateProvider.get().update("foobar", admin.id(), (a, u) -> u.addExternalId(extId));
@@ -2848,17 +2897,21 @@ public class AccountIT extends AbstractDaemonTest {
gApi.accounts().id(foo.id().get()).addEmail(input);
requestScopeOperations.setApiUser(user.id());
- assertThrows(ResourceNotFoundException.class, () -> gApi.accounts().id("secondary"));
+ if (server.isUsernameSupported()) {
+ assertThrows(ResourceNotFoundException.class, () -> gApi.accounts().id("secondary"));
+ }
assertThrows(
ResourceNotFoundException.class, () -> gApi.accounts().id("secondary@example.com"));
requestScopeOperations.setApiUser(admin.id());
- assertThat(gApi.accounts().id("secondary").get()._accountId).isEqualTo(foo.id().get());
+ if (server.isUsernameSupported()) {
+ assertThat(gApi.accounts().id("secondary").get()._accountId).isEqualTo(foo.id().get());
+ }
assertThat(gApi.accounts().id("secondary@example.com").get()._accountId)
.isEqualTo(foo.id().get());
}
@Test
- public void getAccountFromMetaId() throws RestApiException {
+ public void getAccountFromMetaId() throws Exception {
AccountState preUpdateState = accountCache.get(admin.id()).get();
requestScopeOperations.setApiUser(admin.id());
gApi.accounts().self().setStatus("New status");
@@ -2898,7 +2951,7 @@ public class AccountIT extends AbstractDaemonTest {
}
@Test
- public void projectWatchesUpdate_refsUsersUpdated() throws RestApiException {
+ public void projectWatchesUpdate_refsUsersUpdated() throws Exception {
AccountState preUpdateState = accountCache.get(admin.id()).get();
requestScopeOperations.setApiUser(admin.id());
@@ -2921,16 +2974,16 @@ public class AccountIT extends AbstractDaemonTest {
AccountState preUpdateState = accountCache.get(admin.id()).get();
requestScopeOperations.setApiUser(admin.id());
- gApi.accounts().self().addEmail(newEmailInput("secondary@google.com"));
+ gApi.accounts().self().addEmail(newEmailInput("secondary@non.google"));
assertExternalIds(
admin.id(),
ImmutableSet.of(
- "mailto:admin@example.com", "username:admin", "mailto:secondary@google.com"));
+ "mailto:admin@example.com", "username:admin", "mailto:secondary@non.google"));
AccountState updatedState1 = accountCache.get(admin.id()).get();
assertThat(preUpdateState.account().metaId()).isNotEqualTo(updatedState1.account().metaId());
- gApi.accounts().self().deleteExternalIds(ImmutableList.of("mailto:secondary@google.com"));
+ gApi.accounts().self().deleteExternalIds(ImmutableList.of("mailto:secondary@non.google"));
AccountState updatedState2 = accountCache.get(admin.id()).get();
assertThat(updatedState1.account().metaId()).isNotEqualTo(updatedState2.account().metaId());
@@ -2941,7 +2994,7 @@ public class AccountIT extends AbstractDaemonTest {
AccountState preUpdateState = accountCache.get(admin.id()).get();
requestScopeOperations.setApiUser(admin.id());
- ExternalId externalId = externalIdFactory.create("custom", "value", admin.id());
+ ExternalId externalId = getExternalIdFactory().create("custom", "value", admin.id());
accountsUpdateProvider
.get()
.update("Add External ID", admin.id(), (a, u) -> u.addExternalId(externalId));
@@ -2957,7 +3010,7 @@ public class AccountIT extends AbstractDaemonTest {
AccountState preUpdateState = accountCache.get(admin.id()).get();
requestScopeOperations.setApiUser(admin.id());
- ExternalId externalId = externalIdFactory.create("mailto", "admin@example.com", admin.id());
+ ExternalId externalId = createEmailExternalId(admin.id(), "admin@example.com");
accountsUpdateProvider
.get()
.update("Remove External ID", admin.id(), (a, u) -> u.deleteExternalId(externalId));
@@ -2973,12 +3026,16 @@ public class AccountIT extends AbstractDaemonTest {
requestScopeOperations.setApiUser(admin.id());
ExternalId externalId =
- externalIdFactory.createWithEmail(
- SCHEME_USERNAME, "admin", admin.id(), "secondary@example.com");
+ getExternalIdFactory()
+ .createWithEmail(
+ SCHEME_MAILTO, "secondary@non.google", admin.id(), "secondary@non.google");
accountsUpdateProvider
.get()
- .update("Remove External ID", admin.id(), (a, u) -> u.updateExternalId(externalId));
- assertExternalIds(admin.id(), ImmutableSet.of("mailto:admin@example.com", "username:admin"));
+ .update("Update External ID", admin.id(), (a, u) -> u.updateExternalId(externalId));
+ assertExternalIds(
+ admin.id(),
+ ImmutableSet.of(
+ "mailto:admin@example.com", "username:admin", "mailto:secondary@non.google"));
AccountState updatedState = accountCache.get(admin.id()).get();
assertThat(preUpdateState.account().metaId()).isNotEqualTo(updatedState.account().metaId());
@@ -2990,23 +3047,30 @@ public class AccountIT extends AbstractDaemonTest {
requestScopeOperations.setApiUser(admin.id());
ExternalId externalId =
- externalIdFactory.createWithEmail(
- SCHEME_USERNAME, admin.username(), admin.id(), "secondary@example.com");
+ getExternalIdFactory()
+ .createWithEmail(
+ SCHEME_MAILTO, "secondary@non.google", admin.id(), "secondary@non.google");
+ ExternalId oldExternalId =
+ getExternalIdsReader().get(createEmailExternalId(admin.id(), admin.email()).key()).get();
accountsUpdateProvider
.get()
.update(
- "Remove External ID",
+ "Replace External ID",
admin.id(),
- (a, u) ->
- u.replaceExternalId(
- externalIds
- .get(externalIdKeyFactory.create(SCHEME_USERNAME, admin.username()))
- .get(),
- externalId));
- assertExternalIds(admin.id(), ImmutableSet.of("mailto:admin@example.com", "username:admin"));
+ (a, u) -> {
+ u.replaceExternalId(oldExternalId, externalId);
+ });
+ assertExternalIds(admin.id(), ImmutableSet.of("mailto:secondary@non.google", "username:admin"));
AccountState updatedState = accountCache.get(admin.id()).get();
- assertThat(preUpdateState.account().metaId()).isNotEqualTo(updatedState.account().metaId());
+ assertThat(accountCache.get(admin.id()).get()).isNotSameInstanceAs(preUpdateState);
+ if (preUpdateState.account().metaId() == null) {
+ // When the test is executed on google infrastructure, metaId should be either always set
+ // or always be null.
+ assertThat(updatedState.account().metaId()).isNull();
+ } else {
+ assertThat(preUpdateState.account().metaId()).isNotEqualTo(updatedState.account().metaId());
+ }
}
@Test
@@ -3017,10 +3081,11 @@ public class AccountIT extends AbstractDaemonTest {
requestScopeOperations.setApiUser(admin.id());
ExternalId extId1 =
- externalIdFactory.createWithEmail("custom", "admin-id", admin.id(), "admin-id@test.com");
+ getExternalIdFactory()
+ .createWithEmail("custom", "admin-id", admin.id(), "admin-id@test.com");
ExternalId extId2 =
- externalIdFactory.createWithEmail("custom", "user-id", user.id(), "user-id@test.com");
+ getExternalIdFactory().createWithEmail("custom", "user-id", user.id(), "user-id@test.com");
AccountsUpdate.UpdateArguments ua1 =
new AccountsUpdate.UpdateArguments(
@@ -3028,7 +3093,7 @@ public class AccountIT extends AbstractDaemonTest {
AccountsUpdate.UpdateArguments ua2 =
new AccountsUpdate.UpdateArguments(
"Add External ID", user.id(), (a, u) -> u.addExternalId(extId2));
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
accountsUpdateProvider.get().updateBatch(ImmutableList.of(ua1, ua2));
@@ -3069,11 +3134,8 @@ public class AccountIT extends AbstractDaemonTest {
new AccountsUpdate.UpdateArguments(
"Remove external Id",
user.id(),
- (a, u) ->
- u.deleteExternalId(
- externalIdFactory.createWithEmail(
- SCHEME_MAILTO, user.email(), user.id(), user.email())));
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ (a, u) -> u.deleteExternalId(createEmailExternalId(user.id(), user.email())));
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
accountsUpdateProvider.get().updateBatch(ImmutableList.of(ua1, ua2));
@@ -3090,6 +3152,10 @@ public class AccountIT extends AbstractDaemonTest {
.isNotEqualTo(updatedUserState.account().metaId());
}
+ protected ExternalId createEmailExternalId(Account.Id accountId, String email) {
+ return getExternalIdFactory().createWithEmail(SCHEME_MAILTO, email, accountId, email);
+ }
+
@Test
@GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
public void accountsCanSeeEachOtherThroughASharedExternalGroupOnlyWhenTheGroupIsMentionedInAcls()
@@ -3212,7 +3278,8 @@ public class AccountIT extends AbstractDaemonTest {
gApi.accounts().self().delete();
requestScopeOperations.setApiUser(admin.id());
- assertThat(externalIds.byEmails("deleted@internal.com", "deleted@external.com")).isEmpty();
+ assertThat(getExternalIdsReader().byEmails("deleted@internal.com", "deleted@external.com"))
+ .isEmpty();
// Clean up the test framework
accountCreator.evict(deleted.id());
@@ -3271,23 +3338,12 @@ public class AccountIT extends AbstractDaemonTest {
requestScopeOperations.setApiUser(deleted.id());
gApi.accounts().self().starChange(triplet);
- try (Repository repo = repoManager.openRepository(allUsers)) {
- assertThat(
- repo.getRefDatabase()
- .getRefsByPrefix(RefNames.refsStarredChangesPrefix(r.getChange().getId())))
- .hasSize(1);
+ assertThat(getStarredChangesCount(r.getChange().getId())).isEqualTo(1);
- gApi.accounts().self().delete();
- }
+ gApi.accounts().self().delete();
// Reopen the repo to refresh RefDb
- try (Repository repo = repoManager.openRepository(allUsers)) {
- assertThat(
- repo.getRefDatabase()
- .getRefsByPrefix(RefNames.refsStarredChangesPrefix(r.getChange().getId())))
- .isEmpty();
- }
-
+ assertThat(getStarredChangesCount(r.getChange().getId())).isEqualTo(0);
// Clean up the test framework
accountCreator.evict(deleted.id());
}
@@ -3329,27 +3385,35 @@ public class AccountIT extends AbstractDaemonTest {
requestScopeOperations.setApiUser(deleted.id());
createDraft(r, PushOneCommit.FILE_NAME, "draft");
- try (Repository repo = repoManager.openRepository(allUsers)) {
- assertThat(
- repo.getRefDatabase()
- .getRefsByPrefix(RefNames.refsDraftCommentsPrefix(r.getChange().getId())))
- .hasSize(1);
-
- gApi.accounts().self().delete();
- }
+ assertThat(getUsersWithDraftsCount(r.getChange().getId())).isEqualTo(1);
+ gApi.accounts().self().delete();
- // Reopen the repo to refresh RefDb
- try (Repository repo = repoManager.openRepository(allUsers)) {
- assertThat(
- repo.getRefDatabase()
- .getRefsByPrefix(RefNames.refsDraftCommentsPrefix(r.getChange().getId())))
- .isEmpty();
- }
+ assertThat(getUsersWithDraftsCount(r.getChange().getId())).isEqualTo(0);
// Clean up the test framework
accountCreator.evict(deleted.id());
}
+ @UsedAt(UsedAt.Project.GOOGLE)
+ protected int getUsersWithDraftsCount(Change.Id changeId) throws Exception {
+ // The getStarredChangesCount and getUsersWithDraftsCount should be 2 distinct methods,
+ // because in google they can query data from a different storage (i.e. not from noteDb).
+ return getRefCount(RefNames.refsDraftCommentsPrefix(changeId));
+ }
+
+ @UsedAt(UsedAt.Project.GOOGLE)
+ protected int getStarredChangesCount(Change.Id changeId) throws Exception {
+ // The getStarredChangesCount and getDraftsCommentsCount should be 2 distinct methods,
+ // because in google they can query data from a different storage (i.e. not from noteDb).
+ return getRefCount(RefNames.refsStarredChangesPrefix(changeId));
+ }
+
+ private int getRefCount(String refPrefix) throws Exception {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ return repo.getRefDatabase().getRefsByPrefix(refPrefix).size();
+ }
+ }
+
@Test
@SuppressWarnings("unused")
public void deleteAccount_deletesReviewedFlags() throws Exception {
@@ -3410,7 +3474,7 @@ public class AccountIT extends AbstractDaemonTest {
}
@Override
- public Set<AccountGroup.UUID> getKnownGroups() {
+ public ImmutableSet<AccountGroup.UUID> getKnownGroups() {
// Typically for external group backends it's too expensive to query all groups that the
// user is a member of. Instead limit the group membership check to groups that are
// guessed to be relevant.
@@ -3488,7 +3552,7 @@ public class AccountIT extends AbstractDaemonTest {
}
private static void assertIteratorSize(int size, Iterator<?> it) {
- List<?> lst = ImmutableList.copyOf(it);
+ ImmutableList<?> lst = ImmutableList.copyOf(it);
assertThat(lst).hasSize(size);
}
@@ -3524,11 +3588,11 @@ public class AccountIT extends AbstractDaemonTest {
}
// Check raw external IDs.
- Account.Id currAccountId = atrScope.get().getUser().getAccountId();
+ Account.Id currAccountId = localCtx.getContext().getUser().getAccountId();
Iterable<String> expectedFps =
expected.transform(k -> BaseEncoding.base16().encode(k.getPublicKey().getFingerprint()));
- Iterable<String> actualFps =
- externalIds.byAccount(currAccountId, SCHEME_GPGKEY).stream()
+ Set<String> actualFps =
+ getExternalIdsReader().byAccount(currAccountId, SCHEME_GPGKEY).stream()
.map(e -> e.key().id())
.collect(toSet());
assertWithMessage("external IDs in database")
@@ -3549,7 +3613,7 @@ public class AccountIT extends AbstractDaemonTest {
assertWithMessage(id)
.that(actual.fingerprint)
.isEqualTo(Fingerprint.toString(expected.getPublicKey().getFingerprint()));
- List<String> userIds = ImmutableList.copyOf(expected.getPublicKey().getUserIDs());
+ ImmutableList<String> userIds = ImmutableList.copyOf(expected.getPublicKey().getUserIDs());
assertWithMessage(id).that(actual.userIds).containsExactlyElementsIn(userIds);
String key = actual.key;
assertWithMessage(id).that(key).startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n");
@@ -3559,7 +3623,7 @@ public class AccountIT extends AbstractDaemonTest {
}
private void addExternalIdEmail(TestAccount account, String email) throws Exception {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
requireNonNull(email);
@@ -3570,7 +3634,8 @@ public class AccountIT extends AbstractDaemonTest {
account.id(),
u ->
u.addExternalId(
- externalIdFactory.createWithEmail(name("test"), email, account.id(), email)));
+ getExternalIdFactory()
+ .createWithEmail(name("test"), email, account.id(), email)));
accountIndexedCounter.assertReindexOf(account);
requestScopeOperations.setApiUser(account.id());
}
@@ -3585,14 +3650,14 @@ public class AccountIT extends AbstractDaemonTest {
private Map<String, GpgKeyInfo> addGpgKey(TestAccount account, String armored) throws Exception {
return testRefAction(
() -> {
- AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+ AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(accountIndexedCounter)) {
Map<String, GpgKeyInfo> gpgKeys =
gApi.accounts()
- .id(account.username())
+ .id(account.id().get())
.putGpgKeys(ImmutableList.of(armored), ImmutableList.<String>of());
- accountIndexedCounter.assertReindexOf(gApi.accounts().id(account.username()).get());
+ accountIndexedCounter.assertReindexOf(gApi.accounts().id(account.id().get()).get());
return gpgKeys;
}
});
@@ -3610,7 +3675,9 @@ public class AccountIT extends AbstractDaemonTest {
throws Exception {
assertThat(info.name).isEqualTo(account.fullName());
assertThat(info.email).isEqualTo(account.email());
- assertThat(info.username).isEqualTo(account.username());
+ if (server.isUsernameSupported()) {
+ assertThat(info.username).isEqualTo(account.username());
+ }
assertThat(info.status).isEqualTo(expectedStatus);
}
@@ -3647,8 +3714,9 @@ public class AccountIT extends AbstractDaemonTest {
"login?account_id=" + accountId, HttpServletResponse.SC_MOVED_TEMPORARILY);
}
- private AccountsUpdate getAccountsUpdate(Runnable afterReadRevision, Runnable beforeCommit) {
- return getAccountsUpdate(
+ private AccountsUpdate getAccountsUpdateWithRunnables(
+ Runnable afterReadRevision, Runnable beforeCommit) {
+ return getAccountsUpdateWithRunnables(
afterReadRevision,
beforeCommit,
new RetryHelper(
@@ -3657,18 +3725,45 @@ public class AccountIT extends AbstractDaemonTest {
null,
null,
null,
+ null,
exceptionHooks,
r -> r.withBlockStrategy(noSleepBlockStrategy)));
}
- private AccountsUpdate getAccountsUpdate(
+ private ExternalIdNotes getExternalIdNotes(Repository allUsersRepo)
+ throws ConfigInvalidException, IOException {
+ return ExternalIdNotes.load(
+ allUsers,
+ allUsersRepo,
+ externalIdFactoryNoteDbImpl,
+ authConfig.isUserNameCaseInsensitiveMigrationMode());
+ }
+
+ @UsedAt(UsedAt.Project.GOOGLE)
+ protected ExternalIdFactory getExternalIdFactory() {
+ return externalIdFactoryNoteDbImpl;
+ }
+
+ @UsedAt(UsedAt.Project.GOOGLE)
+ protected ExternalIds getExternalIdsReader() {
+ return externalIdsNoteDbImpl;
+ }
+
+ @UsedAt(UsedAt.Project.GOOGLE)
+ protected AccountsUpdate getAccountsUpdateWithRunnables(
+ Runnable afterReadRevision, Runnable beforeCommit, RetryHelper retryHelper) {
+ return getAccountsUpdateNoteDbImplWithRunnables(afterReadRevision, beforeCommit, retryHelper);
+ }
+
+ @UsedAt(UsedAt.Project.GOOGLE)
+ protected final AccountsUpdateNoteDbImpl getAccountsUpdateNoteDbImplWithRunnables(
Runnable afterReadRevision, Runnable beforeCommit, RetryHelper retryHelper) {
return new AccountsUpdateNoteDbImpl(
repoManager,
gitReferenceUpdated,
Optional.empty(),
allUsers,
- externalIds,
+ externalIdsNoteDbImpl,
extIdNotesFactory,
metaDataUpdateInternalFactory,
retryHelper,
@@ -3685,6 +3780,11 @@ public class AccountIT extends AbstractDaemonTest {
}
@UsedAt(UsedAt.Project.GOOGLE)
+ protected RefUpdateCounter createRefUpdateCounter() {
+ return new RefUpdateCounter();
+ }
+
+ @UsedAt(UsedAt.Project.GOOGLE)
public static class RefUpdateCounter implements GitReferenceUpdatedListener {
private final AtomicLongMap<String> countsByProjectRefs = AtomicLongMap.create();
@@ -3715,10 +3815,18 @@ public class AccountIT extends AbstractDaemonTest {
assertRefUpdateFor(expectedRefUpdateCounts);
}
- void assertRefUpdateFor(Map<String, Long> expectedProjectRefUpdateCounts) {
- assertThat(countsByProjectRefs.asMap())
- .containsExactlyEntriesIn(expectedProjectRefUpdateCounts);
+ protected void assertRefUpdateFor(Map<String, Long> expectedProjectRefUpdateCounts) {
+ ImmutableMap<String, Long> exprectedFiltered =
+ expectedProjectRefUpdateCounts.entrySet().stream()
+ .filter(entry -> isRefSupported(entry.getKey()))
+ .collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
+ assertThat(countsByProjectRefs.asMap()).containsExactlyEntriesIn(exprectedFiltered);
clear();
}
+
+ @UsedAt(UsedAt.Project.GOOGLE)
+ protected boolean isRefSupported(String expectedRefEntryKey) {
+ return true;
+ }
}
}
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIndexerIT.java
index 1693411a9b..c56d907c3b 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIndexerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIndexerIT.java
@@ -134,7 +134,8 @@ public class AccountIndexerIT {
}
private void loadAccountToCache(Account.Id accountId) {
- accountCache.get(accountId);
+ @SuppressWarnings("unused")
+ var unused = accountCache.get(accountId);
}
private static AccountDelta.Builder newAccountDelta() {
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountListenersIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountListenersIT.java
index 3ead608c06..b4c45954a9 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountListenersIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountListenersIT.java
@@ -20,6 +20,7 @@ import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
import com.google.gerrit.acceptance.TestPlugin;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.common.UsedAt;
import com.google.gerrit.extensions.events.AccountActivationListener;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -49,16 +50,17 @@ public class AccountListenersIT extends LightweightPluginDaemonTest {
@Override
protected void configure() {
DynamicSet.bind(binder(), AccountActivationValidationListener.class).to(Validator.class);
+ bind(AccountListenersITValidator.class).to(Validator.class);
DynamicSet.bind(binder(), AccountActivationListener.class).to(Listener.class);
}
}
- Validator validator;
+ AccountListenersITValidator validator;
Listener listener;
@Before
public void setUp() {
- validator = plugin.getSysInjector().getInstance(Validator.class);
+ validator = plugin.getSysInjector().getInstance(AccountListenersITValidator.class);
listener = plugin.getSysInjector().getInstance(Listener.class);
}
@@ -128,8 +130,21 @@ public class AccountListenersIT extends LightweightPluginDaemonTest {
listener.assertNoMoreEvents();
}
+ @UsedAt(UsedAt.Project.GOOGLE)
+ protected interface AccountListenersITValidator extends AccountActivationValidationListener {
+ void failActivationValidations();
+
+ void failDeactivationValidations();
+
+ void assertNoMoreEvents();
+
+ void assertActivationValidation(int id);
+
+ void assertDeactivationValidation(int id);
+ }
+
@Singleton
- public static class Validator implements AccountActivationValidationListener {
+ public static final class Validator implements AccountListenersITValidator {
private Integer lastIdActivationValidation;
private Integer lastIdDeactivationValidation;
private boolean failActivationValidations;
@@ -153,25 +168,30 @@ public class AccountListenersIT extends LightweightPluginDaemonTest {
}
}
+ @Override
public void failActivationValidations() {
failActivationValidations = true;
}
+ @Override
public void failDeactivationValidations() {
failDeactivationValidations = true;
}
- private void assertNoMoreEvents() {
+ @Override
+ public void assertNoMoreEvents() {
assertThat(lastIdActivationValidation).isNull();
assertThat(lastIdDeactivationValidation).isNull();
}
- private void assertActivationValidation(int id) {
+ @Override
+ public void assertActivationValidation(int id) {
assertThat(lastIdActivationValidation).isEqualTo(id);
lastIdActivationValidation = null;
}
- private void assertDeactivationValidation(int id) {
+ @Override
+ public void assertDeactivationValidation(int id) {
assertThat(lastIdDeactivationValidation).isEqualTo(id);
lastIdDeactivationValidation = null;
}
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
index 39fa918189..efc7e0ffd6 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
@@ -17,7 +17,6 @@ package com.google.gerrit.acceptance.api.accounts;
import static com.google.common.truth.OptionalSubject.optionals;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.entities.RefNames.REFS_EXTERNAL_IDS;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GOOGLE_OAUTH;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
@@ -29,6 +28,7 @@ import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.client.AccountFieldName;
import com.google.gerrit.server.IdentifiedUser;
@@ -41,6 +41,7 @@ import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.account.SetInactiveFlag;
+import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdFactory;
import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
@@ -52,11 +53,9 @@ import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.gerrit.server.update.context.RefUpdateContext;
import com.google.inject.Inject;
import com.google.inject.util.Providers;
-import java.io.IOException;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.junit.Test;
@@ -594,7 +593,7 @@ public class AccountManagerIT extends AbstractDaemonTest {
AccountException thrown =
assertThrows(AccountException.class, () -> accountManager.authenticate(whoOAuth));
- assertThat(thrown).hasMessageThat().contains("Cannot assign external ID \"username:foo\" to");
+ assertThat(thrown).hasCauseThat().isInstanceOf(DuplicateExternalIdKeyException.class);
}
@Test
@@ -648,9 +647,7 @@ public class AccountManagerIT extends AbstractDaemonTest {
AccountException thrown =
assertThrows(AccountException.class, () -> accountManager.authenticate(whoOAuth));
- assertThat(thrown)
- .hasMessageThat()
- .contains("Cannot assign external ID \"username:foo\" to account");
+ assertThat(thrown).hasCauseThat().isInstanceOf(DuplicateExternalIdKeyException.class);
}
@Test
@@ -799,25 +796,22 @@ public class AccountManagerIT extends AbstractDaemonTest {
"Create Test Account",
accountId,
u -> u.addExternalId(externalIdFactory.create(mailExtIdKey, accountId)));
-
accountManager.link(accountId, authRequestFactory.createForEmail(email1));
+ int initialCommits = countExternalIdsCommits();
- try (Repository allUsersRepo = repoManager.openRepository(allUsers);
- Git git = new Git(allUsersRepo)) {
- int initialCommits = getCommitsInExternalIds(git, allUsersRepo);
-
- accountManager.updateLink(accountId, authRequestFactory.createForEmail(email2));
+ accountManager.updateLink(accountId, authRequestFactory.createForEmail(email2));
- int afterUpdateCommits = getCommitsInExternalIds(git, allUsersRepo);
-
- assertThat(afterUpdateCommits).isEqualTo(initialCommits + 1);
- }
+ int afterUpdateCommits = countExternalIdsCommits();
+ assertThat(afterUpdateCommits).isEqualTo(initialCommits + 1);
}
- private static int getCommitsInExternalIds(Git git, Repository allUsersRepo)
- throws GitAPIException, IOException {
- ObjectId refsMetaExternalIdsHead = allUsersRepo.exactRef(REFS_EXTERNAL_IDS).getObjectId();
- return Iterables.size(git.log().add(refsMetaExternalIdsHead).call());
+ @UsedAt(UsedAt.Project.GOOGLE)
+ protected int countExternalIdsCommits() throws Exception {
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ Git git = new Git(allUsersRepo)) {
+ ObjectId refsMetaExternalIdsHead = allUsersRepo.exactRef(REFS_EXTERNAL_IDS).getObjectId();
+ return Iterables.size(git.log().add(refsMetaExternalIdsHead).call());
+ }
}
private void assertNoSuchExternalIds(ExternalId.Key... extIdKeys) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
index 9b77b01745..ead4c406f1 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -127,7 +127,7 @@ public class AgreementsIT extends AbstractDaemonTest {
assertThat(info.auth.useContributorAgreements).isTrue();
assertThat(info.auth.contributorAgreements).hasSize(2);
// Sort to get a stable assertion as the API does not guarantee ordering.
- List<AgreementInfo> agreements =
+ ImmutableList<AgreementInfo> agreements =
ImmutableList.sortedCopyOf(comparing(a -> a.name), info.auth.contributorAgreements);
assertAgreement(agreements.get(0), caAutoVerify);
assertAgreement(agreements.get(1), caNoAutoVerify);
@@ -187,8 +187,11 @@ public class AgreementsIT extends AbstractDaemonTest {
public void listAgreementPermission() throws Exception {
assume().that(isContributorAgreementsEnabled()).isTrue();
requestScopeOperations.setApiUser(admin.id());
+
// Allowed.
- gApi.accounts().id(user.id().get()).listAgreements();
+ @SuppressWarnings("unused")
+ var unused = gApi.accounts().id(user.id().get()).listAgreements();
+
requestScopeOperations.setApiUser(user.id());
// Not allowed.
@@ -202,7 +205,7 @@ public class AgreementsIT extends AbstractDaemonTest {
AuthException thrown =
assertThrows(
AuthException.class,
- () -> gApi.accounts().id("admin").signAgreement(caAutoVerify.getName()));
+ () -> gApi.accounts().id(admin.id().get()).signAgreement(caAutoVerify.getName()));
assertThat(thrown).hasMessageThat().contains("not allowed to enter contributor agreement");
}
@@ -391,7 +394,9 @@ public class AgreementsIT extends AbstractDaemonTest {
@GerritConfig(name = "auth.contributorAgreements", value = "true")
public void anonymousAccessServerInfoEvenWithCLAs() throws Exception {
requestScopeOperations.setApiUserAnonymous();
- gApi.config().server().getInfo();
+
+ @SuppressWarnings("unused")
+ var unused = gApi.config().server().getInfo();
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
index a442ddd273..e3380c0c6d 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
@@ -28,7 +28,7 @@ public class DiffPreferencesIT extends AbstractDaemonTest {
@Test
public void getDiffPreferences() throws Exception {
DiffPreferencesInfo d = DiffPreferencesInfo.defaults();
- DiffPreferencesInfo o = gApi.accounts().id(admin.id().toString()).getDiffPreferences();
+ DiffPreferencesInfo o = gApi.accounts().id(admin.id().get()).getDiffPreferences();
assertPrefs(o, d);
}
@@ -62,13 +62,13 @@ public class DiffPreferencesIT extends AbstractDaemonTest {
i.matchBrackets ^= true;
i.lineWrapping ^= true;
- DiffPreferencesInfo o = gApi.accounts().id(admin.id().toString()).setDiffPreferences(i);
+ DiffPreferencesInfo o = gApi.accounts().id(admin.id().get()).setDiffPreferences(i);
assertPrefs(o, i);
// Partially fill input record
i = new DiffPreferencesInfo();
i.tabSize = 42;
- DiffPreferencesInfo a = gApi.accounts().id(admin.id().toString()).setDiffPreferences(i);
+ DiffPreferencesInfo a = gApi.accounts().id(admin.id().get()).setDiffPreferences(i);
assertPrefs(a, o, "tabSize");
assertThat(a.tabSize).isEqualTo(42);
}
@@ -85,7 +85,7 @@ public class DiffPreferencesIT extends AbstractDaemonTest {
update.fontSize = newFontSize;
gApi.config().server().setDefaultDiffPreferences(update);
- DiffPreferencesInfo o = gApi.accounts().id(admin.id().toString()).getDiffPreferences();
+ DiffPreferencesInfo o = gApi.accounts().id(admin.id().get()).getDiffPreferences();
// assert configured defaults
assertThat(o.lineLength).isEqualTo(newLineLength);
@@ -104,29 +104,29 @@ public class DiffPreferencesIT extends AbstractDaemonTest {
update.lineLength = configuredDefaultLineLength;
gApi.config().server().setDefaultDiffPreferences(update);
- DiffPreferencesInfo o = gApi.accounts().id(admin.id().toString()).getDiffPreferences();
+ DiffPreferencesInfo o = gApi.accounts().id(admin.id().get()).getDiffPreferences();
assertThat(o.lineLength).isEqualTo(configuredDefaultLineLength);
assertPrefs(o, d, "lineLength");
int newLineLength = configuredDefaultLineLength + 10;
DiffPreferencesInfo i = new DiffPreferencesInfo();
i.lineLength = newLineLength;
- DiffPreferencesInfo a = gApi.accounts().id(admin.id().toString()).setDiffPreferences(i);
+ DiffPreferencesInfo a = gApi.accounts().id(admin.id().get()).setDiffPreferences(i);
assertThat(a.lineLength).isEqualTo(newLineLength);
assertPrefs(a, d, "lineLength");
- a = gApi.accounts().id(admin.id().toString()).getDiffPreferences();
+ a = gApi.accounts().id(admin.id().get()).getDiffPreferences();
assertThat(a.lineLength).isEqualTo(newLineLength);
assertPrefs(a, d, "lineLength");
// overwrite the configured default with original hard-coded default
i = new DiffPreferencesInfo();
i.lineLength = d.lineLength;
- a = gApi.accounts().id(admin.id().toString()).setDiffPreferences(i);
+ a = gApi.accounts().id(admin.id().get()).setDiffPreferences(i);
assertThat(a.lineLength).isEqualTo(d.lineLength);
assertPrefs(a, d, "lineLength");
- a = gApi.accounts().id(admin.id().toString()).getDiffPreferences();
+ a = gApi.accounts().id(admin.id().get()).getDiffPreferences();
assertThat(a.lineLength).isEqualTo(d.lineLength);
assertPrefs(a, d, "lineLength");
}
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java
index f142c0881e..6802333743 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/EditPreferencesIT.java
@@ -25,7 +25,7 @@ import org.junit.Test;
public class EditPreferencesIT extends AbstractDaemonTest {
@Test
public void getSetEditPreferences() throws Exception {
- EditPreferencesInfo out = gApi.accounts().id(admin.id().toString()).getEditPreferences();
+ EditPreferencesInfo out = gApi.accounts().id(admin.id().get()).getEditPreferences();
assertThat(out.lineLength).isEqualTo(100);
assertThat(out.indentUnit).isEqualTo(2);
@@ -58,7 +58,7 @@ public class EditPreferencesIT extends AbstractDaemonTest {
out.autoCloseBrackets = true;
out.showBase = true;
- EditPreferencesInfo info = gApi.accounts().id(admin.id().toString()).setEditPreferences(out);
+ EditPreferencesInfo info = gApi.accounts().id(admin.id().get()).setEditPreferences(out);
assertEditPreferences(info, out);
@@ -66,7 +66,7 @@ public class EditPreferencesIT extends AbstractDaemonTest {
EditPreferencesInfo in = new EditPreferencesInfo();
in.tabSize = 42;
- info = gApi.accounts().id(admin.id().toString()).setEditPreferences(in);
+ info = gApi.accounts().id(admin.id().get()).setEditPreferences(in);
out.tabSize = in.tabSize;
assertEditPreferences(info, out);
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
index eed9de822a..0b28f6f7e6 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
@@ -53,7 +53,7 @@ public class GeneralPreferencesIT extends AbstractDaemonTest {
@Test
public void getAndSetPreferences() throws Exception {
- GeneralPreferencesInfo o = gApi.accounts().id(user42.id().toString()).getPreferences();
+ GeneralPreferencesInfo o = gApi.accounts().id(user42.id().get()).getPreferences();
assertPrefs(o, GeneralPreferencesInfo.defaults(), "my", "changeTable");
assertThat(o.my)
.containsExactly(
@@ -84,6 +84,7 @@ public class GeneralPreferencesIT extends AbstractDaemonTest {
i.muteCommonPathPrefixes ^= true;
i.signedOffBy ^= true;
i.allowBrowserNotifications ^= false;
+ i.allowSuggestCodeWhileCommenting ^= false;
i.diffPageSidebar = "plugin-insight";
i.diffView = DiffView.UNIFIED_DIFF;
i.my = new ArrayList<>();
@@ -91,12 +92,13 @@ public class GeneralPreferencesIT extends AbstractDaemonTest {
i.changeTable = new ArrayList<>();
i.changeTable.add("Status");
- o = gApi.accounts().id(user42.id().toString()).setPreferences(i);
+ o = gApi.accounts().id(user42.id().get()).setPreferences(i);
assertPrefs(o, i, "my");
assertThat(o.my).containsExactlyElementsIn(i.my);
assertThat(o.changeTable).containsExactlyElementsIn(i.changeTable);
assertThat(o.theme).isEqualTo(i.theme);
assertThat(o.allowBrowserNotifications).isEqualTo(i.allowBrowserNotifications);
+ assertThat(o.allowSuggestCodeWhileCommenting).isEqualTo(i.allowSuggestCodeWhileCommenting);
assertThat(o.diffPageSidebar).isEqualTo(i.diffPageSidebar);
assertThat(o.disableKeyboardShortcuts).isEqualTo(i.disableKeyboardShortcuts);
}
@@ -109,7 +111,7 @@ public class GeneralPreferencesIT extends AbstractDaemonTest {
update.changesPerPage = newChangesPerPage;
gApi.config().server().setDefaultPreferences(update);
- GeneralPreferencesInfo o = gApi.accounts().id(user42.id().toString()).getPreferences();
+ GeneralPreferencesInfo o = gApi.accounts().id(user42.id().get()).getPreferences();
// assert configured defaults
assertThat(o.changesPerPage).isEqualTo(newChangesPerPage);
@@ -126,29 +128,29 @@ public class GeneralPreferencesIT extends AbstractDaemonTest {
update.changesPerPage = configuredChangesPerPage;
gApi.config().server().setDefaultPreferences(update);
- GeneralPreferencesInfo o = gApi.accounts().id(admin.id().toString()).getPreferences();
+ GeneralPreferencesInfo o = gApi.accounts().id(admin.id().get()).getPreferences();
assertThat(o.changesPerPage).isEqualTo(configuredChangesPerPage);
assertPrefs(o, d, "my", "changeTable", "changesPerPage");
int newChangesPerPage = configuredChangesPerPage * 2;
GeneralPreferencesInfo i = new GeneralPreferencesInfo();
i.changesPerPage = newChangesPerPage;
- GeneralPreferencesInfo a = gApi.accounts().id(admin.id().toString()).setPreferences(i);
+ GeneralPreferencesInfo a = gApi.accounts().id(admin.id().get()).setPreferences(i);
assertThat(a.changesPerPage).isEqualTo(newChangesPerPage);
assertPrefs(a, d, "my", "changeTable", "changesPerPage");
- a = gApi.accounts().id(admin.id().toString()).getPreferences();
+ a = gApi.accounts().id(admin.id().get()).getPreferences();
assertThat(a.changesPerPage).isEqualTo(newChangesPerPage);
assertPrefs(a, d, "my", "changeTable", "changesPerPage");
// overwrite the configured default with original hard-coded default
i = new GeneralPreferencesInfo();
i.changesPerPage = d.changesPerPage;
- a = gApi.accounts().id(admin.id().toString()).setPreferences(i);
+ a = gApi.accounts().id(admin.id().get()).setPreferences(i);
assertThat(a.changesPerPage).isEqualTo(d.changesPerPage);
assertPrefs(a, d, "my", "changeTable", "changesPerPage");
- a = gApi.accounts().id(admin.id().toString()).getPreferences();
+ a = gApi.accounts().id(admin.id().get()).getPreferences();
assertThat(a.changesPerPage).isEqualTo(d.changesPerPage);
assertPrefs(a, d, "my", "changeTable", "changesPerPage");
}
@@ -162,7 +164,7 @@ public class GeneralPreferencesIT extends AbstractDaemonTest {
BadRequestException thrown =
assertThrows(
BadRequestException.class,
- () -> gApi.accounts().id(user42.id().toString()).setPreferences(i));
+ () -> gApi.accounts().id(user42.id().get()).setPreferences(i));
assertThat(thrown).hasMessageThat().contains("name for menu item is required");
}
@@ -175,7 +177,7 @@ public class GeneralPreferencesIT extends AbstractDaemonTest {
BadRequestException thrown =
assertThrows(
BadRequestException.class,
- () -> gApi.accounts().id(user42.id().toString()).setPreferences(i));
+ () -> gApi.accounts().id(user42.id().get()).setPreferences(i));
assertThat(thrown).hasMessageThat().contains("URL for menu item is required");
}
@@ -185,7 +187,7 @@ public class GeneralPreferencesIT extends AbstractDaemonTest {
i.my = new ArrayList<>();
i.my.add(new MenuItem(" name\t", " url\t", " _blank\t", " id\t"));
- GeneralPreferencesInfo o = gApi.accounts().id(user42.id().toString()).setPreferences(i);
+ GeneralPreferencesInfo o = gApi.accounts().id(user42.id().get()).setPreferences(i);
assertThat(o.my).containsExactly(new MenuItem("name", "url", "_blank", "id"));
}
@@ -197,7 +199,7 @@ public class GeneralPreferencesIT extends AbstractDaemonTest {
BadRequestException thrown =
assertThrows(
BadRequestException.class,
- () -> gApi.accounts().id(user42.id().toString()).setPreferences(i));
+ () -> gApi.accounts().id(user42.id().get()).setPreferences(i));
assertThat(thrown)
.hasMessageThat()
.contains("Unsupported download scheme: " + i.downloadScheme);
@@ -211,10 +213,10 @@ public class GeneralPreferencesIT extends AbstractDaemonTest {
GeneralPreferencesInfo i = GeneralPreferencesInfo.defaults();
i.downloadScheme = schemeName;
- GeneralPreferencesInfo o = gApi.accounts().id(user42.id().toString()).setPreferences(i);
+ GeneralPreferencesInfo o = gApi.accounts().id(user42.id().get()).setPreferences(i);
assertThat(o.downloadScheme).isEqualTo(schemeName);
- o = gApi.accounts().id(user42.id().toString()).getPreferences();
+ o = gApi.accounts().id(user42.id().get()).getPreferences();
assertThat(o.downloadScheme).isEqualTo(schemeName);
}
}
@@ -225,7 +227,7 @@ public class GeneralPreferencesIT extends AbstractDaemonTest {
// becomes unsupported.
setDownloadScheme();
- GeneralPreferencesInfo o = gApi.accounts().id(user42.id().toString()).getPreferences();
+ GeneralPreferencesInfo o = gApi.accounts().id(user42.id().get()).getPreferences();
assertThat(o.downloadScheme).isNull();
}
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java
index 03096463eb..984b32dc18 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java
@@ -18,6 +18,8 @@ import static com.google.common.truth.Truth.assertThat;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Address;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.RefNames;
@@ -34,12 +36,8 @@ public class MessageIdGeneratorIT extends AbstractDaemonTest {
@Test
public void fromAccountUpdate() throws Exception {
- try (Repository repo = repoManager.openRepository(allUsers)) {
- String messageId = messageIdGenerator.fromAccountUpdate(admin.id()).id();
- String sha1 =
- repo.getRefDatabase().findRef(RefNames.refsUsers(admin.id())).getObjectId().getName();
- assertThat(sha1).isEqualTo(messageId);
- }
+ String messageId = messageIdGenerator.fromAccountUpdate(admin.id()).id();
+ validateAccountUpdateMessageId(messageId, admin.id());
}
@Test
@@ -78,4 +76,13 @@ public class MessageIdGeneratorIT extends AbstractDaemonTest {
messageIdGenerator.fromReasonAccountIdAndTimestamp(reason, admin.id(), timestamp).id())
.isEqualTo(reason + "-" + admin.id().toString() + "-" + timestamp.toString());
}
+
+ @UsedAt(UsedAt.Project.GOOGLE)
+ protected void validateAccountUpdateMessageId(String messageId, Account.Id id) throws Exception {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ String sha1 =
+ repo.getRefDatabase().findRef(RefNames.refsUsers(admin.id())).getObjectId().getName();
+ assertThat(sha1).isEqualTo(messageId);
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
index b80ff9b1a4..f0f262ff18 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
@@ -76,10 +76,10 @@ public class AbandonIT extends AbstractDaemonTest {
@Test
public void batchAbandon() throws Exception {
- CurrentUser user = atrScope.get().getUser();
+ CurrentUser user = localCtx.getContext().getUser();
PushOneCommit.Result a = createChange();
PushOneCommit.Result b = createChange();
- List<ChangeData> list = ImmutableList.of(a.getChange(), b.getChange());
+ ImmutableList<ChangeData> list = ImmutableList.of(a.getChange(), b.getChange());
batchAbandon.batchAbandon(batchUpdateFactory, a.getChange().project(), user, list, "deadbeef");
ChangeInfo info = get(a.getChangeId(), MESSAGES);
@@ -106,10 +106,10 @@ public class AbandonIT extends AbstractDaemonTest {
TestRepository<InMemoryRepository> project1 = cloneProject(Project.nameKey(project1Name));
TestRepository<InMemoryRepository> project2 = cloneProject(Project.nameKey(project2Name));
- CurrentUser user = atrScope.get().getUser();
+ CurrentUser user = localCtx.getContext().getUser();
PushOneCommit.Result a = createChange(project1, "master", "x", "x", "x", "");
PushOneCommit.Result b = createChange(project2, "master", "x", "x", "x", "");
- List<ChangeData> list = ImmutableList.of(a.getChange(), b.getChange());
+ ImmutableList<ChangeData> list = ImmutableList.of(a.getChange(), b.getChange());
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class,
@@ -177,8 +177,8 @@ public class AbandonIT extends AbstractDaemonTest {
assertThat(query("is:abandoned")).isEmpty();
// submit one of the conflicting changes
- gApi.changes().id(id3).current().review(ReviewInput.approve());
- gApi.changes().id(id3).current().submit();
+ gApi.changes().id(project.get(), id3).current().review(ReviewInput.approve());
+ gApi.changes().id(project.get(), id3).current().submit();
assertThat(toChangeNumbers(query("is:merged"))).containsExactly(id3);
assertThat(toChangeNumbers(query("-is:mergeable"))).containsExactly(id4);
@@ -221,8 +221,8 @@ public class AbandonIT extends AbstractDaemonTest {
assertThat(query("is:abandoned")).isEmpty();
// submit one of the conflicting changes
- gApi.changes().id(id3).current().review(ReviewInput.approve());
- gApi.changes().id(id3).current().submit();
+ gApi.changes().id(project.get(), id3).current().review(ReviewInput.approve());
+ gApi.changes().id(project.get(), id3).current().submit();
assertThat(toChangeNumbers(query("is:merged"))).containsExactly(id3);
BadRequestException thrown =
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
index e9fac73cdb..78361a132b 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
@@ -28,7 +28,6 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.HEAD;
import com.google.common.collect.ImmutableList;
-import com.google.common.hash.Hashing;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
@@ -57,6 +56,7 @@ import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.restapi.change.ApplyPatchUtil;
import com.google.inject.Inject;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
@@ -220,6 +220,25 @@ public class ApplyPatchIT extends AbstractDaemonTest {
}
@Test
+ public void applyDecodedPatchConsistsOfBase64CharsOnly_success() throws Exception {
+ final String deletedFileName = "file_to_be_deleted";
+ final String deletedFileOriginalContent =
+ "The deletion patch of this file only contain valid Base64 chars.\n"
+ + "However, the patch is not Base64-encoded.\n";
+ final String deletedFileDiff =
+ "diff --git a/file_to_be_deleted b/file_to_be_deleted\n"
+ + "--- a/file_to_be_deleted\n"
+ + "+++ /dev/null\n";
+ initBaseWithFile(deletedFileName, deletedFileOriginalContent);
+ ApplyPatchPatchSetInput in = buildInput(deletedFileDiff);
+
+ ChangeInfo result = applyPatch(in);
+
+ DiffInfo diff = fetchDiffForFile(result, deletedFileName);
+ assertDiffForDeletedFile(diff, deletedFileName, deletedFileOriginalContent);
+ }
+
+ @Test
public void applyGerritBasedPatchWithSingleFile_success() throws Exception {
String head = getHead(repo(), HEAD).name();
createBranchWithRevision(BranchNameKey.create(project, "branch"), head);
@@ -363,7 +382,7 @@ public class ApplyPatchIT extends AbstractDaemonTest {
+ "\n[[[Original patch trimmed due to size. Decoded string size: "
+ removePatchHeader(patch).length()
+ ". Decoded string SHA1: "
- + Hashing.sha1().hashString(removePatchHeader(patch), UTF_8)
+ + ApplyPatchUtil.sha1(removePatchHeader(patch))
+ ".]]]"
+ "\n\nChange-Id: "
+ result.changeId
@@ -591,6 +610,24 @@ public class ApplyPatchIT extends AbstractDaemonTest {
}
@Test
+ public void longCommitMessage_providedMessageWithCorrectChangeId() throws Exception {
+ initDestBranch();
+ String originalChangeId =
+ gApi.changes()
+ .create(new ChangeInput(project.get(), DESTINATION_BRANCH, "Default commit message"))
+ .info()
+ .changeId;
+ ApplyPatchPatchSetInput in = buildInput(ADDED_FILE_DIFF);
+ in.commitMessage =
+ "Looooooooooooooooooong custom commit message.\n\nChange-Id: " + originalChangeId + "\n";
+
+ ChangeInfo result = gApi.changes().id(originalChangeId).applyPatch(in);
+
+ ChangeInfo info = get(result.changeId, CURRENT_REVISION, CURRENT_COMMIT);
+ assertThat(info.revisions.get(info.currentRevision).commit.message).isEqualTo(in.commitMessage);
+ }
+
+ @Test
public void commitMessage_providedMessageWithWrongChangeId() throws Exception {
initDestBranch();
String originalChangeId =
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index cab92aaae8..47582546b9 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -17,7 +17,6 @@ package com.google.gerrit.acceptance.api.change;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
@@ -96,6 +95,7 @@ import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.common.UsedAt;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
@@ -149,6 +149,7 @@ import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.common.CommitMessageInfo;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.common.TrackingIdInfo;
@@ -188,6 +189,7 @@ import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.context.RefUpdateContext;
import com.google.gerrit.server.util.AccountTemplateUtil;
+import com.google.gerrit.server.util.CommitMessageUtil;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.inject.AbstractModule;
@@ -215,6 +217,7 @@ import java.util.stream.Stream;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
@@ -266,6 +269,7 @@ public class ChangeIT extends AbstractDaemonTest {
assertThat(c.changeId).isEqualTo(r.getChangeId());
assertThat(c.created).isEqualTo(c.updated);
assertThat(c._number).isEqualTo(r.getChange().getId().get());
+ assertThat(c.currentRevisionNumber).isEqualTo(r.getPatchSetId().get());
assertThat(c.owner._accountId).isEqualTo(admin.id().get());
assertThat(c.owner.name).isNull();
@@ -306,7 +310,9 @@ public class ChangeIT extends AbstractDaemonTest {
String triplet = project.get() + "~master~" + result.getChangeId();
CacheStats startIntra = cloneStats(intraCache.stats());
CacheStats startSummary = cloneStats(diffSummaryCache.stats());
- gApi.changes().id(triplet).get(ImmutableList.of(ListChangesOption.SKIP_DIFFSTAT));
+
+ @SuppressWarnings("unused")
+ var unused = gApi.changes().id(triplet).get(ImmutableList.of(ListChangesOption.SKIP_DIFFSTAT));
assertThat(intraCache.stats()).since(startIntra).hasMissCount(0);
assertThat(intraCache.stats()).since(startIntra).hasHitCount(0);
@@ -509,7 +515,7 @@ public class ChangeIT extends AbstractDaemonTest {
.reviewer("byemail3@example.com", CC, false)
.reviewer("byemail4@example.com", CC, false);
ReviewResult result = gApi.changes().id(changeId).current().review(in);
- assertThat(result.changeInfo).isNull();
+ assertThat(result.changeInfo).isNotNull();
assertThat(result.reviewers).isNotEmpty();
ChangeInfo info = gApi.changes().id(changeId).get();
Function<Collection<AccountInfo>, Collection<String>> toEmails =
@@ -821,7 +827,9 @@ public class ChangeIT extends AbstractDaemonTest {
public void getAmbiguous() throws Exception {
PushOneCommit.Result r1 = createChange();
String changeId = r1.getChangeId();
- gApi.changes().id(changeId).get();
+
+ @SuppressWarnings("unused")
+ var unused = gApi.changes().id(changeId).get();
BranchInput b = new BranchInput();
b.revision = repo().exactRef("HEAD").getObjectId().name();
@@ -1119,17 +1127,20 @@ public class ChangeIT extends AbstractDaemonTest {
gApi.changes().id(r.getChangeId()).current().createDraft(dri);
Change.Id num = r.getChange().getId();
- try (Repository repo = repoManager.openRepository(allUsers)) {
- assertThat(repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftComments(num, user.id())))
- .isNotEmpty();
- }
+ assertThat(getDraftsCountForChange(num, user.id())).isGreaterThan(0);
requestScopeOperations.setApiUser(admin.id());
gApi.changes().id(r.getChangeId()).delete();
+ assertThat(getDraftsCountForChange(num, user.id())).isEqualTo(0);
+ }
+
+ @UsedAt(UsedAt.Project.GOOGLE)
+ protected int getDraftsCountForChange(Change.Id changeId, Account.Id accountId) throws Exception {
try (Repository repo = repoManager.openRepository(allUsers)) {
- assertThat(repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftComments(num, user.id())))
- .isEmpty();
+ return repo.getRefDatabase()
+ .getRefsByPrefix(RefNames.refsDraftComments(changeId, accountId))
+ .size();
}
}
@@ -1146,11 +1157,13 @@ public class ChangeIT extends AbstractDaemonTest {
.modifyFile(FILE_NAME, RawInputUtil.create("foo".getBytes(UTF_8)));
requestScopeOperations.setApiUser(admin.id());
+ String expected = RefNames.refsUsers(user.id()) + "/edit-" + result.getChange().getId() + "/1";
try (Repository repo = repoManager.openRepository(project)) {
- String expected =
- RefNames.refsUsers(user.id()) + "/edit-" + result.getChange().getId() + "/1";
assertThat(repo.getRefDatabase().getRefsByPrefix(expected)).isNotEmpty();
gApi.changes().id(changeId).delete();
+ }
+ // On google infra, repo should be reopened for getting updated refs.
+ try (Repository repo = repoManager.openRepository(project)) {
assertThat(repo.getRefDatabase().getRefsByPrefix(expected)).isEmpty();
}
}
@@ -1177,7 +1190,6 @@ public class ChangeIT extends AbstractDaemonTest {
@Test
public void attentionSetListener_firesOnChange() throws Exception {
PushOneCommit.Result r1 = createChange();
- AttentionSetInput addUser = new AttentionSetInput(user.email(), "some reason");
TestAttentionSetListener attentionSetListener = new TestAttentionSetListener();
try (Registration registration =
@@ -1191,12 +1203,24 @@ public class ChangeIT extends AbstractDaemonTest {
.usersAdded()
.forEach(u -> assertThat(u).isEqualTo(user.id().get()));
+ // Adding the user with the same reason doesn't fire an event.
+ AttentionSetInput addUser = new AttentionSetInput(user.email(), "Reviewer was added");
gApi.changes().id(r1.getChangeId()).addToAttentionSet(addUser);
assertThat(attentionSetListener.firedCount).isEqualTo(1);
- gApi.changes().id(r1.getChangeId()).attention(user.username()).remove(addUser);
-
+ // Adding the user with a different reason fires an event.
+ addUser = new AttentionSetInput(user.email(), "some reason");
+ gApi.changes().id(r1.getChangeId()).addToAttentionSet(addUser);
assertThat(attentionSetListener.firedCount).isEqualTo(2);
+ assertThat(attentionSetListener.lastEvent.usersAdded().size()).isEqualTo(1);
+ attentionSetListener
+ .lastEvent
+ .usersAdded()
+ .forEach(u -> assertThat(u).isEqualTo(user.id().get()));
+
+ // Removing the user fires an event.
+ gApi.changes().id(r1.getChangeId()).attention(user.username()).remove(addUser);
+ assertThat(attentionSetListener.firedCount).isEqualTo(3);
assertThat(attentionSetListener.lastEvent.usersAdded()).isEmpty();
assertThat(attentionSetListener.lastEvent.usersRemoved().size()).isEqualTo(1);
attentionSetListener
@@ -1271,7 +1295,7 @@ public class ChangeIT extends AbstractDaemonTest {
assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
assertThat(change.reviewers.get(REVIEWER)).isNull();
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.from().name()).isEqualTo("Administrator (Code Review)");
@@ -1341,7 +1365,7 @@ public class ChangeIT extends AbstractDaemonTest {
assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
assertThat(change.reviewers.get(CC)).isNull();
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(user.getNameEmail());
@@ -1440,7 +1464,9 @@ public class ChangeIT extends AbstractDaemonTest {
assertThat(r.reviewers).hasSize(1);
ReviewerInfo reviewer = r.reviewers.get(0);
assertThat(reviewer._accountId).isEqualTo(id.get());
- assertThat(reviewer.username).isEqualTo(username);
+ if (server.isUsernameSupported()) {
+ assertThat(reviewer.username).isEqualTo(username);
+ }
}
@Test
@@ -1459,7 +1485,9 @@ public class ChangeIT extends AbstractDaemonTest {
assertThat(r.reviewers).hasSize(1);
ReviewerInfo reviewer = r.reviewers.get(0);
assertThat(reviewer._accountId).isEqualTo(id.get());
- assertThat(reviewer.username).isEqualTo(username);
+ if (server.isUsernameSupported()) {
+ assertThat(reviewer.username).isEqualTo(username);
+ }
}
@Test
@@ -1468,20 +1496,20 @@ public class ChangeIT extends AbstractDaemonTest {
conf.enableReviewerByEmail = InheritableBoolean.TRUE;
gApi.projects().name(project.get()).config(conf);
PushOneCommit.Result result = createChange();
- String username = "user@domain.com";
- Account.Id id = accountOperations.newAccount().username(username).inactive().create();
+ String email = "user@domain.com";
+ Account.Id id = accountOperations.newAccount().preferredEmail(email).inactive().create();
ReviewerInput in = new ReviewerInput();
- in.reviewer = username;
+ in.reviewer = email;
in.state = ReviewerState.CC;
ReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
- assertThat(r.input).isEqualTo(username);
+ assertThat(r.input).isEqualTo(email);
assertThat(r.error).isNull();
assertThat(r.ccs).hasSize(1);
AccountInfo reviewer = r.ccs.get(0);
assertThat(reviewer._accountId).isEqualTo(id.get());
- assertThat(reviewer.username).isEqualTo(username);
+ assertThat(reviewer.email).isEqualTo(email);
}
@Test
@@ -1516,7 +1544,7 @@ public class ChangeIT extends AbstractDaemonTest {
addReviewer.call(r.getChangeId(), user.email());
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(user.getNameEmail());
@@ -1570,17 +1598,22 @@ public class ChangeIT extends AbstractDaemonTest {
String username1 = name("user1");
String email1 = username1 + "@example.com";
- accountOperations
- .newAccount()
- .username(username1)
- .preferredEmail(email1)
- .fullname("User1")
- .create();
+ Account.Id user1Id =
+ accountOperations
+ .newAccount()
+ .username(username1)
+ .preferredEmail(email1)
+ .fullname("User1")
+ .create();
in.reviewer = email1;
in.state = ReviewerState.CC;
gApi.changes().id(r.getChangeId()).addReviewer(in);
- assertThat(gApi.changes().id(r.getChangeId()).reviewers().stream().map(a -> a.username))
- .containsExactly(user.username(), username1);
+ if (server.isUsernameSupported()) {
+ assertThat(gApi.changes().id(r.getChangeId()).reviewers().stream().map(a -> a.username))
+ .containsExactly(user.username(), username1);
+ }
+ assertThat(gApi.changes().id(r.getChangeId()).reviewers().stream().map(a -> a._accountId))
+ .containsExactly(user.id().get(), user1Id.get());
}
@Test
@@ -1644,7 +1677,7 @@ public class ChangeIT extends AbstractDaemonTest {
in.reviewer = "abc";
gApi.changes().id(r.getChangeId()).addReviewer(in.reviewer);
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(Address.create(fullname, email));
@@ -1705,7 +1738,7 @@ public class ChangeIT extends AbstractDaemonTest {
in.reviewer = testGroup;
gApi.changes().id(r.getChangeId()).addReviewer(in.reviewer);
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(Address.create(myGroupUserFullname, myGroupUserEmail));
@@ -1804,7 +1837,9 @@ public class ChangeIT extends AbstractDaemonTest {
@Test
public void implicitlyCcOnNonVotingReviewForUserWithoutUserNamePgStyle() throws Exception {
com.google.gerrit.acceptance.TestAccount accountWithoutUsername = accountCreator.create();
- assertThat(accountWithoutUsername.username()).isNull();
+ if (server.isUsernameSupported()) {
+ assertThat(accountWithoutUsername.username()).isNull();
+ }
testImplicitlyCcOnNonVotingReviewPgStyle(accountWithoutUsername);
}
@@ -2355,7 +2390,7 @@ public class ChangeIT extends AbstractDaemonTest {
.reviewer(user.id().toString())
.deleteVote(LabelId.CODE_REVIEW);
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message msg = messages.get(0);
assertThat(msg.rcpt()).containsExactly(user.getNameEmail());
@@ -2969,7 +3004,8 @@ public class ChangeIT extends AbstractDaemonTest {
c = gApi.changes().id(r.getChangeId()).info();
assertThat(c.submitted).isNotNull();
assertThat(c.submitter).isNotNull();
- assertThat(c.submitter._accountId).isEqualTo(atrScope.get().getUser().getAccountId().get());
+ assertThat(c.submitter._accountId)
+ .isEqualTo(localCtx.getContext().getUser().getAccountId().get());
}
@Test
@@ -3452,7 +3488,9 @@ public class ChangeIT extends AbstractDaemonTest {
// permittedVotingRange is not served if DETAILED_LABELS is not requested.
assertThat(codeReviewApproval.permittedVotingRange).isNull();
assertThat(codeReviewApproval.value).isEqualTo(1);
- assertThat(codeReviewApproval.username).isEqualTo(admin.username());
+ if (server.isUsernameSupported()) {
+ assertThat(codeReviewApproval.username).isEqualTo(admin.username());
+ }
// Add another +1 vote as user
requestScopeOperations.setApiUser(user.id());
@@ -3468,8 +3506,12 @@ public class ChangeIT extends AbstractDaemonTest {
.containsExactly(null, null);
assertThat(codeReviewApprovals.stream().map(a -> a.value).collect(toList()))
.containsExactly(1, 1);
- assertThat(codeReviewApprovals.stream().map(a -> a.username).collect(toList()))
- .containsExactly(admin.username(), user.username());
+ if (server.isUsernameSupported()) {
+ assertThat(codeReviewApprovals.stream().map(a -> a.username).collect(toList()))
+ .containsExactly(admin.username(), user.username());
+ }
+ assertThat(codeReviewApprovals.stream().map(a -> a._accountId).collect(toList()))
+ .containsExactly(admin.id().get(), user.id().get());
}
@Test
@@ -3584,12 +3626,16 @@ public class ChangeIT extends AbstractDaemonTest {
assertThat(codeReviewApprovals).hasSize(1);
assertThat(codeReviewApprovals.get(0).value).isEqualTo(2);
- assertThat(codeReviewApprovals.get(0).username).isEqualTo(admin.username());
+ if (server.isUsernameSupported()) {
+ assertThat(codeReviewApprovals.get(0).username).isEqualTo(admin.username());
+ }
assertThat(codeReviewApprovals.get(0).permittedVotingRange).isNull();
assertThat(verifiedApprovals).hasSize(1);
assertThat(verifiedApprovals.get(0).value).isEqualTo(1);
- assertThat(verifiedApprovals.get(0).username).isEqualTo(admin.username());
+ if (server.isUsernameSupported()) {
+ assertThat(verifiedApprovals.get(0).username).isEqualTo(admin.username());
+ }
assertThat(codeReviewApprovals.get(0).permittedVotingRange).isNull();
}
@@ -3738,7 +3784,6 @@ public class ChangeIT extends AbstractDaemonTest {
@Test
public void uploadingRulesPlIsNotAllowed() throws Exception {
- projectOperations.project(project).getHead("master");
GitUtil.fetch(testRepo, RefNames.REFS_CONFIG + ":config");
testRepo.reset("config");
PushOneCommit.Result pushResult =
@@ -3974,6 +4019,22 @@ public class ChangeIT extends AbstractDaemonTest {
}
@Test
+ public void getCommitMessage() throws Exception {
+ String subject = "Change Subject";
+ String changeId = "I" + ObjectId.toString(CommitMessageUtil.generateChangeId());
+ String commitMessage =
+ String.format(
+ "%s\n\nFirst Paragraph.\n\nSecond Paragraph\n\nFoo: Bar\nChange-Id: %s\n",
+ subject, changeId);
+ changeOperations.newChange().project(project).commitMessage(commitMessage).create();
+
+ CommitMessageInfo commitMessageInfo = gApi.changes().id(changeId).getMessage();
+ assertThat(commitMessageInfo.subject).isEqualTo(subject);
+ assertThat(commitMessageInfo.fullMessage).isEqualTo(commitMessage);
+ assertThat(commitMessageInfo.footers).containsExactly("Foo", "Bar", "Change-Id", changeId);
+ }
+
+ @Test
public void changeCommitMessage() throws Exception {
// Tests mutating the commit message as both the owner of the change and a regular user with
// addPatchSet permission. Asserts that both cases succeed.
@@ -4121,6 +4182,8 @@ public class ChangeIT extends AbstractDaemonTest {
PushOneCommit push = pushFactory.create(user.newIdent(), userTestRepo);
PushOneCommit.Result r = push.to("refs/for/master");
r.assertOkStatus();
+ assertThat(gApi.changes().id(r.getChangeId()).info().owner._accountId)
+ .isEqualTo(user.id().get());
// Try to change the commit message
AuthException thrown =
assertThrows(
@@ -4373,7 +4436,7 @@ public class ChangeIT extends AbstractDaemonTest {
private void setChangeStatus(Change.Id id, Change.Status newStatus) throws Exception {
try (RefUpdateContext ctx = openTestRefUpdateContext()) {
try (BatchUpdate batchUpdate =
- batchUpdateFactory.create(project, atrScope.get().getUser(), TimeUtil.now())) {
+ batchUpdateFactory.create(project, localCtx.getContext().getUser(), TimeUtil.now())) {
batchUpdate.addOp(id, new ChangeStatusUpdateOp(newStatus));
batchUpdate.execute();
}
@@ -4513,7 +4576,7 @@ public class ChangeIT extends AbstractDaemonTest {
@Test
public void changeDetailsDoesNotRequireIndex() throws Exception {
// This set of options must be kept in sync with gr-rest-api-interface.js
- Set<ListChangesOption> options =
+ ImmutableSet<ListChangesOption> options =
ImmutableSet.of(
ListChangesOption.ALL_COMMITS,
ListChangesOption.ALL_REVISIONS,
@@ -4734,7 +4797,7 @@ public class ChangeIT extends AbstractDaemonTest {
}
sender.clear();
gApi.changes().id(change).addReviewer(user.email());
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
assertThat(((StringEmailHeader) messages.get(0).headers().get("Subject")).getString())
.contains("[" + expectedSizeBucket + "]");
@@ -4746,7 +4809,8 @@ public class ChangeIT extends AbstractDaemonTest {
private ThrowableSubject assertThatQueryException(String query) throws Exception {
try {
- query(query);
+ @SuppressWarnings("unused")
+ var unused = query(query);
} catch (BadRequestException e) {
return assertThat(e);
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/CopiedApprovalsInChangeMessageIT.java b/javatests/com/google/gerrit/acceptance/api/change/CopiedApprovalsInChangeMessageIT.java
index 2b1bef0358..bc210f0338 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/CopiedApprovalsInChangeMessageIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/CopiedApprovalsInChangeMessageIT.java
@@ -15,7 +15,6 @@
package com.google.gerrit.acceptance.api.change;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.project.testing.TestLabels.labelBuilder;
diff --git a/javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsToFollowUpPatchSetsIT.java b/javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsToFollowUpPatchSetsIT.java
index a055201d73..b2aa4fe075 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsToFollowUpPatchSetsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsToFollowUpPatchSetsIT.java
@@ -16,7 +16,6 @@ package com.google.gerrit.acceptance.api.change;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
index 1b06b7b252..4d8566db69 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
@@ -24,6 +24,7 @@ import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.util.stream.Collectors.toList;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -59,10 +60,12 @@ import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
import com.google.gerrit.extensions.api.changes.ReviewResult;
+import com.google.gerrit.extensions.api.changes.ReviewerInput;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.RobotCommentInfo;
import com.google.gerrit.extensions.config.FactoryModule;
@@ -92,6 +95,7 @@ import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -152,6 +156,10 @@ public class PostReviewIT extends AbstractDaemonTest {
@Override
public void configure() {
CommentValidator mockCommentValidator = mock(CommentValidator.class);
+
+ // by default return no validation errors
+ when(mockCommentValidator.validateComments(any(), any())).thenReturn(ImmutableList.of());
+
bind(CommentValidator.class)
.annotatedWith(Exports.named(mockCommentValidator.getClass()))
.toInstance(mockCommentValidator);
@@ -757,6 +765,26 @@ public class PostReviewIT extends AbstractDaemonTest {
}
@Test
+ public void currentRevisionNumberIsSetOnReturnedChangeInfo() throws Exception {
+ PushOneCommit.Result r = createChange();
+ ChangeInfo changeInfo =
+ gApi.changes().id(r.getChangeId()).current().review(ReviewInput.dislike()).changeInfo;
+ assertThat(changeInfo.currentRevisionNumber).isEqualTo(1);
+ amendChange(r.getChangeId());
+ changeInfo =
+ gApi.changes().id(r.getChangeId()).current().review(ReviewInput.recommend()).changeInfo;
+ assertThat(changeInfo.currentRevisionNumber).isEqualTo(2);
+
+ // Check that the current revision number is also returned when list changes options are
+ // requested.
+ ReviewInput reviewInput = ReviewInput.approve();
+ reviewInput.responseFormatOptions =
+ ImmutableList.copyOf(EnumSet.allOf(ListChangesOption.class));
+ changeInfo = gApi.changes().id(r.getChangeId()).current().review(reviewInput).changeInfo;
+ assertThat(changeInfo.currentRevisionNumber).isEqualTo(2);
+ }
+
+ @Test
public void submitRulesAreInvokedOnlyOnce() throws Exception {
PushOneCommit.Result r = createChange();
@@ -770,6 +798,20 @@ public class PostReviewIT extends AbstractDaemonTest {
}
@Test
+ public void submitRulesAreInvokedOnlyOnce_allOptionsSet() throws Exception {
+ PushOneCommit.Result r = createChange();
+
+ TestSubmitRule testSubmitRule = new TestSubmitRule();
+ try (Registration registration = extensionRegistry.newRegistration().add(testSubmitRule)) {
+ ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 1);
+ input.responseFormatOptions = ImmutableList.copyOf(EnumSet.allOf(ListChangesOption.class));
+ gApi.changes().id(r.getChangeId()).current().review(input);
+ }
+
+ assertThat(testSubmitRule.count).isEqualTo(1);
+ }
+
+ @Test
public void addingReviewers() throws Exception {
PushOneCommit.Result r = createChange();
@@ -825,6 +867,41 @@ public class PostReviewIT extends AbstractDaemonTest {
}
@Test
+ public void rejectAddingReviewerIfReviewerUserIdentifierIsMissing() throws Exception {
+ PushOneCommit.Result r = createChange();
+
+ // Add reviewer with ReviewerInput where the 'reviewer' field is not set.
+ ReviewerInput reviewerInput = new ReviewerInput();
+ reviewerInput.state = ReviewerState.REVIEWER;
+
+ ReviewInput reviewInput = ReviewInput.create();
+ reviewInput.reviewers = ImmutableList.of(reviewerInput);
+
+ ReviewResult reviewResult = gApi.changes().id(r.getChangeId()).current().review(reviewInput);
+ assertThat(reviewResult.error).isEqualTo("error adding reviewer");
+ assertThat(reviewResult.reviewers.keySet()).containsExactly(reviewerInput.reviewer);
+ assertThat(reviewResult.reviewers.get(reviewerInput.reviewer).error)
+ .isEqualTo("reviewer user identifier is required");
+
+ // Add reviewer with ReviewerInput where the 'reviewer' field is set to an empty string.
+ reviewerInput.reviewer = "";
+ reviewResult = gApi.changes().id(r.getChangeId()).current().review(reviewInput);
+ assertThat(reviewResult.error).isEqualTo("error adding reviewer");
+ assertThat(reviewResult.reviewers.keySet()).containsExactly(reviewerInput.reviewer);
+ assertThat(reviewResult.reviewers.get(reviewerInput.reviewer).error)
+ .isEqualTo("reviewer user identifier is required");
+
+ // Add reviewer with ReviewerInput where the 'reviewer' field is set to an empty string after
+ // trimming it.
+ reviewerInput.reviewer = " ";
+ reviewResult = gApi.changes().id(r.getChangeId()).current().review(reviewInput);
+ assertThat(reviewResult.error).isEqualTo("error adding reviewer");
+ assertThat(reviewResult.reviewers.keySet()).containsExactly(reviewerInput.reviewer);
+ assertThat(reviewResult.reviewers.get(reviewerInput.reviewer).error)
+ .isEqualTo("reviewer user identifier is required");
+ }
+
+ @Test
public void deletingReviewers() throws Exception {
PushOneCommit.Result r = createChange();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java b/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java
index 465a19aace..c36c9f1c72 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java
@@ -86,7 +86,7 @@ public class QueryChangesIT extends AbstractDaemonTest {
assertThat(result.get(0)).hasSize(2);
assertThat(result.get(1)).hasSize(1);
- List<Integer> firstResultIds =
+ ImmutableList<Integer> firstResultIds =
ImmutableList.of(result.get(0).get(0)._number, result.get(0).get(1)._number);
assertThat(firstResultIds).containsExactly(numericId1, numericId2);
assertThat(result.get(1).get(0)._number).isEqualTo(numericId2);
@@ -97,7 +97,7 @@ public class QueryChangesIT extends AbstractDaemonTest {
public void moreChangesIndicatorDoesNotWronglyCopyToUnrelatedChanges() throws Exception {
String queryWithMoreChanges = "is:wip limit:1 repo:" + project.get();
String queryWithNoMoreChanges = "is:open limit:10 repo:" + project.get();
- createChange().getChangeId();
+ createChange();
String cId2 = createChange().getChangeId();
String cId3 = createChange().getChangeId();
gApi.changes().id(cId2).setWorkInProgress();
@@ -160,8 +160,8 @@ public class QueryChangesIT extends AbstractDaemonTest {
@SuppressWarnings("unchecked")
public void withPagedResults() throws Exception {
// Create 4 visible changes.
- createChange(testRepo).getChange().getId().get();
- createChange(testRepo).getChange().getId().get();
+ createChange(testRepo);
+ createChange(testRepo);
int changeId3 = createChange(testRepo).getChange().getId().get();
int changeId4 = createChange(testRepo).getChange().getId().get();
@@ -291,7 +291,7 @@ public class QueryChangesIT extends AbstractDaemonTest {
assertThat(result.get(0)).hasSize(2);
assertThat(result.get(1)).hasSize(1);
- List<Integer> firstResultIds =
+ ImmutableList<Integer> firstResultIds =
ImmutableList.of(result.get(0).get(0)._number, result.get(0).get(1)._number);
assertThat(firstResultIds).containsExactly(numericId1, numericId2);
assertThat(result.get(1).get(0)._number).isEqualTo(numericId2);
@@ -311,7 +311,7 @@ public class QueryChangesIT extends AbstractDaemonTest {
@Test
@SuppressWarnings("unchecked")
public void skipVisibility_noReadPermission() throws Exception {
- createChange().getChangeId();
+ createChange();
requestScopeOperations.setApiUser(admin.id());
QueryChanges queryChanges = queryChangesProvider.get();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java
index 297579c07c..c32c04b1c1 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java
@@ -15,7 +15,6 @@
package com.google.gerrit.acceptance.api.change;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
index c637916bda..7f21eb6d05 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
@@ -18,12 +18,15 @@ import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.extensions.client.ChangeKind.MERGE_FIRST_PARENT_UPDATE;
import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
import static com.google.gerrit.git.ObjectIds.abbreviateName;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.testing.TestLabels.labelBuilder;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.HEAD;
@@ -44,8 +47,10 @@ import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.AttentionSetInput;
import com.google.gerrit.extensions.api.changes.RebaseInput;
@@ -65,6 +70,7 @@ import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -75,6 +81,7 @@ import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.inject.Inject;
import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@@ -145,6 +152,474 @@ public class RebaseIT {
}
@Test
+ public void rebaseMerge() throws Exception {
+ // Create a new project for this test so that we can configure a copy condition without
+ // affecting any other tests. Copy Code-Review approvals if change kind is
+ // MERGE_FIRST_PARENT_UPDATE. MERGE_FIRST_PARENT_UPDATE is the change kind when a merge commit
+ // is rebased without conflicts.
+ Project.NameKey project = projectOperations.newProject().create();
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType.Builder codeReview =
+ labelBuilder(
+ LabelId.CODE_REVIEW,
+ value(2, "Looks good to me, approved"),
+ value(1, "Looks good to me, but someone else must approve"),
+ value(0, "No score"),
+ value(-1, "I would prefer this is not submitted as is"),
+ value(-2, "This shall not be submitted"))
+ .setCopyCondition("changekind:" + MERGE_FIRST_PARENT_UPDATE.name());
+ u.getConfig().upsertLabelType(codeReview.build());
+ u.save();
+ }
+
+ String file1 = "foo/a.txt";
+ String file2 = "bar/b.txt";
+ String file3 = "baz/c.txt";
+
+ // Create an initial change that adds file1, so that we can modify it later.
+ Change.Id initialChange =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .file(file1)
+ .content("base content")
+ .create();
+ approveAndSubmit(initialChange);
+
+ // Create another branch
+ String branchName = "foo";
+ BranchInput branchInput = new BranchInput();
+ branchInput.ref = branchName;
+ branchInput.revision = projectOperations.project(project).getHead("master").name();
+ gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput);
+
+ // Create a change in master that touches file1.
+ Change.Id baseChangeInMaster =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .file(file1)
+ .content("master content")
+ .create();
+ approveAndSubmit(baseChangeInMaster);
+
+ // Create a change in the other branch and that touches file1 and creates file2.
+ Change.Id changeInOtherBranch =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch(branchName)
+ .file(file1)
+ .content("other content")
+ .file(file2)
+ .content("content")
+ .create();
+ approveAndSubmit(changeInOtherBranch);
+
+ // Create a merge change with a conflict resolution for file1. file2 has the same content as
+ // in the other branch (no conflict on file2).
+ Change.Id mergeChangeId =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .mergeOfButBaseOnFirst()
+ .tipOfBranch("master")
+ .and()
+ .tipOfBranch(branchName)
+ .file(file1)
+ .content("merged content")
+ .file(file2)
+ .content("content")
+ .create();
+
+ // Create a change in master onto which the merge change can be rebased. This change touches
+ // an unrelated file (file3) so that there is no conflict on rebase.
+ Change.Id newBaseChangeInMaster =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .file(file3)
+ .content("other content")
+ .create();
+ approveAndSubmit(newBaseChangeInMaster);
+
+ // Add an approval whose score should be copied on rebase.
+ gApi.changes().id(mergeChangeId.get()).current().review(ReviewInput.recommend());
+
+ // Rebase the merge change
+ rebaseCall.call(mergeChangeId.toString());
+
+ verifyRebaseForChange(
+ mergeChangeId,
+ ImmutableList.of(
+ getCurrentRevision(newBaseChangeInMaster), getCurrentRevision(changeInOtherBranch)),
+ /* shouldHaveApproval= */ true,
+ /* expectedNumRevisions= */ 2);
+
+ // Verify the file contents.
+ assertThat(getFileContent(mergeChangeId, file1)).isEqualTo("merged content");
+ assertThat(getFileContent(mergeChangeId, file2)).isEqualTo("content");
+ assertThat(getFileContent(mergeChangeId, file3)).isEqualTo("other content");
+
+ // Rebasing the merge change again should fail
+ verifyChangeIsUpToDate(mergeChangeId.toString());
+ }
+
+ @Test
+ public void rebaseMergeWithConflict_fails() throws Exception {
+ String file1 = "foo/a.txt";
+ String file2 = "bar/b.txt";
+
+ // Create an initial change that adds file1, so that we can modify it later.
+ Change.Id initialChange =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .file(file1)
+ .content("base content")
+ .create();
+ approveAndSubmit(initialChange);
+
+ // Create another branch
+ String branchName = "foo";
+ BranchInput branchInput = new BranchInput();
+ branchInput.ref = branchName;
+ branchInput.revision = projectOperations.project(project).getHead("master").name();
+ gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput);
+
+ // Create a change in master that touches file1.
+ Change.Id baseChangeInMaster =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .file(file1)
+ .content("master content")
+ .create();
+ approveAndSubmit(baseChangeInMaster);
+
+ // Create a change in the other branch and that touches file1 and creates file2.
+ Change.Id changeInOtherBranch =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch(branchName)
+ .file(file1)
+ .content("other content")
+ .file(file2)
+ .content("content")
+ .create();
+ approveAndSubmit(changeInOtherBranch);
+
+ // Create a merge change with a conflict resolution for file1. file2 has the same content as
+ // in the other branch (no conflict on file2).
+ Change.Id mergeChangeId =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .mergeOfButBaseOnFirst()
+ .tipOfBranch("master")
+ .and()
+ .tipOfBranch(branchName)
+ .file(file1)
+ .content("merged content")
+ .file(file2)
+ .content("content")
+ .create();
+
+ // Create a change in master onto which the merge change can be rebased. This change touches
+ // file1 again so that there is a conflict on rebase.
+ Change.Id newBaseChangeInMaster =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .file(file1)
+ .content("conflicting content")
+ .create();
+ approveAndSubmit(newBaseChangeInMaster);
+
+ // Try to rebase the merge change
+ MergeConflictException mergeConflictException =
+ assertThrows(
+ MergeConflictException.class, () -> rebaseCall.call(mergeChangeId.toString()));
+ assertThat(mergeConflictException)
+ .hasMessageThat()
+ .isEqualTo(
+ String.format(
+ "Change %s could not be rebased due to a conflict during merge.\n"
+ + "\n"
+ + "merge conflict(s):\n"
+ + "%s",
+ mergeChangeId, file1));
+ }
+
+ @Test
+ public void rebaseMergeWithConflict_conflictsAllowed() throws Exception {
+ // Create a new project for this test so that we can configure a copy condition without
+ // affecting any other tests. Copy Code-Review approvals if change kind is
+ // MERGE_FIRST_PARENT_UPDATE. MERGE_FIRST_PARENT_UPDATE is the change kind when a merge commit
+ // is rebased without conflicts.
+ Project.NameKey project = projectOperations.newProject().create();
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType.Builder codeReview =
+ labelBuilder(
+ LabelId.CODE_REVIEW,
+ value(2, "Looks good to me, approved"),
+ value(1, "Looks good to me, but someone else must approve"),
+ value(0, "No score"),
+ value(-1, "I would prefer this is not submitted as is"),
+ value(-2, "This shall not be submitted"))
+ .setCopyCondition("changekind:" + MERGE_FIRST_PARENT_UPDATE.name());
+ u.getConfig().upsertLabelType(codeReview.build());
+ u.save();
+ }
+
+ String file = "foo/a.txt";
+
+ // Create an initial change that adds a file, so that we can modify it later.
+ Change.Id initialChange =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .file(file)
+ .content("base content")
+ .create();
+ approveAndSubmit(initialChange);
+
+ // Create another branch
+ String branchName = "foo";
+ BranchInput branchInput = new BranchInput();
+ branchInput.ref = branchName;
+ branchInput.revision = projectOperations.project(project).getHead("master").name();
+ gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput);
+
+ // Create a change in master that touches the file.
+ Change.Id baseChangeInMaster =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .file(file)
+ .content("master content")
+ .create();
+ approveAndSubmit(baseChangeInMaster);
+
+ // Create a change in the other branch and that also touches the file.
+ Change.Id changeInOtherBranch =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch(branchName)
+ .file(file)
+ .content("other content")
+ .create();
+ approveAndSubmit(changeInOtherBranch);
+
+ // Create a merge change with a conflict resolution.
+ String mergeCommitMessage = "Merge";
+ String mergeContent = "merged content";
+ Change.Id mergeChangeId =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .commitMessage(mergeCommitMessage)
+ .mergeOfButBaseOnFirst()
+ .tipOfBranch("master")
+ .and()
+ .tipOfBranch(branchName)
+ .file(file)
+ .content(mergeContent)
+ .create();
+ String mergeSha1 = abbreviateName(ObjectId.fromString(getCurrentRevision(mergeChangeId)), 6);
+
+ // Create a change in master onto which the merge change can be rebased. This change touches
+ // the file again so that there is a conflict on rebase.
+ String newBaseCommitMessage = "Foo";
+ String newBaseContent = "conflicting content";
+ Change.Id newBaseChangeInMaster =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .commitMessage(newBaseCommitMessage)
+ .file(file)
+ .content(newBaseContent)
+ .create();
+ approveAndSubmit(newBaseChangeInMaster);
+
+ // Add an approval whose score should NOT be copied on rebase (since there is a conflict the
+ // change kind should be REWORK).
+ gApi.changes().id(mergeChangeId.get()).current().review(ReviewInput.recommend());
+
+ // Rebase the merge change with conflicts allowed.
+ TestWorkInProgressStateChangedListener wipStateChangedListener =
+ new TestWorkInProgressStateChangedListener();
+ try (ExtensionRegistry.Registration registration =
+ extensionRegistry.newRegistration().add(wipStateChangedListener)) {
+ RebaseInput rebaseInput = new RebaseInput();
+ rebaseInput.allowConflicts = true;
+ rebaseCallWithInput.call(mergeChangeId.toString(), rebaseInput);
+ }
+ assertThat(wipStateChangedListener.invoked).isTrue();
+ assertThat(wipStateChangedListener.wip).isTrue();
+
+ String baseCommit = getCurrentRevision(newBaseChangeInMaster);
+ verifyRebaseForChange(
+ mergeChangeId,
+ ImmutableList.of(baseCommit, getCurrentRevision(changeInOtherBranch)),
+ /* shouldHaveApproval= */ false,
+ /* expectedNumRevisions= */ 2);
+
+ // Verify the file contents.
+ String baseSha1 = abbreviateName(ObjectId.fromString(baseCommit), 6);
+ assertThat(getFileContent(mergeChangeId, file))
+ .isEqualTo(
+ "<<<<<<< PATCH SET ("
+ + mergeSha1
+ + " "
+ + mergeCommitMessage
+ + ")\n"
+ + mergeContent
+ + "\n"
+ + "=======\n"
+ + newBaseContent
+ + "\n"
+ + ">>>>>>> BASE ("
+ + baseSha1
+ + " "
+ + newBaseCommitMessage
+ + ")\n");
+
+ // Verify that a change message has been posted on the change that informs about the conflict
+ // and the outdated vote.
+ List<ChangeMessageInfo> messages = gApi.changes().id(mergeChangeId.get()).messages();
+ assertThat(messages).hasSize(3);
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ "Patch Set 2: Patch Set 1 was rebased\n\n"
+ + "The following files contain Git conflicts:\n"
+ + "* "
+ + file
+ + "\n\n"
+ + "Outdated Votes:\n"
+ + "* Code-Review+1"
+ + " (copy condition: \"changekind:MERGE_FIRST_PARENT_UPDATE\")\n");
+
+ // Rebasing the merge change again should fail
+ verifyChangeIsUpToDate(mergeChangeId.toString());
+ }
+
+ @Test
+ public void rebaseMergeWithConflict_strategyAcceptTheirs() throws Exception {
+ rebaseMergeWithConflict_strategy("theirs");
+ }
+
+ @Test
+ public void rebaseMergeWithConflict_strategyAcceptOurs() throws Exception {
+ rebaseMergeWithConflict_strategy("ours");
+ }
+
+ private void rebaseMergeWithConflict_strategy(String strategy) throws Exception {
+ String file = "foo/a.txt";
+
+ // Create an initial change that adds a file, so that we can modify it later.
+ Change.Id initialChange =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .file(file)
+ .content("base content")
+ .create();
+ approveAndSubmit(initialChange);
+
+ // Create another branch
+ String branchName = "foo";
+ BranchInput branchInput = new BranchInput();
+ branchInput.ref = branchName;
+ branchInput.revision = projectOperations.project(project).getHead("master").name();
+ gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput);
+
+ // Create a change in master that touches the file.
+ Change.Id baseChangeInMaster =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .file(file)
+ .content("master content")
+ .create();
+ approveAndSubmit(baseChangeInMaster);
+
+ // Create a change in the other branch and that also touches the file.
+ Change.Id changeInOtherBranch =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch(branchName)
+ .file(file)
+ .content("other content")
+ .create();
+ approveAndSubmit(changeInOtherBranch);
+
+ // Create a merge change with a conflict resolution for the file.
+ String mergeContent = "merged content";
+ Change.Id mergeChangeId =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .mergeOfButBaseOnFirst()
+ .tipOfBranch("master")
+ .and()
+ .tipOfBranch(branchName)
+ .file(file)
+ .content(mergeContent)
+ .create();
+
+ // Create a change in master onto which the merge change can be rebased. This change touches
+ // the file again so that there is a conflict on rebase.
+ String newBaseContent = "conflicting content";
+ Change.Id newBaseChangeInMaster =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .file(file)
+ .content(newBaseContent)
+ .create();
+ approveAndSubmit(newBaseChangeInMaster);
+
+ // Rebase the merge change with setting a merge strategy
+ RebaseInput rebaseInput = new RebaseInput();
+ rebaseInput.strategy = strategy;
+ rebaseCallWithInput.call(mergeChangeId.toString(), rebaseInput);
+
+ verifyRebaseForChange(
+ mergeChangeId,
+ ImmutableList.of(
+ getCurrentRevision(newBaseChangeInMaster), getCurrentRevision(changeInOtherBranch)),
+ /* shouldHaveApproval= */ false,
+ /* expectedNumRevisions= */ 2);
+
+ // Verify the file contents.
+ assertThat(getFileContent(mergeChangeId, file))
+ .isEqualTo(strategy.equals("theirs") ? newBaseContent : mergeContent);
+
+ // Rebasing the merge change again should fail
+ verifyChangeIsUpToDate(mergeChangeId.toString());
+ }
+
+ @Test
public void rebaseWithCommitterEmail() throws Exception {
// Create three changes with the same parent
PushOneCommit.Result r1 = createChange();
@@ -365,6 +840,51 @@ public class RebaseIT {
}
@Test
+ public void rebaseChangeWithValidBaseCommit() throws Exception {
+ RevCommit desiredBase =
+ createNewCommitWithoutChangeId(/*branch=*/ "refs/heads/master", "file", "content");
+ PushOneCommit.Result child = createChange();
+ RebaseInput ri = new RebaseInput();
+
+ // rebase child onto desiredBase (referenced by commit)
+ ri.base = desiredBase.getName();
+ rebaseCallWithInput.call(child.getChangeId(), ri);
+
+ PatchSet ps2 = child.getPatchSet();
+ assertThat(ps2.id().get()).isEqualTo(2);
+ RevisionInfo childInfo =
+ get(child.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
+ assertThat(childInfo.commit.parents.get(0).commit).isEqualTo(desiredBase.name());
+ }
+
+ @Test
+ public void cannotRebaseChangeWithInvalidBaseCommit() throws Exception {
+ // Create another branch and push the desired parent commit to it.
+ String branchName = "foo";
+ BranchInput branchInput = new BranchInput();
+ branchInput.ref = branchName;
+ branchInput.revision = projectOperations.project(project).getHead("master").name();
+ gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput);
+ RevCommit desiredBase =
+ createNewCommitWithoutChangeId(/*branch=*/ "refs/heads/foo", "file", "content");
+ // Create the child commit on "master".
+ PushOneCommit.Result child = createChange();
+ RebaseInput ri = new RebaseInput();
+
+ // Try to rebase child onto desiredBase (referenced by commit)
+ ri.base = desiredBase.getName();
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> rebaseCallWithInput.call(child.getChangeId(), ri));
+
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains(
+ String.format("base revision is missing from the destination branch: %s", ri.base));
+ }
+
+ @Test
public void rebaseUpToDateChange() throws Exception {
PushOneCommit.Result r = createChange();
verifyChangeIsUpToDate(r);
@@ -660,6 +1180,23 @@ public class RebaseIT {
/* expectedNumRevisions= */ 2);
}
+ protected void approveAndSubmit(Change.Id changeId) throws Exception {
+ approve(Integer.toString(changeId.get()));
+ gApi.changes().id(changeId.get()).current().submit();
+ }
+
+ protected String getCurrentRevision(Change.Id changeId) throws RestApiException {
+ return gApi.changes().id(changeId.get()).get(CURRENT_REVISION).currentRevision;
+ }
+
+ protected String getFileContent(Change.Id changeId, String file)
+ throws RestApiException, IOException {
+ BinaryResult bin = gApi.changes().id(changeId.get()).current().file(file).content();
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ bin.writeTo(os);
+ return new String(os.toByteArray(), UTF_8);
+ }
+
protected void verifyRebaseForChange(
Change.Id changeId, Change.Id baseChangeId, boolean shouldHaveApproval)
throws RestApiException {
@@ -672,14 +1209,26 @@ public class RebaseIT {
boolean shouldHaveApproval,
int expectedNumRevisions)
throws RestApiException {
- ChangeInfo baseInfo = gApi.changes().id(baseChangeId.get()).get(CURRENT_REVISION);
verifyRebaseForChange(
- changeId, baseInfo.currentRevision, shouldHaveApproval, expectedNumRevisions);
+ changeId,
+ ImmutableList.of(getCurrentRevision(baseChangeId)),
+ shouldHaveApproval,
+ expectedNumRevisions);
}
protected void verifyRebaseForChange(
Change.Id changeId, String baseCommit, boolean shouldHaveApproval, int expectedNumRevisions)
throws RestApiException {
+ verifyRebaseForChange(
+ changeId, ImmutableList.of(baseCommit), shouldHaveApproval, expectedNumRevisions);
+ }
+
+ protected void verifyRebaseForChange(
+ Change.Id changeId,
+ List<String> baseCommits,
+ boolean shouldHaveApproval,
+ int expectedNumRevisions)
+ throws RestApiException {
ChangeInfo info =
gApi.changes().id(changeId.get()).get(CURRENT_REVISION, CURRENT_COMMIT, DETAILED_LABELS);
@@ -688,10 +1237,12 @@ public class RebaseIT {
assertThat(r.realUploader).isNull();
// ...and the base should be correct
- assertThat(r.commit.parents).hasSize(1);
- assertWithMessage("base commit for change " + changeId)
- .that(r.commit.parents.get(0).commit)
- .isEqualTo(baseCommit);
+ assertThat(r.commit.parents).hasSize(baseCommits.size());
+ for (int baseNum = 0; baseNum < baseCommits.size(); baseNum++) {
+ assertWithMessage("base commit " + baseNum + " for change " + changeId)
+ .that(r.commit.parents.get(baseNum).commit)
+ .isEqualTo(baseCommits.get(baseNum));
+ }
// ...and the committer and description should be correct
GitPerson committer = r.commit.committer;
@@ -711,8 +1262,12 @@ public class RebaseIT {
}
protected void verifyChangeIsUpToDate(PushOneCommit.Result r) {
+ verifyChangeIsUpToDate(r.getChangeId());
+ }
+
+ protected void verifyChangeIsUpToDate(String changeId) {
ResourceConflictException thrown =
- assertThrows(ResourceConflictException.class, () -> rebaseCall.call(r.getChangeId()));
+ assertThrows(ResourceConflictException.class, () -> rebaseCall.call(changeId));
assertThat(thrown).hasMessageThat().contains("Change is already up to date");
}
@@ -1167,9 +1722,9 @@ public class RebaseIT {
}
@Override
- protected void verifyChangeIsUpToDate(PushOneCommit.Result r) {
+ protected void verifyChangeIsUpToDate(String changeId) {
ResourceConflictException thrown =
- assertThrows(ResourceConflictException.class, () -> rebaseCall.call(r.getChangeId()));
+ assertThrows(ResourceConflictException.class, () -> rebaseCall.call(changeId));
assertThat(thrown).hasMessageThat().contains("The whole chain is already up to date.");
}
@@ -1294,6 +1849,152 @@ public class RebaseIT {
}
@Test
+ public void rebaseChainWithMerges() throws Exception {
+ String file1 = "foo/a.txt";
+ String file2 = "bar/b.txt";
+
+ // Create an initial change that adds file1, so that we can modify it later.
+ Change.Id initialChange =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .file(file1)
+ .content("base content")
+ .create();
+ approveAndSubmit(initialChange);
+
+ // Create another branch
+ String branchName = "foo";
+ BranchInput branchInput = new BranchInput();
+ branchInput.ref = branchName;
+ branchInput.revision = projectOperations.project(project).getHead("master").name();
+ gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput);
+
+ // Create a change in master that touches file1.
+ Change.Id baseChangeInMaster =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .file(file1)
+ .content("master content")
+ .create();
+ approveAndSubmit(baseChangeInMaster);
+
+ // Create a change in the other branch and that also touches file1.
+ Change.Id changeInOtherBranch =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch(branchName)
+ .file(file1)
+ .content("other content")
+ .create();
+ approveAndSubmit(changeInOtherBranch);
+
+ // Create a merge change with a conflict resolution.
+ Change.Id mergeChangeId =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .mergeOfButBaseOnFirst()
+ .tipOfBranch("master")
+ .and()
+ .tipOfBranch(branchName)
+ .file(file1)
+ .content("merged content")
+ .create();
+
+ // Create a follow up change.
+ Change.Id followUpChangeId =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .childOf()
+ .change(mergeChangeId)
+ .file(file1)
+ .content("modified content")
+ .create();
+
+ // Create another change in the other branch so that we can create another merge
+ Change.Id anotherChangeInOtherBranch =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch(branchName)
+ .file(file1)
+ .content("yet another content")
+ .create();
+ approveAndSubmit(anotherChangeInOtherBranch);
+
+ // Create a second merge change with a conflict resolution.
+ Change.Id followUpMergeChangeId =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .childOf()
+ .change(followUpChangeId)
+ .mergeOfButBaseOnFirst()
+ .change(followUpChangeId)
+ .and()
+ .tipOfBranch(branchName)
+ .file(file1)
+ .content("another merged content")
+ .create();
+
+ // Create a change in master onto which the chain can be rebased. This change touches an
+ // unrelated file (file2) so that there is no conflict on rebase.
+ Change.Id newBaseChangeInMaster =
+ changeOperations
+ .newChange()
+ .project(project)
+ .branch("master")
+ .file(file2)
+ .content("other content")
+ .create();
+ approveAndSubmit(newBaseChangeInMaster);
+
+ // Rebase the chain
+ RebaseChainInfo rebaseChainInfo =
+ gApi.changes().id(followUpMergeChangeId.get()).rebaseChain().value();
+ assertThat(rebaseChainInfo.rebasedChanges).hasSize(3);
+ assertThat(rebaseChainInfo.containsGitConflicts).isNull();
+
+ verifyRebaseForChange(
+ mergeChangeId,
+ ImmutableList.of(
+ getCurrentRevision(newBaseChangeInMaster), getCurrentRevision(changeInOtherBranch)),
+ /* shouldHaveApproval= */ false,
+ /* expectedNumRevisions= */ 2);
+ verifyRebaseForChange(
+ followUpChangeId,
+ ImmutableList.of(getCurrentRevision(mergeChangeId)),
+ /* shouldHaveApproval= */ false,
+ /* expectedNumRevisions= */ 2);
+ verifyRebaseForChange(
+ followUpMergeChangeId,
+ ImmutableList.of(
+ getCurrentRevision(followUpChangeId), getCurrentRevision(anotherChangeInOtherBranch)),
+ /* shouldHaveApproval= */ false,
+ /* expectedNumRevisions= */ 2);
+
+ // Verify the file contents.
+ assertThat(getFileContent(mergeChangeId, file1)).isEqualTo("merged content");
+ assertThat(getFileContent(mergeChangeId, file2)).isEqualTo("other content");
+ assertThat(getFileContent(followUpChangeId, file1)).isEqualTo("modified content");
+ assertThat(getFileContent(followUpChangeId, file2)).isEqualTo("other content");
+ assertThat(getFileContent(followUpMergeChangeId, file1)).isEqualTo("another merged content");
+ assertThat(getFileContent(followUpMergeChangeId, file2)).isEqualTo("other content");
+
+ // Rebasing the chain again should fail
+ verifyChangeIsUpToDate(followUpChangeId.toString());
+ }
+
+ @Test
public void rebasePartlyOutdatedChain() throws Exception {
final String file = "modified_file.txt";
final String oldContent = "old content";
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
index 319c0cdde0..99a299f688 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
@@ -15,7 +15,6 @@
package com.google.gerrit.acceptance.api.change;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
index 7c50e93819..cd3e76d3ac 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
@@ -318,7 +318,7 @@ public class RevertIT extends AbstractDaemonTest {
sender.clear();
ChangeInfo revertChange = gApi.changes().id(r.getChangeId()).revert().get();
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(2);
assertThat(sender.getMessages(revertChange.changeId, "newchange")).hasSize(1);
assertThat(sender.getMessages(r.getChangeId(), "revert")).hasSize(1);
@@ -335,7 +335,7 @@ public class RevertIT extends AbstractDaemonTest {
// If notify input not specified, the endpoint overrides it to NONE
RevertInput revertInput = createWipRevertInput();
revertInput.notify = null;
- gApi.changes().id(r.getChangeId()).revert(revertInput).get();
+ gApi.changes().id(r.getChangeId()).revert(revertInput);
assertThat(sender.getMessages()).isEmpty();
}
@@ -350,7 +350,7 @@ public class RevertIT extends AbstractDaemonTest {
revertInput.notify = NotifyHandling.NONE;
sender.clear();
- gApi.changes().id(r.getChangeId()).revert(revertInput).get();
+ gApi.changes().id(r.getChangeId()).revert(revertInput);
assertThat(sender.getMessages()).isEmpty();
}
@@ -751,7 +751,7 @@ public class RevertIT extends AbstractDaemonTest {
RevertSubmissionInfo revertChanges =
gApi.changes().id(secondResult).revertSubmission(revertInput);
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(4);
assertThat(sender.getMessages(revertChanges.revertChanges.get(0).changeId, "newchange"))
diff --git a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index 6ba1498a68..2cc27984b6 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -33,6 +33,7 @@ import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.util.Comparator.comparing;
import com.google.common.cache.Cache;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.MoreCollectors;
import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -471,7 +472,7 @@ public class StickyApprovalsIT extends AbstractDaemonTest {
// The code-review approval is copied for the second change between PS1 and PS2 since the only
// modified file is due to rebase.
- List<PatchSetApproval> patchSetApprovals =
+ ImmutableList<PatchSetApproval> patchSetApprovals =
r2.getChange().notes().getApprovals().all().values().stream()
.sorted(comparing(a -> a.patchSetId().get()))
.collect(toImmutableList());
@@ -1024,7 +1025,7 @@ public class StickyApprovalsIT extends AbstractDaemonTest {
amendChange(r.getChangeId());
}
- List<PatchSetApproval> patchSetApprovals =
+ ImmutableList<PatchSetApproval> patchSetApprovals =
r.getChange().notes().getApprovals().all().values().stream()
.sorted(comparing(a -> a.patchSetId().get()))
.collect(toImmutableList());
@@ -1065,7 +1066,7 @@ public class StickyApprovalsIT extends AbstractDaemonTest {
// Rebase the second change
gApi.changes().id(r2.getChangeId()).rebase();
- List<PatchSetApproval> patchSetApprovals =
+ ImmutableList<PatchSetApproval> patchSetApprovals =
r2.getChange().notes().getApprovals().all().values().stream()
.sorted(comparing(a -> a.patchSetId().get()))
.collect(toImmutableList());
@@ -1102,7 +1103,7 @@ public class StickyApprovalsIT extends AbstractDaemonTest {
// Make a new patchset, keeping the Code-Review +1 vote.
amendChange(r.getChangeId());
- List<PatchSetApproval> patchSetApprovals =
+ ImmutableList<PatchSetApproval> patchSetApprovals =
r.getChange().notes().getApprovals().all().values().stream()
.sorted(comparing(a -> a.patchSetId().get()))
.collect(toImmutableList());
@@ -1152,7 +1153,7 @@ public class StickyApprovalsIT extends AbstractDaemonTest {
// Make a new patchset, keeping the Code-Review +1 vote.
amendChange(r.getChangeId());
- List<PatchSetApproval> patchSetApprovals =
+ ImmutableList<PatchSetApproval> patchSetApprovals =
r.getChange().notes().getApprovals().all().values().stream()
.sorted(comparing(a -> a.patchSetId().get()))
.collect(toImmutableList());
@@ -1289,7 +1290,7 @@ public class StickyApprovalsIT extends AbstractDaemonTest {
.create();
vote(admin, changeId, 2, 1);
- List<PatchSetApproval> patchSetApprovals =
+ ImmutableList<PatchSetApproval> patchSetApprovals =
notesFactory.create(project, r.getChange().getId()).getApprovals().all().values().stream()
.sorted(comparing(a -> a.patchSetId().get()))
.collect(toImmutableList());
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementCustomRuleIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementCustomRuleIT.java
index 5584c2b29d..2c376fce64 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementCustomRuleIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementCustomRuleIT.java
@@ -178,9 +178,12 @@ public class SubmitRequirementCustomRuleIT extends AbstractDaemonTest {
String changeId = r.getChangeId();
rule.numberOfEvaluations.set(0);
- gApi.changes()
- .id(changeId)
- .get(ListChangesOption.ALL_REVISIONS, ListChangesOption.CURRENT_ACTIONS);
+
+ @SuppressWarnings("unused")
+ var unused =
+ gApi.changes()
+ .id(changeId)
+ .get(ListChangesOption.ALL_REVISIONS, ListChangesOption.CURRENT_ACTIONS);
// Submit rules are computed freshly, but only once.
assertThat(rule.numberOfEvaluations.get()).isEqualTo(1);
@@ -192,10 +195,13 @@ public class SubmitRequirementCustomRuleIT extends AbstractDaemonTest {
String changeId = r.getChangeId();
rule.numberOfEvaluations.set(0);
- gApi.changes()
- .query(changeId)
- .withOptions(ListChangesOption.ALL_REVISIONS, ListChangesOption.CURRENT_ACTIONS)
- .get();
+
+ @SuppressWarnings("unused")
+ var unused =
+ gApi.changes()
+ .query(changeId)
+ .withOptions(ListChangesOption.ALL_REVISIONS, ListChangesOption.CURRENT_ACTIONS)
+ .get();
// Submit rule evaluation results from the change index are reused
assertThat(rule.numberOfEvaluations.get()).isEqualTo(0);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
index c8f361e6f7..42af666a77 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
@@ -1134,7 +1134,7 @@ public class SubmitRequirementIT extends AbstractDaemonTest {
LabelDefinitionInput input = new LabelDefinitionInput();
input.function = "NoOp";
input.values = ImmutableMap.of("+1", "Override", " 0", "No Override");
- gApi.projects().name(project.get()).label("Code-Review-Override").create(input).get();
+ gApi.projects().name(project.get()).label("Code-Review-Override").create(input);
// Allow to vote on the Code-Review-Override label.
projectOperations
@@ -1300,7 +1300,7 @@ public class SubmitRequirementIT extends AbstractDaemonTest {
LabelDefinitionInput input = new LabelDefinitionInput();
input.function = "NoOp";
input.values = ImmutableMap.of("+1", "Override", " 0", "No Override");
- gApi.projects().name(project.get()).label("build-cop-override").create(input).get();
+ gApi.projects().name(project.get()).label("build-cop-override").create(input);
// Allow to vote on the build-cop-override label.
projectOperations
@@ -1325,7 +1325,7 @@ public class SubmitRequirementIT extends AbstractDaemonTest {
.build());
// Create Code-Review-Override label
- gApi.projects().name(project.get()).label("Code-Review-Override").create(input).get();
+ gApi.projects().name(project.get()).label("Code-Review-Override").create(input);
// Allow to vote on the Code-Review-Override label.
projectOperations
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java
index b5416aacd5..86434896d3 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java
@@ -92,6 +92,37 @@ public class SubmitRequirementPredicateIT extends AbstractDaemonTest {
}
@Test
+ public void labelVote_greaterThan_withManyMaxVotes() throws Exception {
+ TestRepository<InMemoryRepository> clonedRepo = cloneProject(project, admin);
+ PushOneCommit.Result r1 =
+ pushFactory
+ .create(user.newIdent(), clonedRepo, "Subject", "file.txt", "text")
+ .to("refs/for/master");
+
+ Account.Id user11 = accountCreator.create("user11").id();
+ Account.Id user12 = accountCreator.create("user12").id();
+ Account.Id user13 = accountCreator.create("user13").id();
+ Account.Id user14 = accountCreator.create("user14").id();
+ Account.Id user15 = accountCreator.create("user15").id();
+ Account.Id user16 = accountCreator.create("user16").id();
+ Account.Id user17 = accountCreator.create("user17").id();
+ ImmutableList<Account.Id> allUsers =
+ ImmutableList.of(user11, user12, user13, user14, user15, user16, user17);
+
+ // Give voting permissions to all users
+ requestScopeOperations.setApiUser(admin.id());
+ allowLabelPermission(
+ codeReview().getName(), RefNames.REFS_HEADS + "*", REGISTERED_USERS, -2, +2);
+
+ // The predicate uses the MAX_COUNT_INTERNAL in label predicate, and the SR expression matches
+ // even if the change has more than 5 votes.
+ for (Account.Id aId : allUsers) {
+ approveAsUser(r1.getChangeId(), aId);
+ assertMatching("label:Code-Review=+2,count>=1", r1.getChange().getId());
+ }
+ }
+
+ @Test
public void distinctVoters_sameUserVotesOnDifferentLabels_fails() throws Exception {
Change.Id c1 = changeOperations.newChange().project(project).create();
requestScopeOperations.setApiUser(admin.id());
@@ -436,6 +467,11 @@ public class SubmitRequirementPredicateIT extends AbstractDaemonTest {
assertMatching("label:Code-Review=+2,user=non_contributor", r1.getChange().getId());
}
+ private void approveAsUser(String changeId, Account.Id userId) throws Exception {
+ requestScopeOperations.setApiUser(userId);
+ approve(changeId);
+ }
+
private static void assertUploader(ChangeInfo changeInfo, String email) {
assertThat(changeInfo.revisions.get(changeInfo.currentRevision).uploader.email)
.isEqualTo(email);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
index 308e4e05bd..83de9824b2 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
@@ -28,7 +28,7 @@ import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.entities.Change;
+import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.projects.BranchInput;
@@ -36,15 +36,12 @@ import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.TestSubmitRuleInfo;
import com.google.gerrit.extensions.common.TestSubmitRuleInput;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.git.meta.VersionedMetaData;
import com.google.gerrit.testing.ConfigSuite;
import java.io.IOException;
-import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
@@ -60,7 +57,7 @@ public class SubmitTypeRuleIT extends AbstractDaemonTest {
return submitWholeTopicEnabledConfig();
}
- private class RulesPl extends VersionedMetaData {
+ private static class RulesPl extends VersionedMetaData {
private String rule;
@Override
@@ -69,33 +66,23 @@ public class SubmitTypeRuleIT extends AbstractDaemonTest {
}
@Override
- protected void onLoad() throws IOException, ConfigInvalidException {
+ protected void onLoad() throws IOException {
rule = readUTF8(RULES_PL_FILE);
}
@Override
- protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
- TestSubmitRuleInput in = new TestSubmitRuleInput();
- in.rule = rule;
- try {
- gApi.changes().id(testChangeId.get()).current().testSubmitType(in);
- } catch (RestApiException e) {
- throw new ConfigInvalidException("Invalid submit type rule", e);
- }
-
+ protected boolean onSave(CommitBuilder commit) throws IOException {
saveUTF8(RULES_PL_FILE, rule);
return true;
}
}
private AtomicInteger fileCounter;
- private Change.Id testChangeId;
@Before
public void setUp() throws Exception {
fileCounter = new AtomicInteger();
gApi.projects().name(project.get()).branch("test").create(new BranchInput());
- testChangeId = createChange("test", "test change").getChange().getId();
}
private void setRulesPl(String rule) throws Exception {
@@ -186,6 +173,21 @@ public class SubmitTypeRuleIT extends AbstractDaemonTest {
}
@Test
+ @GerritConfig(name = "rules.enable", value = "false")
+ public void submitType_rulesTakeNoEffectWhenDisabled() throws Exception {
+ PushOneCommit.Result r1 = createChange("master", "Default 1");
+ PushOneCommit.Result r2 = createChange("master", "FAST_FORWARD_ONLY 2");
+ PushOneCommit.Result r3 = createChange("master", "MERGE_IF_NECESSARY 3");
+
+ setRulesPl(SUBMIT_TYPE_FROM_SUBJECT);
+
+ // Rules take no effect
+ assertSubmitType(MERGE_IF_NECESSARY, r1.getChangeId());
+ assertSubmitType(MERGE_IF_NECESSARY, r2.getChangeId());
+ assertSubmitType(MERGE_IF_NECESSARY, r3.getChangeId());
+ }
+
+ @Test
public void submitTypeIsUsedForSubmit() throws Exception {
setRulesPl(SUBMIT_TYPE_FROM_SUBJECT);
@@ -194,7 +196,7 @@ public class SubmitTypeRuleIT extends AbstractDaemonTest {
gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
gApi.changes().id(r.getChangeId()).current().submit();
- List<RevCommit> log = log("master", 1);
+ ImmutableList<RevCommit> log = log("master", 1);
assertThat(log.get(0).getShortMessage()).isEqualTo("CHERRY_PICK 1");
assertThat(log.get(0).name()).isNotEqualTo(r.getCommit().name());
assertThat(log.get(0).getFullMessage()).contains("Change-Id: " + r.getChangeId());
@@ -223,7 +225,7 @@ public class SubmitTypeRuleIT extends AbstractDaemonTest {
assertThat(log("master", 1).get(0).name()).isEqualTo(r1.getCommit().name());
- List<RevCommit> branchLog = log("branch", 1);
+ ImmutableList<RevCommit> branchLog = log("branch", 1);
assertThat(branchLog.get(0).getParents()).hasLength(2);
assertThat(branchLog.get(0).getParent(1).name()).isEqualTo(r2.getCommit().name());
}
@@ -285,7 +287,7 @@ public class SubmitTypeRuleIT extends AbstractDaemonTest {
return info;
}
- private List<RevCommit> log(String commitish, int n) throws Exception {
+ private ImmutableList<RevCommit> log(String commitish, int n) throws Exception {
try (Repository repo = repoManager.openRepository(project);
Git git = new Git(repo)) {
ObjectId id = repo.resolve(commitish);
diff --git a/javatests/com/google/gerrit/acceptance/api/config/GetExperimentIT.java b/javatests/com/google/gerrit/acceptance/api/config/GetExperimentIT.java
new file mode 100644
index 0000000000..0609b5c0d6
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/config/GetExperimentIT.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.acceptance.api.config;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.extensions.common.ExperimentInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+public class GetExperimentIT extends AbstractDaemonTest {
+ @Inject private RequestScopeOperations requestScopeOperations;
+
+ @Test
+ public void cannotGetAsNonAdmin() throws Exception {
+ requestScopeOperations.setApiUser(user.id());
+
+ AuthException exception =
+ assertThrows(
+ AuthException.class,
+ () ->
+ gApi.config()
+ .server()
+ .experiment(ExperimentFeaturesConstants.ALLOW_FIX_SUGGESTIONS_IN_COMMENTS)
+ .get());
+ assertThat(exception).hasMessageThat().isEqualTo("administrate server not permitted");
+ }
+
+ @Test
+ public void cannotGetNonExistingExperiment() throws Exception {
+ ResourceNotFoundException exception =
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> gApi.config().server().experiment("non-existing").get());
+ assertThat(exception).hasMessageThat().isEqualTo("non-existing");
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ values = {"GerritBackendFeature__attach_nonce_to_documentation"})
+ public void getEnabled() throws Exception {
+ ExperimentInfo experimentInfo =
+ gApi.config()
+ .server()
+ .experiment(
+ ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION)
+ .get();
+ assertThat(experimentInfo.enabled).isTrue();
+ }
+
+ @Test
+ public void getDisabled() throws Exception {
+ ExperimentInfo experimentInfo =
+ gApi.config()
+ .server()
+ .experiment(
+ ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION)
+ .get();
+ assertThat(experimentInfo.enabled).isFalse();
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java b/javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java
new file mode 100644
index 0000000000..fe3cb003bd
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java
@@ -0,0 +1,115 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.acceptance.api.config;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.extensions.common.ExperimentInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+@NoHttpd
+public class ListExperimentsIT extends AbstractDaemonTest {
+ @Inject private RequestScopeOperations requestScopeOperations;
+
+ @Test
+ public void cannotListAsNonAdmin() throws Exception {
+ requestScopeOperations.setApiUser(user.id());
+
+ AuthException exception =
+ assertThrows(AuthException.class, () -> gApi.config().server().listExperiments().get());
+ assertThat(exception).hasMessageThat().isEqualTo("administrate server not permitted");
+ }
+
+ @Test
+ public void listAll() throws Exception {
+ ImmutableMap<String, ExperimentInfo> experiments =
+ gApi.config().server().listExperiments().get();
+ assertThat(experiments.keySet())
+ .containsExactly(
+ ExperimentFeaturesConstants.ALLOW_FIX_SUGGESTIONS_IN_COMMENTS,
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_FEATURE_ALWAYS_REJECT_IMPLICIT_MERGES_ON_MERGE,
+ ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION,
+ ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_CHECK_IMPLICIT_MERGES_ON_MERGE,
+ ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_REJECT_IMPLICIT_MERGES_ON_MERGE)
+ .inOrder();
+
+ // "GerritBackendFeature__check_implicit_merges_on_merge",
+ // "GerritBackendFeature__reject_implicit_merges_on_merge" and
+ // "GerritBackendFeature__always_reject_implicit_merges_on_merge" are enabled via
+ // AbstractDaemonTest#beforeTest
+ assertThat(
+ experiments.get(
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_FEATURE_ALWAYS_REJECT_IMPLICIT_MERGES_ON_MERGE)
+ .enabled)
+ .isTrue();
+ assertThat(
+ experiments.get(
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_FEATURE_CHECK_IMPLICIT_MERGES_ON_MERGE)
+ .enabled)
+ .isTrue();
+ assertThat(
+ experiments.get(
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_FEATURE_REJECT_IMPLICIT_MERGES_ON_MERGE)
+ .enabled)
+ .isTrue();
+
+ assertThat(
+ experiments.get(ExperimentFeaturesConstants.ALLOW_FIX_SUGGESTIONS_IN_COMMENTS).enabled)
+ .isFalse();
+ assertThat(
+ experiments.get(
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION)
+ .enabled)
+ .isFalse();
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ values = {"GerritBackendFeature__attach_nonce_to_documentation"})
+ // "GerritBackendFeature__check_implicit_merges_on_merge",
+ // "GerritBackendFeature__reject_implicit_merges_on_merge" and
+ // "GerritBackendFeature__always_reject_implicit_merges_on_merge" are enabled via
+ // AbstractDaemonTest#beforeTest
+ public void listEnabled_noneEnabled() throws Exception {
+ ImmutableMap<String, ExperimentInfo> experiments =
+ gApi.config().server().listExperiments().enabledOnly().get();
+ assertThat(experiments.keySet())
+ .containsExactly(
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_FEATURE_ALWAYS_REJECT_IMPLICIT_MERGES_ON_MERGE,
+ ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION,
+ ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_CHECK_IMPLICIT_MERGES_ON_MERGE,
+ ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_REJECT_IMPLICIT_MERGES_ON_MERGE)
+ .inOrder();
+ for (ExperimentInfo experimentInfo : experiments.values()) {
+ assertThat(experimentInfo.enabled).isTrue();
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
index 98ed56c31a..2f3ef24ce5 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
@@ -41,7 +41,6 @@ import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
import java.util.Optional;
-import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.junit.Rule;
import org.junit.Test;
@@ -68,7 +67,7 @@ public class GroupIndexerIT {
groupIndexer.index(groupUuid);
- Set<AccountGroup.UUID> parentGroups =
+ ImmutableSet<AccountGroup.UUID> parentGroups =
groupQueryProvider.get().bySubgroups(ImmutableSet.of(subgroupUuid)).get(subgroupUuid);
assertThat(parentGroups).hasSize(1);
assertThat(parentGroups).containsExactly(groupUuid);
@@ -87,7 +86,7 @@ public class GroupIndexerIT {
groupIndexer.index(groupUuid);
- Set<AccountGroup.UUID> parentGroups =
+ ImmutableSet<AccountGroup.UUID> parentGroups =
groupQueryProvider.get().bySubgroups(ImmutableSet.of(subgroupUuid)).get(subgroupUuid);
assertThat(parentGroups).hasSize(1);
assertThat(parentGroups).containsExactly(groupUuid);
@@ -117,7 +116,7 @@ public class GroupIndexerIT {
groupIndexer.reindexIfStale(groupUuid);
- Set<AccountGroup.UUID> parentGroups =
+ ImmutableSet<AccountGroup.UUID> parentGroups =
groupQueryProvider.get().bySubgroups(ImmutableSet.of(subgroupUuid)).get(subgroupUuid);
assertThat(parentGroups).hasSize(1);
assertThat(parentGroups).containsExactly(groupUuid);
@@ -170,7 +169,8 @@ public class GroupIndexerIT {
}
private void loadGroupToCache(AccountGroup.UUID groupUuid) {
- groupCache.get(groupUuid);
+ @SuppressWarnings("unused")
+ var unused = groupCache.get(groupUuid);
}
private static GroupDelta.Builder newGroupDelta() {
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index db12e85027..289e642453 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -17,7 +17,6 @@ package com.google.gerrit.acceptance.api.group;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.GitUtil.deleteRef;
import static com.google.gerrit.acceptance.GitUtil.fetch;
import static com.google.gerrit.acceptance.api.group.GroupAssert.assertGroupInfo;
@@ -90,6 +89,8 @@ import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupIncludeCache;
import com.google.gerrit.server.account.GroupsSnapshotReader;
import com.google.gerrit.server.account.ServiceUserClassifier;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.group.PeriodicGroupIndexer;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.group.db.GroupDelta;
@@ -116,6 +117,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import java.util.stream.Stream;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
@@ -153,6 +155,8 @@ public class GroupsIT extends AbstractDaemonTest {
@Inject private ExtensionRegistry extensionRegistry;
@Inject private GroupsSnapshotReader groupsSnapshotReader;
+ @Inject private ProjectResetter.Builder.Factory projectResetterFactory;
+
@Override
public Module createModule() {
return new AbstractModule() {
@@ -166,17 +170,18 @@ public class GroupsIT extends AbstractDaemonTest {
@After
public void consistencyCheck() throws Exception {
- if (description.getAnnotation(IgnoreGroupInconsistencies.class) == null) {
+ if (configRule.description().getAnnotation(IgnoreGroupInconsistencies.class) == null) {
assertThat(consistencyChecker.check()).isEmpty();
}
}
@Override
- protected ProjectResetter.Config resetProjects() {
+ protected ProjectResetter.Config resetProjects(
+ AllProjectsName allProjects, AllUsersName allUsers) {
// Don't reset All-Users since deleting users makes groups inconsistent (e.g. groups would
// contain members that no longer exist) and as result of this the group consistency checker
// that is executed after each test would fail.
- return new ProjectResetter.Config().reset(allProjects, RefNames.REFS_CONFIG);
+ return new ProjectResetter.Config.Builder().reset(allProjects, RefNames.REFS_CONFIG).build();
}
@Test
@@ -292,7 +297,9 @@ public class GroupsIT extends AbstractDaemonTest {
Account.Id accountId = accountOperations.newAccount().username(username).create();
// Fill the cache for the observed account.
- groupIncludeCache.getGroupsWithMember(accountId);
+ @SuppressWarnings("unused")
+ var unused = groupIncludeCache.getGroupsWithMember(accountId);
+
AccountGroup.UUID groupUuid = groupOperations.newGroup().create();
gApi.groups().id(groupUuid.get()).addMembers(username);
@@ -578,7 +585,8 @@ public class GroupsIT extends AbstractDaemonTest {
Account.Id accountId = accountOperations.newAccount().create();
// Fill the cache for the observed account.
- groupIncludeCache.getGroupsWithMember(accountId);
+ @SuppressWarnings("unused")
+ var unused = groupIncludeCache.getGroupsWithMember(accountId);
GroupInput groupInput = new GroupInput();
groupInput.name = name("Users");
@@ -654,7 +662,7 @@ public class GroupsIT extends AbstractDaemonTest {
@Test
public void groupCannotBeCreatedWithNameOfAnotherGroup() throws Exception {
String name = name("Users");
- gApi.groups().create(name).get();
+ gApi.groups().create(name);
assertThrows(ResourceConflictException.class, () -> gApi.groups().create(name));
}
@@ -937,7 +945,7 @@ public class GroupsIT extends AbstractDaemonTest {
@Test
public void defaultGroupsCreated() throws Exception {
- Iterable<String> names = gApi.groups().list().getAsMap().keySet();
+ Set<String> names = gApi.groups().list().getAsMap().keySet();
assertThat(names)
.containsAtLeast("Administrators", ServiceUserClassifier.SERVICE_USERS)
.inOrder();
@@ -1348,9 +1356,12 @@ public class GroupsIT extends AbstractDaemonTest {
public void cannotCreateGroupNamesBranch() throws Exception {
// Use ProjectResetter to restore the group names ref
try (ProjectResetter resetter =
- projectResetter
+ projectResetterFactory
.builder()
- .build(new ProjectResetter.Config().reset(allUsers, RefNames.REFS_GROUPNAMES))) {
+ .build(
+ new ProjectResetter.Config.Builder()
+ .reset(allUsers, RefNames.REFS_GROUPNAMES)
+ .build())) {
// Manually delete group names ref
try (Repository repo = repoManager.openRepository(allUsers);
RevWalk rw = new RevWalk(repo)) {
@@ -1482,7 +1493,7 @@ public class GroupsIT extends AbstractDaemonTest {
GroupInput groupInput = new GroupInput();
groupInput.name = name("contributors");
groupInput.members = ImmutableList.of(user.username());
- gApi.groups().create(groupInput).get();
+ gApi.groups().create(groupInput);
restartAsSlave();
requestScopeOperations.setApiUser(user.id());
@@ -1690,7 +1701,7 @@ public class GroupsIT extends AbstractDaemonTest {
}
private static void assertIncludes(List<GroupInfo> includes, String... expectedNames) {
- List<String> names = includes.stream().map(i -> i.name).collect(toImmutableList());
+ ImmutableList<String> names = includes.stream().map(i -> i.name).collect(toImmutableList());
assertThat(names).containsExactlyElementsIn(Arrays.asList(expectedNames));
assertThat(names).isInOrder();
}
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java
index 2796488a0a..6eecda9bb6 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java
@@ -14,7 +14,7 @@
package com.google.gerrit.acceptance.api.group;
-import static com.google.common.truth.Truth8.assertThat;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.collect.ImmutableSet;
diff --git a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
index 462d0a5d35..dbd1fb8a2c 100644
--- a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
@@ -145,7 +145,9 @@ public class PluginIT extends AbstractDaemonTest {
new com.google.gerrit.extensions.common.InstallPluginInput();
input.raw = JS_PLUGIN_CONTENT;
gApi.plugins().install("legacy.js", input);
- gApi.plugins().name("legacy").get();
+
+ @SuppressWarnings("unused")
+ var unused = gApi.plugins().name("legacy").get();
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/api/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/AccessIT.java
index a2f1f46901..035b56705b 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/AccessIT.java
@@ -15,7 +15,6 @@
package com.google.gerrit.acceptance.api.project;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
@@ -244,7 +243,8 @@ public class AccessIT extends AbstractDaemonTest {
testRefAction(() -> assertThat(u.delete()).isEqualTo(Result.FORCED));
// This should not crash.
- pApi().access();
+ @SuppressWarnings("unused")
+ var unused = pApi().access();
}
}
@@ -465,9 +465,11 @@ public class AccessIT extends AbstractDaemonTest {
.forUpdate()
.add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
.update();
+
// User can see the branch
requestScopeOperations.setApiUser(user.id());
- pApi().branch("refs/heads/master").get();
+ @SuppressWarnings("unused")
+ var unused = pApi().branch("refs/heads/master").get();
ProjectAccessInput accessInput = newProjectAccessInput();
@@ -516,7 +518,7 @@ public class AccessIT extends AbstractDaemonTest {
// Now it works again.
requestScopeOperations.setApiUser(user.id());
- pApi().branch("refs/heads/master").get();
+ unused = pApi().branch("refs/heads/master").get();
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
index 27193ddd33..a5fa55af82 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
@@ -227,7 +227,7 @@ public class CheckAccessIT extends AbstractDaemonTest {
@Test
public void accessible() throws Exception {
- List<TestCase> inputs =
+ ImmutableList<TestCase> inputs =
ImmutableList.of(
// Test 1
TestCase.projectRefPerm(
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
index 4163e1722b..02f6784855 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
@@ -253,7 +253,9 @@ public class CheckProjectIT extends AbstractDaemonTest {
@Test
public void branchPrefixCanBeOmitted() throws Exception {
CheckProjectInput input = checkProjectInputForAutoCloseableCheck("master");
- gApi.projects().name(project.get()).check(input);
+
+ @SuppressWarnings("unused")
+ var unused = gApi.projects().name(project.get()).check(input);
}
@Test
@@ -261,7 +263,9 @@ public class CheckProjectIT extends AbstractDaemonTest {
CheckProjectInput input = checkProjectInputForAutoCloseableCheck("refs/heads/master");
input.autoCloseableChangesCheck.maxCommits =
ProjectsConsistencyChecker.AUTO_CLOSE_MAX_COMMITS_LIMIT;
- gApi.projects().name(project.get()).check(input);
+
+ @SuppressWarnings("unused")
+ var unused = gApi.projects().name(project.get()).check(input);
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java b/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
index 84a4a40612..bf3c80fdea 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
@@ -47,6 +47,7 @@ import com.google.gerrit.extensions.api.projects.TagInput;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.common.CommitMessageInput;
import com.google.gerrit.extensions.common.GitPerson;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -121,7 +122,10 @@ public class CommitIT extends AbstractDaemonTest {
createBranch(BranchNameKey.create(project, "test-branch-1"));
createBranch(BranchNameKey.create(project, "test-branch-2"));
- createAndSubmitChange("refs/for/test-branch-1");
+ RevCommit changeCommit = createAndSubmitChange("refs/for/test-branch-1").getCommit();
+ // Reset repo back to the original state - otherwise all changes in tests have testChange as a
+ // parent.
+ testRepo.reset(changeCommit.getParent(0));
createAndSubmitChange("refs/for/test-branch-2");
assertThat(getIncludedIn(baseChange.getCommit().getId()).branches)
@@ -550,6 +554,51 @@ public class CommitIT extends AbstractDaemonTest {
.containsExactly(baseChangeNumber, cherryPickChange._number);
}
+ @Test
+ public void editMessageWithSecondaryEmail() throws Exception {
+ // Create new user with a secondary email
+ Account.Id testUser =
+ accountOperations
+ .newAccount()
+ .preferredEmail("preferred@example.com")
+ .addSecondaryEmail("secondary@example.com")
+ .create();
+ requestScopeOperations.setApiUser(testUser);
+
+ // Create a change and edit its message using secondary email
+ PushOneCommit.Result r = createChange();
+ RevCommit commit = r.getCommit();
+ String message = commit.getFullMessage();
+ CommitMessageInput in = new CommitMessageInput();
+ in.message = "new message" + message;
+ in.committerEmail = "secondary@example.com";
+ gApi.changes().id(r.getChangeId()).setMessage(in);
+ CommitInfo newCommit = gApi.changes().id(r.getChangeId()).current().commit(false);
+ assertThat(newCommit.message).contains("new message");
+ assertThat(newCommit.committer.email).isEqualTo("secondary@example.com");
+ }
+
+ @Test
+ public void cannotEditMessageWithUnregisteredEmail() throws Exception {
+ PushOneCommit.Result r = createChange();
+ RevCommit commit = r.getCommit();
+ String message = commit.getFullMessage();
+ CommitMessageInput in = new CommitMessageInput();
+ in.message = "new message" + message;
+ in.committerEmail = "unregistered@example.com";
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class, () -> gApi.changes().id(r.getChangeId()).setMessage(in));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo(
+ String.format(
+ "Cannot set commit message using committer email %s,"
+ + " as it is not among the registered emails of account %s",
+ in.committerEmail, admin.id()));
+ }
+
private IncludedInInfo getIncludedIn(ObjectId id) throws Exception {
return gApi.projects().name(project.get()).commit(id.name()).includedIn();
}
diff --git a/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java b/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java
index a22b55818a..b9cbbcd516 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java
@@ -38,7 +38,6 @@ import com.google.inject.Inject;
import java.util.List;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
import org.junit.Test;
@@ -219,8 +218,7 @@ public class DashboardIT extends AbstractDaemonTest {
}
}
cb.add(info.path, content.toString());
- RevCommit c = cb.create();
- project().commit(c.name());
+ cb.create();
}
return info;
}
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectConfigIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectConfigIT.java
index 28a0196c32..9a926ea3a2 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectConfigIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectConfigIT.java
@@ -20,6 +20,8 @@ import static com.google.gerrit.acceptance.GitUtil.fetch;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.common.Nullable;
@@ -29,15 +31,22 @@ import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
+import com.google.gerrit.extensions.api.projects.ConfigInfo;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
+import com.google.gerrit.extensions.api.projects.ConfigValue;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.git.ObjectIds;
+import com.google.gerrit.server.config.ProjectConfigEntry;
+import com.google.gerrit.server.git.validators.ValidationMessage;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.project.GroupList;
import com.google.gerrit.server.project.LabelConfigValidator;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.inject.Inject;
+import java.util.HashMap;
+import java.util.Map;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Repository;
@@ -51,6 +60,7 @@ public class ProjectConfigIT extends AbstractDaemonTest {
+ " copyAllScoresOnTrivialRebase = true";
@Inject private ProjectOperations projectOperations;
+ @Inject private ExtensionRegistry extensionRegistry;
@Test
public void noLabelValidationForNonRefsMetaConfigChange() throws Exception {
@@ -515,6 +525,119 @@ public class ProjectConfigIT extends AbstractDaemonTest {
}
@Test
+ public void pluginConfigs_neverWriteDefaultValueToConfigFile() throws Exception {
+ String projectConfig = projectOperations.project(project).getConfig().toText();
+ assertThat(projectConfig).doesNotContain("myPlugin");
+
+ ProjectConfigEntry entry = new ProjectConfigEntry("enabled", "true");
+ try (Registration ignored =
+ extensionRegistry.newRegistration().add(entry, "test-config-entry")) {
+ // Default value is populated in API response
+ ConfigInfo configInfo = gApi.projects().name(project.get()).config();
+ assertThat(configInfo.pluginConfig.get("myPlugin").get("test-config-entry").value)
+ .isEqualTo("true");
+
+ // Set an unrelated parameter
+ ConfigInput input = new ConfigInput();
+ input.description = "New description";
+
+ gApi.projects().name(project.get()).config(input);
+
+ // The project config does not contain a section for the plugin
+ projectConfig = projectOperations.project(project).getConfig().toText();
+ assertThat(projectConfig).doesNotContain("myPlugin");
+ assertThat(projectConfig).contains("New description");
+
+ // Set the plugin config to the default value. Set an unrelated setting on the side.
+ Map<String, ConfigValue> val = new HashMap<>();
+ input.pluginConfigValues = new HashMap<>();
+ input.pluginConfigValues.put("myPlugin", val);
+ val.put("test-config-entry", new ConfigValue("true"));
+ input.description = "New description2";
+ gApi.projects().name(project.get()).config(input);
+
+ // The project config does not contain a section for the plugin
+ projectConfig = projectOperations.project(project).getConfig().toText();
+ assertThat(projectConfig).doesNotContain("myPlugin");
+ assertThat(projectConfig).contains("New description2");
+ }
+ }
+
+ @Test
+ public void pluginConfigs_persistNonDefaultConfig() throws Exception {
+ String projectConfig = projectOperations.project(project).getConfig().toText();
+ assertThat(projectConfig).doesNotContain("myPlugin");
+
+ ProjectConfigEntry entry = new ProjectConfigEntry("enabled", "true");
+ try (Registration ignored =
+ extensionRegistry.newRegistration().add(entry, "test-config-entry")) {
+ // Default value is populated in API response
+ ConfigInfo configInfo = gApi.projects().name(project.get()).config();
+ assertThat(configInfo.pluginConfig.get("myPlugin").get("test-config-entry").value)
+ .isEqualTo("true");
+
+ // Change value to non-default
+ ConfigInput input = new ConfigInput();
+ input.pluginConfigValues = new HashMap<>();
+ Map<String, ConfigValue> val = new HashMap<>();
+ input.pluginConfigValues.put("myPlugin", val);
+ val.put("test-config-entry", new ConfigValue("false"));
+ gApi.projects().name(project.get()).config(input);
+
+ // API response serves new setting
+ configInfo = gApi.projects().name(project.get()).config();
+ assertThat(configInfo.pluginConfig.get("myPlugin").get("test-config-entry").value)
+ .isEqualTo("false");
+
+ // The project config contains a section for the plugin
+ projectConfig = projectOperations.project(project).getConfig().toText();
+ assertThat(projectConfig).contains("myPlugin");
+ }
+ }
+
+ @Test
+ public void pluginConfigs_canUnsetPluginSetting() throws Exception {
+ String projectConfig = projectOperations.project(project).getConfig().toText();
+ assertThat(projectConfig).doesNotContain("myPlugin");
+
+ ProjectConfigEntry entry = new ProjectConfigEntry("enabled", "true");
+ try (Registration ignored =
+ extensionRegistry.newRegistration().add(entry, "test-config-entry")) {
+ // Default value is populated in API response
+ ConfigInfo configInfo = gApi.projects().name(project.get()).config();
+ assertThat(configInfo.pluginConfig.get("myPlugin").get("test-config-entry").value)
+ .isEqualTo("true");
+
+ // Change value to non-default
+ ConfigInput input = new ConfigInput();
+ input.pluginConfigValues = new HashMap<>();
+ Map<String, ConfigValue> val = new HashMap<>();
+ input.pluginConfigValues.put("myPlugin", val);
+ val.put("test-config-entry", new ConfigValue("false"));
+ gApi.projects().name(project.get()).config(input);
+
+ // API response serves new setting
+ configInfo = gApi.projects().name(project.get()).config();
+ assertThat(configInfo.pluginConfig.get("myPlugin").get("test-config-entry").value)
+ .isEqualTo("false");
+
+ // The project config contains a section for the plugin
+ projectConfig = projectOperations.project(project).getConfig().toText();
+ assertThat(projectConfig).contains("myPlugin");
+
+ // Reset value to default
+ val.put("test-config-entry", new ConfigValue("true"));
+ gApi.projects().name(project.get()).config(input);
+
+ projectConfig = projectOperations.project(project).getConfig().toText();
+ assertThat(projectConfig).doesNotContain("myPlugin");
+ configInfo = gApi.projects().name(project.get()).config();
+ assertThat(configInfo.pluginConfig.get("myPlugin").get("test-config-entry").value)
+ .isEqualTo("true");
+ }
+ }
+
+ @Test
public void rejectSubmitRequirement_duplicateApplicableIfKeys() throws Exception {
fetchRefsMetaConfig();
PushOneCommit push =
@@ -1059,7 +1182,100 @@ public class ProjectConfigIT extends AbstractDaemonTest {
}
@Test
- public void setCopyCondition() throws Exception {
+ public void testSettingCopyCondition() throws Exception {
+ testChangingCopyCondition(/* initialCopyCondition= */ null, /* newCopyCondition= */ "is:ANY");
+ }
+
+ @Test
+ public void testRejectNonParseableCopyCondition_badSyntax() throws Exception {
+ testChangingCopyConditionExpectError(
+ /* initialCopyCondition= */ "is:ANY",
+ /* newCopyCondition= */ ":",
+ /* errorMessage= */ "Cannot parse copy condition ':' of label Foo (parameter"
+ + " 'label.Foo.copyCondition'): line 1:0 no viable alternative at input ':'");
+ }
+
+ @Test
+ public void testRejectNonParseableCopyCondition_unsupportedOperator() throws Exception {
+ testChangingCopyConditionExpectError(
+ /* initialCopyCondition= */ "is:ANY",
+ /* newCopyCondition= */ "foo:bar",
+ /* errorMessage= */ "Cannot parse copy condition 'foo:bar' of label Foo (parameter"
+ + " 'label.Foo.copyCondition'): unsupported operator foo:bar");
+ }
+
+ @Test
+ public void testFixNonParseableCopyCondition() throws Exception {
+ testChangingCopyCondition(/* initialCopyCondition= */ ":", /* newCopyCondition= */ "is:ANY");
+ }
+
+ @Test
+ public void testChangingCopyCondition() throws Exception {
+ testChangingCopyCondition(
+ /* initialCopyCondition= */ "is:ANY", /* newCopyCondition= */ "is:MAX");
+ }
+
+ @Test
+ public void testDeletingCopyCondition() throws Exception {
+ testChangingCopyCondition(/* initialCopyCondition= */ "is:ANY", /* newCopyCondition= */ null);
+ }
+
+ @Test
+ public void testDeletingNonParseableCopyCondition() throws Exception {
+ testChangingCopyCondition(/* initialCopyCondition= */ ":", /* newCopyCondition= */ null);
+ }
+
+ @Test
+ public void testChangingNonParseableCopyCondition() throws Exception {
+ testChangingCopyConditionExpectWarning(
+ /* initialCopyCondition= */ ":",
+ /* newCopyCondition= */ ":foo",
+ /* warningMessage= */ "Cannot parse copy condition ':foo' of label Foo (parameter"
+ + " 'label.Foo.copyCondition'): line 1:0 no viable alternative at input ':'");
+ }
+
+ private void testChangingCopyCondition(
+ String initialCopyCondition, @Nullable String newCopyCondition) throws Exception {
+ testChangingCopyCondition(
+ initialCopyCondition, newCopyCondition, /* type= */ null, /* message= */ null);
+ }
+
+ private void testChangingCopyConditionExpectError(
+ String initialCopyCondition, @Nullable String newCopyCondition, String errorMessage)
+ throws Exception {
+ testChangingCopyCondition(
+ initialCopyCondition, newCopyCondition, ValidationMessage.Type.ERROR, errorMessage);
+ }
+
+ private void testChangingCopyConditionExpectWarning(
+ String initialCopyCondition, @Nullable String newCopyCondition, String warningMessage)
+ throws Exception {
+ testChangingCopyCondition(
+ initialCopyCondition, newCopyCondition, ValidationMessage.Type.WARNING, warningMessage);
+ }
+
+ private void testChangingCopyCondition(
+ @Nullable String initialCopyCondition,
+ @Nullable String newCopyCondition,
+ @Nullable ValidationMessage.Type type,
+ @Nullable String message)
+ throws Exception {
+ if (initialCopyCondition != null) {
+ try (TestRepository<Repository> testRepo =
+ new TestRepository<>(repoManager.openRepository(project))) {
+ testRepo
+ .branch(RefNames.REFS_CONFIG)
+ .commit()
+ .add(
+ ProjectConfig.PROJECT_CONFIG,
+ String.format(
+ "[label \"Foo\"]\n %s = %s\n",
+ ProjectConfig.KEY_COPY_CONDITION, initialCopyCondition))
+ .parent(projectOperations.project(project).getHead(RefNames.REFS_CONFIG))
+ .create();
+ }
+ }
+
fetchRefsMetaConfig();
PushOneCommit push =
pushFactory.create(
@@ -1067,9 +1283,22 @@ public class ProjectConfigIT extends AbstractDaemonTest {
testRepo,
"Test Change",
ProjectConfig.PROJECT_CONFIG,
- String.format("[label \"Foo\"]\n %s = is:ANY", ProjectConfig.KEY_COPY_CONDITION));
+ newCopyCondition == null
+ ? "[label \"Foo\"]\n"
+ : String.format(
+ "[label \"Foo\"]\n %s = %s\n",
+ ProjectConfig.KEY_COPY_CONDITION, newCopyCondition));
PushOneCommit.Result r = push.to(RefNames.REFS_CONFIG);
- r.assertOkStatus();
+ if (!ValidationMessage.Type.ERROR.equals(type)) {
+ r.assertOkStatus();
+ return;
+ }
+ r.assertErrorStatus(
+ String.format(
+ "invalid %s file in revision %s", ProjectConfig.PROJECT_CONFIG, r.getCommit().name()));
+ if (message != null) {
+ r.assertMessage(message);
+ }
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index a93c0a5039..e34b985f66 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -15,7 +15,6 @@
package com.google.gerrit.acceptance.api.project;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
@@ -1140,6 +1139,10 @@ public class ProjectIT extends AbstractDaemonTest {
}
@Test
+ @GerritConfig(
+ name = "experiments.disabled",
+ // The test intentionally create an implicit merge change.
+ value = "GerritBackendFeature__reject_implicit_merges_on_merge")
public void commitsIncludedInRefsMergedChangeNonTipCommit() throws Exception {
String branchWithChange1 = R_HEADS + "branch-with-change1";
String tagWithChange1 = R_TAGS + "tag-with-change1";
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
index 4302b5058a..a99cf523aa 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
@@ -15,10 +15,10 @@
package com.google.gerrit.acceptance.api.project;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.GitUtil.fetch;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.testsuite.change.IndexOperations;
@@ -37,7 +37,6 @@ import com.google.gerrit.server.index.project.StalenessChecker;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.inject.Inject;
import java.util.Collection;
-import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -69,7 +68,8 @@ public class ProjectIndexerIT extends AbstractDaemonTest {
Iterable<byte[]> refState = result.get().getValue(ProjectField.REF_STATE_SPEC);
assertThat(refState).isNotEmpty();
- Map<Project.NameKey, Collection<RefState>> states = RefState.parseStates(refState).asMap();
+ ImmutableMap<Project.NameKey, Collection<RefState>> states =
+ RefState.parseStates(refState).asMap();
fetch(testRepo, "refs/meta/config:refs/meta/config");
Ref projectConfigRef = testRepo.getRepository().exactRef("refs/meta/config");
diff --git a/javatests/com/google/gerrit/acceptance/api/project/SubmitRequirementsAPIIT.java b/javatests/com/google/gerrit/acceptance/api/project/SubmitRequirementsAPIIT.java
index 97a2d2bc5d..e388dd15d1 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/SubmitRequirementsAPIIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/SubmitRequirementsAPIIT.java
@@ -58,7 +58,7 @@ public class SubmitRequirementsAPIIT extends AbstractDaemonTest {
input.name = "code-review";
input.applicabilityExpression = "topic:foo";
input.submittabilityExpression = "label:code-review=+2";
- gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input);
SubmitRequirementInfo info =
gApi.projects().name(project.get()).submitRequirement("code-review").get();
@@ -74,7 +74,7 @@ public class SubmitRequirementsAPIIT extends AbstractDaemonTest {
input.name = "code-review";
input.applicabilityExpression = "topic:foo";
input.submittabilityExpression = "label:code-review=+2";
- gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input);
input.submittabilityExpression = "label:code-review=+1";
SubmitRequirementInfo info =
@@ -88,7 +88,7 @@ public class SubmitRequirementsAPIIT extends AbstractDaemonTest {
input.name = "code-review";
input.applicabilityExpression = "topic:foo";
input.submittabilityExpression = "label:code-review=+2";
- gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input);
input.applicabilityExpression = null;
SubmitRequirementInfo info =
@@ -102,7 +102,7 @@ public class SubmitRequirementsAPIIT extends AbstractDaemonTest {
input.name = "code-review";
input.overrideExpression = "topic:foo";
input.submittabilityExpression = "label:code-review=+2";
- gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input);
input.overrideExpression = null;
SubmitRequirementInfo info =
@@ -115,7 +115,7 @@ public class SubmitRequirementsAPIIT extends AbstractDaemonTest {
SubmitRequirementInput input = new SubmitRequirementInput();
input.name = "code-review";
input.submittabilityExpression = "label:code-review=+2";
- gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input);
input.overrideExpression = "topic:foo";
SubmitRequirementInfo info =
@@ -129,7 +129,7 @@ public class SubmitRequirementsAPIIT extends AbstractDaemonTest {
input.name = "code-review";
input.submittabilityExpression = "label:code-review=+2";
- gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input);
input.submittabilityExpression = "label:code-review=+1";
requestScopeOperations.setApiUserAnonymous();
AuthException thrown =
@@ -166,7 +166,7 @@ public class SubmitRequirementsAPIIT extends AbstractDaemonTest {
input.name = "code-review";
input.submittabilityExpression = "project:foo AND branch:refs/heads/main";
- gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input);
input.submittabilityExpression = null;
BadRequestException thrown =
assertThrows(
@@ -183,7 +183,7 @@ public class SubmitRequirementsAPIIT extends AbstractDaemonTest {
input.name = "code-review";
input.submittabilityExpression = "project:foo AND branch:refs/heads/main";
- gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input);
input.submittabilityExpression = "invalid_field:invalid_value";
BadRequestException thrown =
assertThrows(
@@ -207,7 +207,7 @@ public class SubmitRequirementsAPIIT extends AbstractDaemonTest {
input.name = "code-review";
input.submittabilityExpression = "project:foo AND branch:refs/heads/main";
- gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input);
input.overrideExpression = "invalid_field:invalid_value";
BadRequestException thrown =
assertThrows(
@@ -231,7 +231,7 @@ public class SubmitRequirementsAPIIT extends AbstractDaemonTest {
input.name = "code-review";
input.submittabilityExpression = "project:foo AND branch:refs/heads/main";
- gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input);
input.applicabilityExpression = "invalid_field:invalid_value";
BadRequestException thrown =
assertThrows(
@@ -531,8 +531,11 @@ public class SubmitRequirementsAPIIT extends AbstractDaemonTest {
.forUpdate()
.add(allow(Permission.READ).ref("refs/meta/config").group(REGISTERED_USERS))
.update();
+
requestScopeOperations.setApiUser(user.id());
- gApi.projects().name(project.get()).submitRequirements().get();
+
+ @SuppressWarnings("unused")
+ var unused = gApi.projects().name(project.get()).submitRequirements().get();
}
@Test
@@ -547,8 +550,12 @@ public class SubmitRequirementsAPIIT extends AbstractDaemonTest {
.forUpdate()
.add(block(Permission.READ).ref("refs/meta/config").group(REGISTERED_USERS))
.update();
+
requestScopeOperations.setApiUser(user.id());
- gApi.projects().name(project.get()).submitRequirements().withInherited(false).get();
+
+ @SuppressWarnings("unused")
+ var unused =
+ gApi.projects().name(project.get()).submitRequirements().withInherited(false).get();
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/BUILD b/javatests/com/google/gerrit/acceptance/api/revision/BUILD
index 517b04111b..9c6584e389 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/BUILD
+++ b/javatests/com/google/gerrit/acceptance/api/revision/BUILD
@@ -4,7 +4,10 @@ load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
srcs = [f],
group = f[:f.index(".")],
labels = ["api"],
- deps = [":revision-diff-it"],
+ deps = [
+ ":revision-diff-it",
+ "//javatests/com/google/gerrit/acceptance/server/change:util",
+ ],
) for f in glob(["*IT.java"])]
# This is needed because RevisionDiffIT has subclasses that depend on it
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/CommentWithFixIT.java b/javatests/com/google/gerrit/acceptance/api/revision/CommentWithFixIT.java
new file mode 100644
index 0000000000..d411a21da4
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/revision/CommentWithFixIT.java
@@ -0,0 +1,1397 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.acceptance.api.revision;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.server.change.CommentsUtil.createFixReplacementInfo;
+import static com.google.gerrit.acceptance.server.change.CommentsUtil.createFixSuggestionInfo;
+import static com.google.gerrit.acceptance.server.change.CommentsUtil.createRange;
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
+import static com.google.gerrit.extensions.common.testing.CommentInfoSubject.assertThatList;
+import static com.google.gerrit.extensions.common.testing.DiffInfoSubject.assertThat;
+import static com.google.gerrit.extensions.common.testing.EditInfoSubject.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
+import com.google.gerrit.extensions.common.ChangeType;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.common.DiffInfo;
+import com.google.gerrit.extensions.common.DiffInfo.IntraLineStatus;
+import com.google.gerrit.extensions.common.EditInfo;
+import com.google.gerrit.extensions.common.FixReplacementInfo;
+import com.google.gerrit.extensions.common.FixSuggestionInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.testing.BinaryResultSubject;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.TestCommentHelper;
+import com.google.inject.Inject;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+
+public class CommentWithFixIT extends AbstractDaemonTest {
+ @Inject private TestCommentHelper testCommentHelper;
+ @Inject private ChangeOperations changeOperations;
+ @Inject private AccountOperations accountOperations;
+ @Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private ExperimentFeatures experimentFeatures;
+
+ private static final String PLAIN_TEXT_CONTENT_TYPE = "text/plain";
+ private static final String GERRIT_COMMIT_MESSAGE_TYPE = "text/x-gerrit-commit-message";
+
+ private static final String FILE_NAME = "file_to_fix.txt";
+ private static final String FILE_NAME2 = "another_file_to_fix.txt";
+ private static final String FILE_NAME3 = "file_without_newline_at_end.txt";
+ private static final String FILE_CONTENT =
+ "First line\nSecond line\nThird line\nFourth line\nFifth line\nSixth line"
+ + "\nSeventh line\nEighth line\nNinth line\nTenth line\n";
+ private static final String FILE_CONTENT2 = "1st line\n2nd line\n3rd line\n";
+ private static final String FILE_CONTENT3 = "1st line\n2nd line";
+ private String changeId;
+ private String commitId;
+ private FixReplacementInfo fixReplacementInfo;
+ private FixSuggestionInfo fixSuggestionInfo;
+ private CommentInput withFixCommentInput;
+
+ @Before
+ public void setUp() throws Exception {
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(),
+ testRepo,
+ "Provide files which can be used for fixes",
+ ImmutableMap.of(
+ FILE_NAME, FILE_CONTENT, FILE_NAME2, FILE_CONTENT2, FILE_NAME3, FILE_CONTENT3));
+ PushOneCommit.Result changeResult = push.to("refs/for/master");
+ changeId = changeResult.getChangeId();
+ commitId = changeResult.getCommit().getName();
+
+ fixReplacementInfo = createFixReplacementInfo();
+ fixSuggestionInfo = createFixSuggestionInfo(fixReplacementInfo);
+ withFixCommentInput = TestCommentHelper.createCommentInput(FILE_NAME, fixSuggestionInfo);
+ }
+
+ @ConfigSuite.Default
+ public static Config setExperimentFlag() {
+ Config cfg = new Config();
+ cfg.setString(
+ "experiments",
+ null,
+ "enabled",
+ ExperimentFeaturesConstants.ALLOW_FIX_SUGGESTIONS_IN_COMMENTS);
+ return cfg;
+ }
+
+ @Test
+ public void fixSuggestionCannotPointToPatchsetLevel() throws Exception {
+ CommentInput input = TestCommentHelper.createCommentInput(FILE_NAME);
+ FixReplacementInfo brokenFixReplacement = createFixReplacementInfo();
+ brokenFixReplacement.path = PATCHSET_LEVEL;
+ input.fixSuggestions = ImmutableList.of(createFixSuggestionInfo(brokenFixReplacement));
+ BadRequestException ex =
+ assertThrows(
+ BadRequestException.class, () -> testCommentHelper.addComment(changeId, input));
+ assertThat(ex.getMessage()).contains("file path must not be " + PATCHSET_LEVEL);
+ }
+
+ @Test
+ public void hugeCommentIsRejected() {
+ int defaultSizeLimit = 1 << 20;
+ fixReplacementInfo.replacement = getStringFor(defaultSizeLimit + 1);
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> testCommentHelper.addComment(changeId, withFixCommentInput));
+ assertThat(thrown).hasMessageThat().contains("limit");
+ }
+
+ @Test
+ public void reasonablyLargeCommentIsAccepted() throws Exception {
+ int defaultSizeLimit = 1 << 10;
+ // Allow for a few hundred bytes in other fields.
+ fixReplacementInfo.replacement = getStringFor(defaultSizeLimit - 666);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+
+ List<CommentInfo> commentInfos = getComments();
+ assertThat(commentInfos).hasSize(1);
+ }
+
+ @Test
+ @GerritConfig(name = "change.commentSizeLimit", value = "0")
+ public void zeroForMaximumAllowedSizeOfCommentRemovesRestriction() throws Exception {
+ int defaultSizeLimit = 1 << 10;
+ fixReplacementInfo.replacement = getStringFor(2 * defaultSizeLimit);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+
+ List<CommentInfo> commentInfos = getComments();
+ assertThat(commentInfos).hasSize(1);
+ }
+
+ @Test
+ @GerritConfig(name = "change.commentSizeLimit", value = "-1")
+ public void negativeValueForMaximumAllowedSizeOfCommentRemovesRestriction() throws Exception {
+ int defaultSizeLimit = 1 << 20;
+ fixReplacementInfo.replacement = getStringFor(2 * defaultSizeLimit);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+
+ List<CommentInfo> commentInfos = getComments();
+ assertThat(commentInfos).hasSize(1);
+ }
+
+ @Test
+ public void addedFixSuggestionCanBeRetrieved() throws Exception {
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> commentInfos = getComments();
+
+ assertThatList(commentInfos).onlyElement().onlyFixSuggestion().isNotNull();
+ }
+
+ @Test
+ public void fixIdIsGeneratedForFixSuggestion() throws Exception {
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> commentInfos = getComments();
+
+ assertThatList(commentInfos).onlyElement().onlyFixSuggestion().fixId().isNotEmpty();
+ assertThatList(commentInfos)
+ .onlyElement()
+ .onlyFixSuggestion()
+ .fixId()
+ .isNotEqualTo(fixSuggestionInfo.fixId);
+ }
+
+ @Test
+ public void descriptionOfFixSuggestionIsAcceptedAsIs() throws Exception {
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> commentInfos = getComments();
+
+ assertThatList(commentInfos)
+ .onlyElement()
+ .onlyFixSuggestion()
+ .description()
+ .isEqualTo(fixSuggestionInfo.description);
+ }
+
+ @Test
+ public void descriptionOfFixSuggestionIsMandatory() {
+ fixSuggestionInfo.description = null;
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> testCommentHelper.addComment(changeId, withFixCommentInput));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains(
+ String.format(
+ "A description is required for the suggested fix of the comment on %s",
+ withFixCommentInput.path));
+ }
+
+ @Test
+ public void addedFixReplacementCanBeRetrieved() throws Exception {
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> commentInfos = getComments();
+
+ assertThatList(commentInfos).onlyElement().onlyFixSuggestion().onlyReplacement().isNotNull();
+ }
+
+ @Test
+ public void fixReplacementsAreMandatory() {
+ fixSuggestionInfo.replacements = Collections.emptyList();
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> testCommentHelper.addComment(changeId, withFixCommentInput));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains(
+ String.format(
+ "At least one replacement is required"
+ + " for the suggested fix of the comment on %s",
+ withFixCommentInput.path));
+ }
+
+ @Test
+ public void pathOfFixReplacementIsAcceptedAsIs() throws Exception {
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+
+ List<CommentInfo> commentInfos = getComments();
+
+ assertThatList(commentInfos)
+ .onlyElement()
+ .onlyFixSuggestion()
+ .onlyReplacement()
+ .path()
+ .isEqualTo(fixReplacementInfo.path);
+ }
+
+ @Test
+ public void pathOfFixReplacementIsMandatory() {
+ fixReplacementInfo.path = null;
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> testCommentHelper.addComment(changeId, withFixCommentInput));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains(
+ String.format(
+ "A file path must be given for the replacement of the comment on %s",
+ withFixCommentInput.path));
+ }
+
+ @Test
+ public void rangeOfFixReplacementIsAcceptedAsIs() throws Exception {
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+
+ List<CommentInfo> commentInfos = getComments();
+
+ assertThatList(commentInfos)
+ .onlyElement()
+ .onlyFixSuggestion()
+ .onlyReplacement()
+ .range()
+ .isEqualTo(fixReplacementInfo.range);
+ }
+
+ @Test
+ public void rangeOfFixReplacementIsMandatory() {
+ fixReplacementInfo.range = null;
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> testCommentHelper.addComment(changeId, withFixCommentInput));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains(
+ String.format(
+ "A range must be given for the replacement of the comment on %s",
+ withFixCommentInput.path));
+ }
+
+ @Test
+ public void rangeOfFixReplacementNeedsToBeValid() {
+ fixReplacementInfo.range = createRange(13, 9, 5, 10);
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> testCommentHelper.addComment(changeId, withFixCommentInput));
+ assertThat(thrown).hasMessageThat().contains("Range (13:9 - 5:10)");
+ }
+
+ @Test
+ public void commentWithRangeAndLine_lineIsIgnored() throws Exception {
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 1);
+ fixReplacementInfo1.replacement = "First modification\n";
+
+ withFixCommentInput.line = 1;
+ withFixCommentInput.range = createRange(2, 0, 3, 1);
+ withFixCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> comments = getComments();
+ assertThat(comments.get(0).line).isEqualTo(3);
+ }
+
+ @Test
+ public void rangesOfFixReplacementsOfSameFixSuggestionForSameFileMayNotOverlap() {
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 1);
+ fixReplacementInfo1.replacement = "First modification\n";
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = FILE_NAME;
+ fixReplacementInfo2.range = createRange(3, 0, 4, 0);
+ fixReplacementInfo2.replacement = "Second modification\n";
+
+ FixSuggestionInfo fixSuggestionInfo =
+ createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2);
+ withFixCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> testCommentHelper.addComment(changeId, withFixCommentInput));
+ assertThat(thrown).hasMessageThat().contains("overlap");
+ }
+
+ @Test
+ public void rangesOfFixReplacementsOfSameFixSuggestionForDifferentFileMayOverlap()
+ throws Exception {
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 1);
+ fixReplacementInfo1.replacement = "First modification\n";
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = FILE_NAME2;
+ fixReplacementInfo2.range = createRange(3, 0, 4, 0);
+ fixReplacementInfo2.replacement = "Second modification\n";
+
+ FixSuggestionInfo fixSuggestionInfo =
+ createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2);
+ withFixCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+
+ List<CommentInfo> commentInfos = getComments();
+ assertThatList(commentInfos).onlyElement().fixSuggestions().hasSize(1);
+ }
+
+ @Test
+ public void rangesOfFixReplacementsOfDifferentFixSuggestionsForSameFileMayOverlap()
+ throws Exception {
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 1);
+ fixReplacementInfo1.replacement = "First modification\n";
+ FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1);
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = FILE_NAME;
+ fixReplacementInfo2.range = createRange(3, 0, 4, 0);
+ fixReplacementInfo2.replacement = "Second modification\n";
+ FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2);
+
+ withFixCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo1, fixSuggestionInfo2);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+
+ List<CommentInfo> commentInfos = getComments();
+ assertThatList(commentInfos).onlyElement().fixSuggestions().hasSize(2);
+ }
+
+ @Test
+ public void fixReplacementsDoNotNeedToBeOrderedAccordingToRange() throws Exception {
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 0);
+ fixReplacementInfo1.replacement = "First modification\n";
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = FILE_NAME;
+ fixReplacementInfo2.range = createRange(3, 0, 4, 0);
+ fixReplacementInfo2.replacement = "Second modification\n";
+
+ FixReplacementInfo fixReplacementInfo3 = new FixReplacementInfo();
+ fixReplacementInfo3.path = FILE_NAME;
+ fixReplacementInfo3.range = createRange(4, 0, 5, 0);
+ fixReplacementInfo3.replacement = "Third modification\n";
+
+ FixSuggestionInfo fixSuggestionInfo =
+ createFixSuggestionInfo(fixReplacementInfo2, fixReplacementInfo1, fixReplacementInfo3);
+ withFixCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+
+ List<CommentInfo> commentInfos = getComments();
+ assertThatList(commentInfos).onlyElement().onlyFixSuggestion().replacements().hasSize(3);
+ }
+
+ @Test
+ public void replacementStringOfFixReplacementIsAcceptedAsIs() throws Exception {
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+
+ List<CommentInfo> commentInfos = getComments();
+
+ assertThatList(commentInfos)
+ .onlyElement()
+ .onlyFixSuggestion()
+ .onlyReplacement()
+ .replacement()
+ .isEqualTo(fixReplacementInfo.replacement);
+ }
+
+ @Test
+ public void replacementStringOfFixReplacementIsMandatory() {
+ fixReplacementInfo.replacement = null;
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> testCommentHelper.addComment(changeId, withFixCommentInput));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains(
+ String.format(
+ "A content for replacement must be "
+ + "indicated for the replacement of the comment on %s",
+ withFixCommentInput.path));
+ }
+
+ @Test
+ public void storedFixWithinALineCanBeApplied() throws Exception {
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> commentInfos = getComments();
+
+ List<String> fixIds = getFixIds(commentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ gApi.changes().id(changeId).current().applyFix(fixId);
+
+ Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
+ BinaryResultSubject.assertThat(file)
+ .value()
+ .asString()
+ .isEqualTo(
+ "First line\nSecond line\nTModified contentrd line\nFourth line\nFifth line\n"
+ + "Sixth line\nSeventh line\nEighth line\nNinth line\nTenth line\n");
+ }
+
+ @Test
+ public void applyStoredFixAfterUpdatingPreferredEmail() throws Exception {
+ String emailOne = "email1@example.com";
+ Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
+
+ // Create change
+ Change.Id change =
+ changeOperations
+ .newChange()
+ .project(project)
+ .file(FILE_NAME)
+ .content(FILE_CONTENT)
+ .owner(testUser)
+ .create();
+
+ // Add Robot Comment to the change
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+ testCommentHelper.addComment(project + "~" + change.get(), withFixCommentInput);
+
+ // Change preferred email for the user
+ String emailTwo = "email2@example.com";
+ accountOperations.account(testUser).forUpdate().preferredEmail(emailTwo).update();
+ requestScopeOperations.setApiUser(testUser);
+
+ // Fetch Fix ID
+ List<CommentInfo> commentInfoList = gApi.changes().id(change.get()).current().commentsAsList();
+
+ List<String> fixIds = getFixIds(commentInfoList);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ // Apply fix
+ gApi.changes().id(change.get()).current().applyFix(fixId);
+
+ EditInfo editInfo = gApi.changes().id(change.get()).edit().get().orElseThrow();
+ assertThat(editInfo.commit.committer.email).isEqualTo(emailOne);
+ }
+
+ @Test
+ public void storedFixSpanningMultipleLinesCanBeApplied() throws Exception {
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content\n5";
+ fixReplacementInfo.range = createRange(3, 2, 5, 3);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> commentInfos = getComments();
+ List<String> fixIds = getFixIds(commentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ gApi.changes().id(changeId).current().applyFix(fixId);
+
+ Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
+ BinaryResultSubject.assertThat(file)
+ .value()
+ .asString()
+ .isEqualTo(
+ "First line\nSecond line\nThModified content\n5th line\nSixth line\nSeventh line\n"
+ + "Eighth line\nNinth line\nTenth line\n");
+ }
+
+ @Test
+ public void storedFixWithTwoCloseReplacementsOnSameFileCanBeApplied() throws Exception {
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 0);
+ fixReplacementInfo1.replacement = "First modification\n";
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = FILE_NAME;
+ fixReplacementInfo2.range = createRange(3, 0, 4, 0);
+ fixReplacementInfo2.replacement = "Some other modified content\n";
+
+ FixSuggestionInfo fixSuggestionInfo =
+ createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2);
+ withFixCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> commentInfos = getComments();
+ List<String> fixIds = getFixIds(commentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ gApi.changes().id(changeId).current().applyFix(fixId);
+
+ Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
+ BinaryResultSubject.assertThat(file)
+ .value()
+ .asString()
+ .isEqualTo(
+ "First line\nFirst modification\nSome other modified content\nFourth line\nFifth line\n"
+ + "Sixth line\nSeventh line\nEighth line\nNinth line\nTenth line\n");
+ }
+
+ @Test
+ public void twoStoredFixesOnSameFileCanBeApplied() throws Exception {
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 0);
+ fixReplacementInfo1.replacement = "First modification\n";
+ FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1);
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = FILE_NAME;
+ fixReplacementInfo2.range = createRange(8, 0, 9, 0);
+ fixReplacementInfo2.replacement = "Some other modified content\n";
+ FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2);
+
+ CommentInput commentInput1 =
+ TestCommentHelper.createCommentInput(FILE_NAME, fixSuggestionInfo1);
+ CommentInput commentInput2 =
+ TestCommentHelper.createCommentInput(FILE_NAME, fixSuggestionInfo2);
+ testCommentHelper.addComment(changeId, commentInput1);
+ testCommentHelper.addComment(changeId, commentInput2);
+ List<CommentInfo> commentInfos = getComments();
+
+ List<String> fixIds = getFixIds(commentInfos);
+ gApi.changes().id(changeId).current().applyFix(fixIds.get(0));
+ gApi.changes().id(changeId).current().applyFix(fixIds.get(1));
+
+ Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
+ BinaryResultSubject.assertThat(file)
+ .value()
+ .asString()
+ .isEqualTo(
+ "First line\nFirst modification\nThird line\nFourth line\nFifth line\nSixth line\n"
+ + "Seventh line\nSome other modified content\nNinth line\nTenth line\n");
+ }
+
+ @Test
+ public void twoConflictingStoredFixesOnSameFileCannotBeApplied() throws Exception {
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 1);
+ fixReplacementInfo1.replacement = "First modification\n";
+ FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1);
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = FILE_NAME;
+ fixReplacementInfo2.range = createRange(3, 0, 4, 0);
+ fixReplacementInfo2.replacement = "Some other modified content\n";
+ FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2);
+
+ CommentInput commentInput1 =
+ TestCommentHelper.createCommentInput(FILE_NAME, fixSuggestionInfo1);
+ CommentInput commentInput2 =
+ TestCommentHelper.createCommentInput(FILE_NAME, fixSuggestionInfo2);
+ testCommentHelper.addComment(changeId, commentInput1);
+ testCommentHelper.addComment(changeId, commentInput2);
+ List<CommentInfo> commentInfos = getComments();
+
+ List<String> fixIds = getFixIds(commentInfos);
+ gApi.changes().id(changeId).current().applyFix(fixIds.get(0));
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.changes().id(changeId).current().applyFix(fixIds.get(1)));
+ assertThat(thrown).hasMessageThat().contains("merge");
+ }
+
+ @Test
+ public void twoStoredFixesOfSameCommentCanBeApplied() throws Exception {
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 0);
+ fixReplacementInfo1.replacement = "First modification\n";
+ FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1);
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = FILE_NAME;
+ fixReplacementInfo2.range = createRange(8, 0, 9, 0);
+ fixReplacementInfo2.replacement = "Some other modified content\n";
+ FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2);
+
+ withFixCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo1, fixSuggestionInfo2);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> commentInfos = getComments();
+
+ List<String> fixIds = getFixIds(commentInfos);
+ gApi.changes().id(changeId).current().applyFix(fixIds.get(0));
+ gApi.changes().id(changeId).current().applyFix(fixIds.get(1));
+
+ Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
+ BinaryResultSubject.assertThat(file)
+ .value()
+ .asString()
+ .isEqualTo(
+ "First line\nFirst modification\nThird line\nFourth line\nFifth line\nSixth line\n"
+ + "Seventh line\nSome other modified content\nNinth line\nTenth line\n");
+ }
+
+ @Test
+ public void storedFixReferringToDifferentFileThanCommentCanBeApplied() throws Exception {
+ fixReplacementInfo.path = FILE_NAME2;
+ fixReplacementInfo.range = createRange(2, 0, 3, 0);
+ fixReplacementInfo.replacement = "Modified content\n";
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> commentInfos = getComments();
+ List<String> fixIds = getFixIds(commentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ gApi.changes().id(changeId).current().applyFix(fixId);
+
+ Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME2);
+ BinaryResultSubject.assertThat(file)
+ .value()
+ .asString()
+ .isEqualTo("1st line\nModified content\n3rd line\n");
+ }
+
+ @Test
+ public void storedFixInvolvingTwoFilesCanBeApplied() throws Exception {
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 0);
+ fixReplacementInfo1.replacement = "First modification\n";
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = FILE_NAME2;
+ fixReplacementInfo2.range = createRange(1, 0, 2, 0);
+ fixReplacementInfo2.replacement = "Different file modification\n";
+
+ FixSuggestionInfo fixSuggestionInfo =
+ createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2);
+ withFixCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> commentInfos = getComments();
+ List<String> fixIds = getFixIds(commentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ gApi.changes().id(changeId).current().applyFix(fixId);
+
+ Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
+ BinaryResultSubject.assertThat(file)
+ .value()
+ .asString()
+ .isEqualTo(
+ "First line\nFirst modification\nThird line\nFourth line\nFifth line\nSixth line\n"
+ + "Seventh line\nEighth line\nNinth line\nTenth line\n");
+ Optional<BinaryResult> file2 = gApi.changes().id(changeId).edit().getFile(FILE_NAME2);
+ BinaryResultSubject.assertThat(file2)
+ .value()
+ .asString()
+ .isEqualTo("Different file modification\n2nd line\n3rd line\n");
+ }
+
+ @Test
+ public void storedFixReferringToNonExistentFileCannotBeApplied() throws Exception {
+ fixReplacementInfo.path = "a_non_existent_file.txt";
+ fixReplacementInfo.range = createRange(1, 0, 2, 0);
+ fixReplacementInfo.replacement = "Modified content\n";
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> commentInfos = getComments();
+ List<String> fixIds = getFixIds(commentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> gApi.changes().id(changeId).current().applyFix(fixId));
+ }
+
+ @Test
+ public void storedFixOnPreviousPatchSetWithoutChangeEditCannotBeApplied() throws Exception {
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> commentInfos = getComments();
+
+ // Remember patch set and add another one.
+ String previousRevision = gApi.changes().id(changeId).get().currentRevision;
+ amendChange(changeId);
+
+ List<String> fixIds = getFixIds(commentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.changes().id(changeId).revision(previousRevision).applyFix(fixId));
+ assertThat(thrown).hasMessageThat().contains("current");
+ }
+
+ @Test
+ public void storedFixOnPreviousPatchSetWithExistingChangeEditCanBeApplied() throws Exception {
+ // Create an empty change edit.
+ gApi.changes().id(changeId).edit().create();
+
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> commentInfos = getComments();
+
+ // Remember patch set and add another one.
+ String previousRevision = gApi.changes().id(changeId).get().currentRevision;
+ amendChange(changeId);
+
+ List<String> fixIds = getFixIds(commentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ EditInfo editInfo = gApi.changes().id(changeId).revision(previousRevision).applyFix(fixId);
+
+ Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
+ BinaryResultSubject.assertThat(file)
+ .value()
+ .asString()
+ .isEqualTo(
+ "First line\nSecond line\nTModified contentrd line\nFourth line\nFifth line\n"
+ + "Sixth line\nSeventh line\nEighth line\nNinth line\nTenth line\n");
+ assertThat(editInfo).baseRevision().isEqualTo(previousRevision);
+ }
+
+ @Test
+ public void storedFixOnCurrentPatchSetWithChangeEditOnPreviousPatchSetCannotBeApplied()
+ throws Exception {
+ // Create an empty change edit.
+ gApi.changes().id(changeId).edit().create();
+
+ // Add another patch set.
+ amendChange(changeId);
+
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> commentInfos = getComments();
+
+ List<String> fixIds = getFixIds(commentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.changes().id(changeId).current().applyFix(fixId));
+ assertThat(thrown).hasMessageThat().contains("based");
+ }
+
+ @Test
+ public void storedFixDoesNotModifyCommitMessageOfChangeEdit() throws Exception {
+ String changeEditCommitMessage =
+ "This is the commit message of the change edit.\n\nChange-Id: " + changeId + "\n";
+ gApi.changes().id(changeId).edit().modifyCommitMessage(changeEditCommitMessage);
+
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ String fixId = Iterables.getOnlyElement(getFixIds(getComments()));
+
+ gApi.changes().id(changeId).current().applyFix(fixId);
+
+ String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
+ assertThat(commitMessage).isEqualTo(changeEditCommitMessage);
+ }
+
+ @Test
+ public void storedFixOnCommitMessageCanBeApplied() throws Exception {
+ // Set a dedicated commit message.
+ String footer = "\nChange-Id: " + changeId + "\n";
+ String originalCommitMessage = "Line 1 of commit message\nLine 2 of commit message\n" + footer;
+ gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
+ gApi.changes().id(changeId).edit().publish();
+
+ withFixCommentInput.path = Patch.COMMIT_MSG;
+ fixReplacementInfo.path = Patch.COMMIT_MSG;
+ fixReplacementInfo.replacement = "Modified line\n";
+ fixReplacementInfo.range = createRange(7, 0, 8, 0);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ String fixId = Iterables.getOnlyElement(getFixIds(getComments()));
+
+ gApi.changes().id(changeId).current().applyFix(fixId);
+
+ String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
+ assertThat(commitMessage).isEqualTo("Modified line\nLine 2 of commit message\n" + footer);
+ }
+
+ @Test
+ public void storedFixOnHeaderPartOfCommitMessageCannotBeApplied() throws Exception {
+ // Set a dedicated commit message.
+ String footer = "Change-Id: " + changeId;
+ String originalCommitMessage =
+ "Line 1 of commit message\nLine 2 of commit message\n" + "\n" + footer + "\n";
+ gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
+ gApi.changes().id(changeId).edit().publish();
+
+ withFixCommentInput.path = Patch.COMMIT_MSG;
+ fixReplacementInfo.path = Patch.COMMIT_MSG;
+ fixReplacementInfo.replacement = "Modified line\n";
+ fixReplacementInfo.range = createRange(1, 0, 2, 0);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ String fixId = Iterables.getOnlyElement(getFixIds(getComments()));
+
+ ResourceConflictException exception =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.changes().id(changeId).current().applyFix(fixId));
+ assertThat(exception).hasMessageThat().contains("header");
+ }
+
+ @Test
+ public void storedFixContainingSeveralModificationsOfCommitMessageCanBeApplied()
+ throws Exception {
+ // Set a dedicated commit message.
+ String footer = "\nChange-Id: " + changeId + "\n";
+ String originalCommitMessage =
+ "Line 1 of commit message\nLine 2 of commit message\nLine 3 of commit message\n" + footer;
+ gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
+ gApi.changes().id(changeId).edit().publish();
+
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = Patch.COMMIT_MSG;
+ fixReplacementInfo1.range = createRange(7, 0, 8, 0);
+ fixReplacementInfo1.replacement = "Modified line 1\n";
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = Patch.COMMIT_MSG;
+ fixReplacementInfo2.range = createRange(9, 0, 10, 0);
+ fixReplacementInfo2.replacement = "Modified line 3\n";
+
+ FixSuggestionInfo fixSuggestionInfo =
+ createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2);
+ withFixCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
+ withFixCommentInput.path = Patch.COMMIT_MSG;
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ String fixId = Iterables.getOnlyElement(getFixIds(getComments()));
+
+ gApi.changes().id(changeId).current().applyFix(fixId);
+
+ String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
+ assertThat(commitMessage)
+ .isEqualTo("Modified line 1\nLine 2 of commit message\nModified line 3\n" + footer);
+ }
+
+ @Test
+ public void storedFixModifyingTheCommitMessageAndAFileCanBeApplied() throws Exception {
+ // Set a dedicated commit message.
+ String footer = "\nChange-Id: " + changeId + "\n";
+ String originalCommitMessage = "Line 1 of commit message\nLine 2 of commit message\n" + footer;
+ gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
+ gApi.changes().id(changeId).edit().publish();
+
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = Patch.COMMIT_MSG;
+ fixReplacementInfo1.range = createRange(7, 0, 8, 0);
+ fixReplacementInfo1.replacement = "Modified line 1\n";
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = FILE_NAME2;
+ fixReplacementInfo2.range = createRange(1, 0, 2, 0);
+ fixReplacementInfo2.replacement = "File modification\n";
+
+ FixSuggestionInfo fixSuggestionInfo =
+ createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2);
+ withFixCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ String fixId = Iterables.getOnlyElement(getFixIds(getComments()));
+
+ gApi.changes().id(changeId).current().applyFix(fixId);
+
+ String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
+ assertThat(commitMessage).isEqualTo("Modified line 1\nLine 2 of commit message\n" + footer);
+ Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME2);
+ BinaryResultSubject.assertThat(file)
+ .value()
+ .asString()
+ .isEqualTo("File modification\n2nd line\n3rd line\n");
+ }
+
+ @Test
+ public void twoStoredFixesOnCommitMessageCanBeAppliedOneAfterTheOther() throws Exception {
+ // Set a dedicated commit message.
+ String footer = "\nChange-Id: " + changeId + "\n";
+ String originalCommitMessage =
+ "Line 1 of commit message\nLine 2 of commit message\nLine 3 of commit message\n" + footer;
+ gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
+ gApi.changes().id(changeId).edit().publish();
+
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = Patch.COMMIT_MSG;
+ fixReplacementInfo1.range = createRange(7, 0, 8, 0);
+ fixReplacementInfo1.replacement = "Modified line 1\n";
+ FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1);
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = Patch.COMMIT_MSG;
+ fixReplacementInfo2.range = createRange(9, 0, 10, 0);
+ fixReplacementInfo2.replacement = "Modified line 3\n";
+ FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2);
+
+ CommentInput commentInput1 =
+ TestCommentHelper.createCommentInput(FILE_NAME, fixSuggestionInfo1);
+ CommentInput commentInput2 =
+ TestCommentHelper.createCommentInput(FILE_NAME, fixSuggestionInfo2);
+ testCommentHelper.addComment(changeId, commentInput1);
+ testCommentHelper.addComment(changeId, commentInput2);
+ List<String> fixIds = getFixIds(getComments());
+
+ gApi.changes().id(changeId).current().applyFix(fixIds.get(0));
+ gApi.changes().id(changeId).current().applyFix(fixIds.get(1));
+
+ String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
+ assertThat(commitMessage)
+ .isEqualTo("Modified line 1\nLine 2 of commit message\nModified line 3\n" + footer);
+ }
+
+ @Test
+ public void twoConflictingStoredFixesOnCommitMessageCanNotBeAppliedOneAfterTheOther()
+ throws Exception {
+ // Set a dedicated commit message.
+ String footer = "Change-Id: " + changeId;
+ String originalCommitMessage =
+ "Line 1 of commit message\nLine 2 of commit message\nLine 3 of commit message\n\n"
+ + footer
+ + "\n";
+ gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
+ gApi.changes().id(changeId).edit().publish();
+
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = Patch.COMMIT_MSG;
+ fixReplacementInfo1.range = createRange(7, 0, 8, 0);
+ fixReplacementInfo1.replacement = "Modified line 1\n";
+ FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1);
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = Patch.COMMIT_MSG;
+ fixReplacementInfo2.range = createRange(7, 0, 10, 0);
+ fixReplacementInfo2.replacement = "Differently modified line 1\n";
+ FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2);
+
+ CommentInput commentInput1 =
+ TestCommentHelper.createCommentInput(FILE_NAME, fixSuggestionInfo1);
+ CommentInput commentInput2 =
+ TestCommentHelper.createCommentInput(FILE_NAME, fixSuggestionInfo2);
+ testCommentHelper.addComment(changeId, commentInput1);
+ testCommentHelper.addComment(changeId, commentInput2);
+ List<String> fixIds = getFixIds(getComments());
+
+ gApi.changes().id(changeId).current().applyFix(fixIds.get(0));
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.changes().id(changeId).current().applyFix(fixIds.get(1)));
+ }
+
+ @Test
+ public void applyingStoredFixTwiceIsIdempotent() throws Exception {
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> commentInfos = getComments();
+
+ List<String> fixIds = getFixIds(commentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ gApi.changes().id(changeId).current().applyFix(fixId);
+ String expectedEditCommit =
+ gApi.changes().id(changeId).edit().get().map(edit -> edit.commit.commit).orElse("");
+
+ // Apply the fix again.
+ gApi.changes().id(changeId).current().applyFix(fixId);
+
+ Optional<EditInfo> editInfo = gApi.changes().id(changeId).edit().get();
+ assertThat(editInfo).value().commit().commit().isEqualTo(expectedEditCommit);
+ }
+
+ @Test
+ public void nonExistentStoredFixCannotBeApplied() throws Exception {
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> commentInfos = getComments();
+
+ List<String> fixIds = getFixIds(commentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+ String nonExistentFixId = fixId + "_non-existent";
+
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> gApi.changes().id(changeId).current().applyFix(nonExistentFixId));
+ }
+
+ @Test
+ public void applyingStoredFixReturnsEditInfoForCreatedChangeEdit() throws Exception {
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> commentInfos = getComments();
+
+ List<String> fixIds = getFixIds(commentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ EditInfo editInfo = gApi.changes().id(changeId).current().applyFix(fixId);
+
+ Optional<EditInfo> expectedEditInfo = gApi.changes().id(changeId).edit().get();
+ String expectedEditCommit = expectedEditInfo.map(edit -> edit.commit.commit).orElse("");
+ assertThat(editInfo).commit().commit().isEqualTo(expectedEditCommit);
+ String expectedBaseRevision = expectedEditInfo.map(edit -> edit.baseRevision).orElse("");
+ assertThat(editInfo).baseRevision().isEqualTo(expectedBaseRevision);
+ }
+
+ @Test
+ public void applyingStoredFixOnTopOfChangeEditReturnsEditInfoForUpdatedChangeEdit()
+ throws Exception {
+ gApi.changes().id(changeId).edit().create();
+
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> commentInfos = getComments();
+
+ List<String> fixIds = getFixIds(commentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ EditInfo editInfo = gApi.changes().id(changeId).current().applyFix(fixId);
+
+ Optional<EditInfo> expectedEditInfo = gApi.changes().id(changeId).edit().get();
+ String expectedEditCommit = expectedEditInfo.map(edit -> edit.commit.commit).orElse("");
+ assertThat(editInfo).commit().commit().isEqualTo(expectedEditCommit);
+ String expectedBaseRevision = expectedEditInfo.map(edit -> edit.baseRevision).orElse("");
+ assertThat(editInfo).baseRevision().isEqualTo(expectedBaseRevision);
+ }
+
+ @Test
+ public void previewStoredFixWithNonexistentFixId() throws Exception {
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> gApi.changes().id(changeId).current().getFixPreview("Non existing fixId"));
+ }
+
+ @Test
+ public void previewStoredFixForCommitMsg() throws Exception {
+ String footer = "Change-Id: " + changeId;
+ updateCommitMessage(
+ changeId,
+ "Commit title\n\nCommit message line 1\nLine 2\nLine 3\nLast line\n\n" + footer + "\n");
+ FixReplacementInfo commitMsgReplacement = new FixReplacementInfo();
+ commitMsgReplacement.path = Patch.COMMIT_MSG;
+ // The test assumes that the first 5 lines is a header.
+ // Line 10 has content "Line 2"
+ commitMsgReplacement.range = createRange(10, 0, 11, 0);
+ commitMsgReplacement.replacement = "New content\n";
+
+ FixSuggestionInfo commitMsgSuggestionInfo = createFixSuggestionInfo(commitMsgReplacement);
+ CommentInput commitMsgCommentInput =
+ TestCommentHelper.createCommentInput(Patch.COMMIT_MSG, commitMsgSuggestionInfo);
+ testCommentHelper.addComment(changeId, commitMsgCommentInput);
+
+ List<CommentInfo> commentInfos = getComments();
+
+ List<String> fixIds = getFixIds(commentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ Map<String, DiffInfo> fixPreview = gApi.changes().id(changeId).current().getFixPreview(fixId);
+ assertThat(fixPreview).hasSize(1);
+ assertThat(fixPreview).containsKey(Patch.COMMIT_MSG);
+
+ DiffInfo diff = fixPreview.get(Patch.COMMIT_MSG);
+ assertThat(diff).metaA().name().isEqualTo(Patch.COMMIT_MSG);
+ assertThat(diff).metaA().contentType().isEqualTo(GERRIT_COMMIT_MESSAGE_TYPE);
+ assertThat(diff).metaB().name().isEqualTo(Patch.COMMIT_MSG);
+ assertThat(diff).metaB().contentType().isEqualTo(GERRIT_COMMIT_MESSAGE_TYPE);
+
+ assertThat(diff).content().element(0).commonLines().hasSize(9);
+ // Header has a dynamic content, do not check it
+ assertThat(diff).content().element(0).commonLines().element(6).isEqualTo("Commit title");
+ assertThat(diff).content().element(0).commonLines().element(7).isEqualTo("");
+ assertThat(diff)
+ .content()
+ .element(0)
+ .commonLines()
+ .element(8)
+ .isEqualTo("Commit message line 1");
+ assertThat(diff).content().element(1).linesOfA().containsExactly("Line 2");
+ assertThat(diff).content().element(1).linesOfB().containsExactly("New content");
+ assertThat(diff)
+ .content()
+ .element(2)
+ .commonLines()
+ .containsExactly("Line 3", "Last line", "", footer, "");
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.disabled",
+ values = {ExperimentFeaturesConstants.ALLOW_FIX_SUGGESTIONS_IN_COMMENTS})
+ public void commentWithFixFailsToPersistWithoutFeatureFlag() {
+ IllegalStateException thrown =
+ assertThrows(
+ IllegalStateException.class,
+ () -> testCommentHelper.addComment(changeId, withFixCommentInput));
+ assertThat(thrown).hasMessageThat().contains("feature flag prohibits setting fixSuggestions");
+ }
+
+ private void updateCommitMessage(String changeId, String newCommitMessage) throws Exception {
+ gApi.changes().id(changeId).edit().create();
+ gApi.changes().id(changeId).edit().modifyCommitMessage(newCommitMessage);
+ PublishChangeEditInput publishInput = new PublishChangeEditInput();
+ gApi.changes().id(changeId).edit().publish(publishInput);
+ }
+
+ @Test
+ public void previewStoredFixForNonExistingFile() throws Exception {
+ FixReplacementInfo replacement = new FixReplacementInfo();
+ replacement.path = "a_non_existent_file.txt";
+ replacement.range = createRange(1, 0, 2, 0);
+ replacement.replacement = "Modified content\n";
+
+ FixSuggestionInfo fixSuggestion = createFixSuggestionInfo(replacement);
+ CommentInput commentInput = TestCommentHelper.createCommentInput(FILE_NAME2, fixSuggestion);
+ testCommentHelper.addComment(changeId, commentInput);
+
+ List<CommentInfo> commentInfos = getComments();
+ List<String> fixIds = getFixIds(commentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> gApi.changes().id(changeId).current().getFixPreview(fixId));
+ }
+
+ @Test
+ public void previewStoredFix() throws Exception {
+ FixReplacementInfo fixReplacementInfoFile1 = new FixReplacementInfo();
+ fixReplacementInfoFile1.path = FILE_NAME;
+ fixReplacementInfoFile1.replacement = "some replacement code";
+ fixReplacementInfoFile1.range = createRange(3, 9, 8, 4);
+
+ FixReplacementInfo fixReplacementInfoFile2 = new FixReplacementInfo();
+ fixReplacementInfoFile2.path = FILE_NAME2;
+ fixReplacementInfoFile2.replacement = "New line\n";
+ fixReplacementInfoFile2.range = createRange(2, 0, 2, 0);
+
+ fixSuggestionInfo = createFixSuggestionInfo(fixReplacementInfoFile1, fixReplacementInfoFile2);
+
+ withFixCommentInput = TestCommentHelper.createCommentInput(FILE_NAME, fixSuggestionInfo);
+
+ testCommentHelper.addComment(changeId, withFixCommentInput);
+ List<CommentInfo> commentInfos = getComments();
+
+ List<String> fixIds = getFixIds(commentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ Map<String, DiffInfo> fixPreview = gApi.changes().id(changeId).current().getFixPreview(fixId);
+ assertThat(fixPreview).hasSize(2);
+ assertThat(fixPreview).containsKey(FILE_NAME);
+ assertThat(fixPreview).containsKey(FILE_NAME2);
+
+ DiffInfo diff = fixPreview.get(FILE_NAME);
+ assertThat(diff).intralineStatus().isEqualTo(IntraLineStatus.OK);
+ assertThat(diff).webLinks().isNull();
+ assertThat(diff).binary().isNull();
+ assertThat(diff).diffHeader().isNull();
+ assertThat(diff).changeType().isEqualTo(ChangeType.MODIFIED);
+ assertThat(diff).metaA().totalLineCount().isEqualTo(11);
+ assertThat(diff).metaA().name().isEqualTo(FILE_NAME);
+ assertThat(diff).metaA().commitId().isEqualTo(commitId);
+ assertThat(diff).metaA().contentType().isEqualTo(PLAIN_TEXT_CONTENT_TYPE);
+ assertThat(diff).metaA().webLinks().isNull();
+ assertThat(diff).metaB().totalLineCount().isEqualTo(6);
+ assertThat(diff).metaB().name().isEqualTo(FILE_NAME);
+ assertThat(diff).metaB().commitId().isNull();
+ assertThat(diff).metaB().contentType().isEqualTo(PLAIN_TEXT_CONTENT_TYPE);
+ assertThat(diff).metaB().webLinks().isNull();
+
+ assertThat(diff).content().hasSize(3);
+ assertThat(diff)
+ .content()
+ .element(0)
+ .commonLines()
+ .containsExactly("First line", "Second line");
+ assertThat(diff).content().element(0).linesOfA().isNull();
+ assertThat(diff).content().element(0).linesOfB().isNull();
+
+ assertThat(diff).content().element(1).commonLines().isNull();
+ assertThat(diff)
+ .content()
+ .element(1)
+ .linesOfA()
+ .containsExactly(
+ "Third line", "Fourth line", "Fifth line", "Sixth line", "Seventh line", "Eighth line");
+ assertThat(diff)
+ .content()
+ .element(1)
+ .linesOfB()
+ .containsExactly("Third linsome replacement codeth line");
+
+ assertThat(diff)
+ .content()
+ .element(2)
+ .commonLines()
+ .containsExactly("Ninth line", "Tenth line", "");
+ assertThat(diff).content().element(2).linesOfA().isNull();
+ assertThat(diff).content().element(2).linesOfB().isNull();
+
+ DiffInfo diff2 = fixPreview.get(FILE_NAME2);
+ assertThat(diff2).intralineStatus().isEqualTo(IntraLineStatus.OK);
+ assertThat(diff2).webLinks().isNull();
+ assertThat(diff2).binary().isNull();
+ assertThat(diff2).diffHeader().isNull();
+ assertThat(diff2).changeType().isEqualTo(ChangeType.MODIFIED);
+ assertThat(diff2).metaA().totalLineCount().isEqualTo(4);
+ assertThat(diff2).metaA().name().isEqualTo(FILE_NAME2);
+ assertThat(diff2).metaA().commitId().isEqualTo(commitId);
+ assertThat(diff2).metaA().contentType().isEqualTo(PLAIN_TEXT_CONTENT_TYPE);
+ assertThat(diff2).metaA().webLinks().isNull();
+ assertThat(diff2).metaB().totalLineCount().isEqualTo(5);
+ assertThat(diff2).metaB().name().isEqualTo(FILE_NAME2);
+ assertThat(diff2).metaB().commitId().isNull();
+ assertThat(diff2).metaA().contentType().isEqualTo(PLAIN_TEXT_CONTENT_TYPE);
+ assertThat(diff2).metaB().webLinks().isNull();
+
+ assertThat(diff2).content().hasSize(3);
+ assertThat(diff2).content().element(0).commonLines().containsExactly("1st line");
+ assertThat(diff2).content().element(0).linesOfA().isNull();
+ assertThat(diff2).content().element(0).linesOfB().isNull();
+
+ assertThat(diff2).content().element(1).commonLines().isNull();
+ assertThat(diff2).content().element(1).linesOfA().isNull();
+ assertThat(diff2).content().element(1).linesOfB().containsExactly("New line");
+
+ assertThat(diff2)
+ .content()
+ .element(2)
+ .commonLines()
+ .containsExactly("2nd line", "3rd line", "");
+ assertThat(diff2).content().element(2).linesOfA().isNull();
+ assertThat(diff2).content().element(2).linesOfB().isNull();
+ }
+
+ @Test
+ public void previewStoredFixAddNewLineAtEnd() throws Exception {
+ FixReplacementInfo replacement = new FixReplacementInfo();
+ replacement.path = FILE_NAME3;
+ replacement.range = createRange(2, 8, 2, 8);
+ replacement.replacement = "\n";
+
+ FixSuggestionInfo fixSuggestion = createFixSuggestionInfo(replacement);
+ CommentInput commentInput = TestCommentHelper.createCommentInput(FILE_NAME3, fixSuggestion);
+ testCommentHelper.addComment(changeId, commentInput);
+
+ List<CommentInfo> commentInfos = getComments();
+
+ List<String> fixIds = getFixIds(commentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ Map<String, DiffInfo> fixPreview = gApi.changes().id(changeId).current().getFixPreview(fixId);
+
+ assertThat(fixPreview).hasSize(1);
+ assertThat(fixPreview).containsKey(FILE_NAME3);
+
+ DiffInfo diff = fixPreview.get(FILE_NAME3);
+ assertThat(diff).metaA().totalLineCount().isEqualTo(2);
+ // Original file doesn't have EOL marker at the end of file.
+ // Due to the additional EOL mark diff has one additional line
+ // This behavior is in line with ordinary get diff API.
+ assertThat(diff).metaB().totalLineCount().isEqualTo(3);
+
+ assertThat(diff).content().hasSize(2);
+ assertThat(diff).content().element(0).commonLines().containsExactly("1st line");
+ assertThat(diff).content().element(1).linesOfA().containsExactly("2nd line");
+ assertThat(diff).content().element(1).linesOfB().containsExactly("2nd line", "");
+ }
+
+ private List<CommentInfo> getComments() throws RestApiException {
+ return gApi.changes().id(changeId).current().commentsAsList();
+ }
+
+ private static String getStringFor(int numberOfBytes) {
+ char[] chars = new char[numberOfBytes];
+ // 'a' will require one byte even when mapped to a JSON string
+ Arrays.fill(chars, 'a');
+ return new String(chars);
+ }
+
+ private static List<String> getFixIds(List<CommentInfo> comments) {
+ assertThatList(comments).isNotNull();
+ return comments.stream()
+ .map(commentInfo -> commentInfo.fixSuggestions)
+ .filter(Objects::nonNull)
+ .flatMap(List::stream)
+ .map(fixSuggestionInfo -> fixSuggestionInfo.fixId)
+ .collect(toList());
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java
index ba45fb2a09..d61c2d67e5 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java
@@ -77,7 +77,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
newComment(patchset2Id).create();
newComment(patchset3Id).create();
- List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+ ImmutableList<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
assertThatList(portedComments).comparingElementsUsing(hasUuid()).containsExactly(comment1Uuid);
}
@@ -94,7 +94,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
String comment1Uuid = newComment(patchset1Id).create();
String comment3Uuid = newComment(patchset3Id).create();
- List<CommentInfo> portedComments = flatten(getPortedComments(patchset4Id));
+ ImmutableList<CommentInfo> portedComments = flatten(getPortedComments(patchset4Id));
assertThat(portedComments)
.comparingElementsUsing(hasUuid())
@@ -111,7 +111,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
String comment1Uuid = newComment(patchset1Id).create();
String comment2Uuid = newComment(patchset1Id).create();
- List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+ ImmutableList<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
assertThat(portedComments)
.comparingElementsUsing(hasUuid())
@@ -129,7 +129,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
String child1CommentUuid = newComment(patchset1Id).parentUuid(rootCommentUuid).create();
String child2CommentUuid = newComment(patchset1Id).parentUuid(child1CommentUuid).create();
- List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+ ImmutableList<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
assertThat(portedComments)
.comparingElementsUsing(hasUuid())
@@ -146,7 +146,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
newComment(patchset1Id).resolved().create();
String comment2Uuid = newComment(patchset1Id).unresolved().create();
- List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+ ImmutableList<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
assertThat(portedComments).comparingElementsUsing(hasUuid()).containsExactly(comment2Uuid);
}
@@ -174,7 +174,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
rebaseChangeOn(changeId.toString(), newBase);
PatchSet.Id ps3Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
- List<CommentInfo> portedComments = flatten(getPortedComments(ps3Id));
+ ImmutableList<CommentInfo> portedComments = flatten(getPortedComments(ps3Id));
assertThat(portedComments).hasSize(1);
int portedLine = portedComments.get(0).line;
BinaryResult fileContent = gApi.changes().id(changeId.get()).current().file(fileName).content();
@@ -195,7 +195,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
String comment1Uuid = newDraftComment(patchset1Id).author(accountId).resolved().create();
String comment2Uuid = newDraftComment(patchset1Id).author(accountId).unresolved().create();
- List<CommentInfo> portedComments =
+ ImmutableList<CommentInfo> portedComments =
flatten(getPortedDraftCommentsOfUser(patchset2Id, accountId));
assertThat(portedComments)
@@ -216,7 +216,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
String rootComment2Uuid = newComment(patchset1Id).unresolved().create();
newComment(patchset1Id).parentUuid(rootComment2Uuid).resolved().create();
- List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+ ImmutableList<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
assertThat(portedComments)
.comparingElementsUsing(hasUuid())
@@ -246,7 +246,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
.createdOn(now.plusSeconds(10))
.create();
- List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+ ImmutableList<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
assertThat(portedComments)
.comparingElementsUsing(hasUuid())
@@ -272,7 +272,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
// Draft comments are only visible to their author.
requestScopeOps.setApiUser(accountId);
- List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+ ImmutableList<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
assertThat(portedComments).comparingElementsUsing(hasUuid()).containsExactly(rootComment2Uuid);
}
@@ -289,7 +289,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
// Draft comments are only visible to their author.
requestScopeOps.setApiUser(accountId);
- List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+ ImmutableList<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
assertThatList(portedComments).isEmpty();
}
@@ -304,7 +304,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
// Add comment.
newComment(patchset1Id).author(accountId).create();
- List<CommentInfo> portedComments =
+ ImmutableList<CommentInfo> portedComments =
flatten(getPortedDraftCommentsOfUser(patchset2Id, accountId));
assertThatList(portedComments).isEmpty();
@@ -320,7 +320,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
// Add draft comment.
newComment(patchset1Id).author(accountId).create();
- List<CommentInfo> portedComments =
+ ImmutableList<CommentInfo> portedComments =
flatten(getPortedDraftCommentsOfUser(patchset2Id, accountId));
assertThatList(portedComments).isEmpty();
@@ -337,7 +337,8 @@ public class PortedCommentsIT extends AbstractDaemonTest {
// Add draft comment.
newComment(patchset1Id).author(otherUserId).create();
- List<CommentInfo> portedComments = flatten(getPortedDraftCommentsOfUser(patchset2Id, userId));
+ ImmutableList<CommentInfo> portedComments =
+ flatten(getPortedDraftCommentsOfUser(patchset2Id, userId));
assertThatList(portedComments).isEmpty();
}
@@ -365,7 +366,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
String patchsetLevelCommentUuid =
newComment(patchset1Id).message("Patchset-level comment").onPatchsetLevel().create();
- List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+ ImmutableList<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
assertThat(portedComments)
.comparingElementsUsing(hasUuid())
@@ -382,7 +383,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
// Add comments.
String commentUuid = newComment(patchset1Id).onParentCommit().create();
- List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+ ImmutableList<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
assertThat(portedComments).comparingElementsUsing(hasUuid()).containsExactly(commentUuid);
}
@@ -396,7 +397,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
// Add comments.
String commentUuid = newComment(patchset1Id).onSecondParentCommit().create();
- List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+ ImmutableList<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
assertThat(portedComments).comparingElementsUsing(hasUuid()).containsExactly(commentUuid);
}
@@ -411,7 +412,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
String commentUuid1 = newComment(patchset1Id).onFileLevelOf("not-existing file").create();
String commentUuid2 = newComment(patchset1Id).onLine(3).ofFile("myFile").create();
- List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+ ImmutableList<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
assertThat(portedComments)
.comparingElementsUsing(hasUuid())
@@ -441,7 +442,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
// Add comment.
String commentUuid = newComment(patchset1Id).create();
- List<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
+ ImmutableList<CommentInfo> portedComments = flatten(getPortedComments(patchset2Id));
assertThatList(portedComments).onlyElement().uuid().isEqualTo(commentUuid);
}
@@ -1859,7 +1860,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
.revision(patchsetId1.get())
.comment(commentUuid)
.delete(new DeleteCommentInput());
- List<CommentInfo> portedComments = flatten(getPortedComments(patchsetId2));
+ ImmutableList<CommentInfo> portedComments = flatten(getPortedComments(patchsetId2));
assertThatList(portedComments).isEmpty();
}
@@ -1918,7 +1919,7 @@ public class PortedCommentsIT extends AbstractDaemonTest {
}
private Map<String, List<CommentInfo>> getPortedDraftCommentsOfUser(
- PatchSet.Id patchsetId, Account.Id accountId) throws RestApiException {
+ PatchSet.Id patchsetId, Account.Id accountId) throws Exception {
// Draft comments are only visible to their author.
requestScopeOps.setApiUser(accountId);
return gApi.changes().id(patchsetId.changeId().get()).revision(patchsetId.get()).portedDrafts();
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 4a5c3dd200..e4d4610573 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -15,7 +15,6 @@
package com.google.gerrit.acceptance.api.revision;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
import static com.google.gerrit.acceptance.PushOneCommit.PATCH;
@@ -800,7 +799,7 @@ public class RevisionIT extends AbstractDaemonTest {
TestCommitValidationListener testCommitValidationListener = new TestCommitValidationListener();
try (Registration registration =
extensionRegistry.newRegistration().add(testCommitValidationListener)) {
- gApi.changes().id(r.getChangeId()).current().cherryPickAsInfo(in);
+ gApi.changes().id(r.getChangeId()).current().cherryPick(in);
assertThat(testCommitValidationListener.receiveEvent.pushOptions)
.containsExactly("key", "value");
}
@@ -1694,7 +1693,8 @@ public class RevisionIT extends AbstractDaemonTest {
@Test
public void queryRevisionFiles() throws Exception {
- Map<String, String> files = ImmutableMap.of("file1.txt", "content 1", "file2.txt", "content 2");
+ ImmutableMap<String, String> files =
+ ImmutableMap.of("file1.txt", "content 1", "file2.txt", "content 2");
PushOneCommit.Result result =
pushFactory.create(admin.newIdent(), testRepo, SUBJECT, files).to("refs/for/master");
result.assertOkStatus();
@@ -1814,7 +1814,7 @@ public class RevisionIT extends AbstractDaemonTest {
@Test
@GerritConfig(name = "change.maxFileSizeDownload", value = "10")
public void content_maxFileSizeDownload() throws Exception {
- Map<String, String> files =
+ ImmutableMap<String, String> files =
ImmutableMap.of("dir/file1.txt", " 9 bytes ", "dir/file2.txt", "11 bytes xx");
PushOneCommit.Result result =
pushFactory.create(admin.newIdent(), testRepo, SUBJECT, files).to("refs/for/master");
@@ -1854,7 +1854,7 @@ public class RevisionIT extends AbstractDaemonTest {
@Test
public void cannotGetContentOfDirectory() throws Exception {
- Map<String, String> files = ImmutableMap.of("dir/file1.txt", "content 1");
+ ImmutableMap<String, String> files = ImmutableMap.of("dir/file1.txt", "content 1");
PushOneCommit.Result result =
pushFactory.create(admin.newIdent(), testRepo, SUBJECT, files).to("refs/for/master");
result.assertOkStatus();
@@ -2236,7 +2236,7 @@ public class RevisionIT extends AbstractDaemonTest {
// check that reviewer is notified.
amendChange(r.getChangeId());
- List<FakeEmailSender.Message> messages = sender.getMessages();
+ ImmutableList<FakeEmailSender.Message> messages = sender.getMessages();
FakeEmailSender.Message m = Iterables.getOnlyElement(messages);
assertThat(m.rcpt()).containsExactly(user.getNameEmail());
assertThat(m.body()).contains("I'd like you to reexamine a change.");
@@ -2265,7 +2265,7 @@ public class RevisionIT extends AbstractDaemonTest {
// check that watcher is notified
amendChange(r.getChangeId());
- List<FakeEmailSender.Message> messages = sender.getMessages();
+ ImmutableList<FakeEmailSender.Message> messages = sender.getMessages();
FakeEmailSender.Message m = Iterables.getOnlyElement(messages);
assertThat(m.rcpt()).containsExactly(user.getNameEmail());
assertThat(m.body()).contains(admin.fullName() + " has uploaded a new patch set (#2).");
@@ -2583,7 +2583,7 @@ public class RevisionIT extends AbstractDaemonTest {
ChangeInfo info = gApi.changes().id(changeId).get(DETAILED_LABELS);
LabelInfo li = info.labels.get(label);
assertThat(li).isNotNull();
- int accountId = atrScope.get().getUser().getAccountId().get();
+ int accountId = localCtx.getContext().getUser().getAccountId().get();
return li.all.stream().filter(a -> a._accountId == accountId).findFirst().get();
}
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
index b31d35ca78..5322785d08 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
@@ -18,6 +18,9 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.acceptance.server.change.CommentsUtil.createFixReplacementInfo;
+import static com.google.gerrit.acceptance.server.change.CommentsUtil.createFixSuggestionInfo;
+import static com.google.gerrit.acceptance.server.change.CommentsUtil.createRange;
import static com.google.gerrit.entities.Patch.COMMIT_MSG;
import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
@@ -43,7 +46,6 @@ import com.google.gerrit.entities.Patch;
import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
-import com.google.gerrit.extensions.client.Comment;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
@@ -489,7 +491,7 @@ public class RobotCommentsIT extends AbstractDaemonTest {
.hasMessageThat()
.contains(
String.format(
- "A description is required for the suggested fix of the robot comment on %s",
+ "A description is required for the suggested fix of the comment on %s",
withFixRobotCommentInput.path));
}
@@ -518,7 +520,7 @@ public class RobotCommentsIT extends AbstractDaemonTest {
.contains(
String.format(
"At least one replacement is required"
- + " for the suggested fix of the robot comment on %s",
+ + " for the suggested fix of the comment on %s",
withFixRobotCommentInput.path));
}
@@ -548,7 +550,7 @@ public class RobotCommentsIT extends AbstractDaemonTest {
.hasMessageThat()
.contains(
String.format(
- "A file path must be given for the replacement of the robot comment on %s",
+ "A file path must be given for the replacement of the comment on %s",
withFixRobotCommentInput.path));
}
@@ -578,7 +580,7 @@ public class RobotCommentsIT extends AbstractDaemonTest {
.hasMessageThat()
.contains(
String.format(
- "A range must be given for the replacement of the robot comment on %s",
+ "A range must be given for the replacement of the comment on %s",
withFixRobotCommentInput.path));
}
@@ -732,7 +734,7 @@ public class RobotCommentsIT extends AbstractDaemonTest {
.contains(
String.format(
"A content for replacement must be "
- + "indicated for the replacement of the robot comment on %s",
+ + "indicated for the replacement of the comment on %s",
withFixRobotCommentInput.path));
}
@@ -1676,33 +1678,6 @@ public class RobotCommentsIT extends AbstractDaemonTest {
assertThat(diff).content().element(1).linesOfB().containsExactly("2nd line", "");
}
- private static FixSuggestionInfo createFixSuggestionInfo(
- FixReplacementInfo... fixReplacementInfos) {
- FixSuggestionInfo newFixSuggestionInfo = new FixSuggestionInfo();
- newFixSuggestionInfo.fixId = "An ID which must be overwritten.";
- newFixSuggestionInfo.description = "A description for a suggested fix.";
- newFixSuggestionInfo.replacements = Arrays.asList(fixReplacementInfos);
- return newFixSuggestionInfo;
- }
-
- private static FixReplacementInfo createFixReplacementInfo() {
- FixReplacementInfo newFixReplacementInfo = new FixReplacementInfo();
- newFixReplacementInfo.path = FILE_NAME;
- newFixReplacementInfo.replacement = "some replacement code";
- newFixReplacementInfo.range = createRange(3, 9, 8, 4);
- return newFixReplacementInfo;
- }
-
- private static Comment.Range createRange(
- int startLine, int startCharacter, int endLine, int endCharacter) {
- Comment.Range range = new Comment.Range();
- range.startLine = startLine;
- range.startCharacter = startCharacter;
- range.endLine = endLine;
- range.endCharacter = endCharacter;
- return range;
- }
-
private List<RobotCommentInfo> getRobotComments() throws RestApiException {
return gApi.changes().id(changeId).current().robotCommentsAsList();
}
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 9c691ae088..62a095fae0 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -50,6 +50,7 @@ import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.changes.ChangeEditIdentityType;
import com.google.gerrit.extensions.api.changes.FileContentInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
@@ -62,9 +63,12 @@ import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.DiffInfo;
import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.common.FileInfo;
+import com.google.gerrit.extensions.common.GitPerson;
+import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
@@ -72,6 +76,7 @@ import com.google.gerrit.extensions.restapi.RawInput;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.project.testing.TestLabels;
+import com.google.gerrit.server.restapi.change.ChangeEdits;
import com.google.gerrit.server.restapi.change.ChangeEdits.EditMessage;
import com.google.gerrit.server.restapi.change.ChangeEdits.Post;
import com.google.gson.reflect.TypeToken;
@@ -137,10 +142,11 @@ public class ChangeEditIT extends AbstractDaemonTest {
createArbitraryEditFor(changeId);
// check that '0' is parsed as edit revision
- gApi.changes().id(changeId).revision(0).comments();
+ @SuppressWarnings("unused")
+ var unused = gApi.changes().id(changeId).revision(0).comments();
// check that 'edit' is parsed as edit revision
- gApi.changes().id(changeId).revision("edit").comments();
+ unused = gApi.changes().id(changeId).revision("edit").comments();
}
@Test
@@ -485,6 +491,202 @@ public class ChangeEditIT extends AbstractDaemonTest {
}
@Test
+ public void updateCommitter() throws Exception {
+ createEmptyEditFor(changeId);
+ gApi.changes()
+ .id(changeId)
+ .edit()
+ .modifyIdentity("Test", "test@example.com", ChangeEditIdentityType.COMMITTER);
+
+ PublishChangeEditInput publishInput = new PublishChangeEditInput();
+ publishInput.notify = NotifyHandling.NONE;
+ gApi.changes().id(changeId).edit().publish(publishInput);
+ assertThat(getEdit(changeId)).isAbsent();
+
+ ChangeInfo info =
+ get(changeId, ListChangesOption.CURRENT_COMMIT, ListChangesOption.CURRENT_REVISION);
+ GitPerson committer = info.revisions.get(info.currentRevision).commit.committer;
+ assertThat(committer.name).isEqualTo("Test");
+ assertThat(committer.email).isEqualTo("test@example.com");
+ }
+
+ @Test
+ public void updateCommitterRest() throws Exception {
+ adminRestSession.get(urlEditIdentity(changeId)).assertNotFound();
+ ChangeEdits.EditIdentity.Input in = new ChangeEdits.EditIdentity.Input();
+ in.name = "Test";
+ in.email = "test@example.com";
+ in.type = ChangeEditIdentityType.COMMITTER;
+ adminRestSession.put(urlEditIdentity(changeId), in).assertNoContent();
+ adminRestSession.post(urlPublish(changeId)).assertNoContent();
+ ChangeInfo info =
+ get(changeId, ListChangesOption.CURRENT_COMMIT, ListChangesOption.CURRENT_REVISION);
+ GitPerson committer = info.revisions.get(info.currentRevision).commit.committer;
+ assertThat(committer.name).isEqualTo("Test");
+ assertThat(committer.email).isEqualTo("test@example.com");
+ }
+
+ @Test
+ public void updateCommitterRestWithDefaultName() throws Exception {
+ adminRestSession.get(urlEditIdentity(changeId)).assertNotFound();
+ ChangeEdits.EditIdentity.Input in = new ChangeEdits.EditIdentity.Input();
+ in.type = ChangeEditIdentityType.COMMITTER;
+ in.email = "test@example.com";
+ adminRestSession.put(urlEditIdentity(changeId), in).assertNoContent();
+ adminRestSession.post(urlPublish(changeId)).assertNoContent();
+ ChangeInfo info =
+ get(changeId, ListChangesOption.CURRENT_COMMIT, ListChangesOption.CURRENT_REVISION);
+ GitPerson committer = info.revisions.get(info.currentRevision).commit.committer;
+ assertThat(committer.name).isEqualTo("Administrator");
+ assertThat(committer.email).isEqualTo("test@example.com");
+ }
+
+ @Test
+ public void updateCommitterRestWithDefaultEmail() throws Exception {
+ adminRestSession.get(urlEditIdentity(changeId)).assertNotFound();
+ ChangeEdits.EditIdentity.Input in = new ChangeEdits.EditIdentity.Input();
+ in.type = ChangeEditIdentityType.COMMITTER;
+ in.name = "John Doe";
+ adminRestSession.put(urlEditIdentity(changeId), in).assertNoContent();
+ adminRestSession.post(urlPublish(changeId)).assertNoContent();
+ ChangeInfo info =
+ get(changeId, ListChangesOption.CURRENT_COMMIT, ListChangesOption.CURRENT_REVISION);
+ GitPerson committer = info.revisions.get(info.currentRevision).commit.committer;
+ assertThat(committer.name).isEqualTo("John Doe");
+ assertThat(committer.email).isEqualTo("admin@example.com");
+ }
+
+ @Test
+ public void cannotForgeCommitterWithoutPerm() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(block(Permission.FORGE_COMMITTER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ createEmptyEditFor(changeId);
+ assertThrows(
+ ResourceConflictException.class,
+ () ->
+ gApi.changes()
+ .id(changeId)
+ .edit()
+ .modifyIdentity("Test", "test@example.com", ChangeEditIdentityType.COMMITTER));
+ }
+
+ @Test
+ public void updateAuthor() throws Exception {
+ createEmptyEditFor(changeId);
+ gApi.changes()
+ .id(changeId)
+ .edit()
+ .modifyIdentity("Test", "test@example.com", ChangeEditIdentityType.AUTHOR);
+
+ PublishChangeEditInput publishInput = new PublishChangeEditInput();
+ publishInput.notify = NotifyHandling.NONE;
+ gApi.changes().id(changeId).edit().publish(publishInput);
+ assertThat(getEdit(changeId)).isAbsent();
+
+ ChangeInfo info =
+ get(changeId, ListChangesOption.CURRENT_COMMIT, ListChangesOption.CURRENT_REVISION);
+ GitPerson author = info.revisions.get(info.currentRevision).commit.author;
+ assertThat(author.name).isEqualTo("Test");
+ assertThat(author.email).isEqualTo("test@example.com");
+ }
+
+ @Test
+ public void updateAuthorRest() throws Exception {
+ adminRestSession.get(urlEditIdentity(changeId)).assertNotFound();
+ ChangeEdits.EditIdentity.Input in = new ChangeEdits.EditIdentity.Input();
+ in.name = "Test";
+ in.type = ChangeEditIdentityType.AUTHOR;
+ in.email = "test@example.com";
+ adminRestSession.put(urlEditIdentity(changeId), in).assertNoContent();
+ adminRestSession.post(urlPublish(changeId)).assertNoContent();
+ ChangeInfo info =
+ get(changeId, ListChangesOption.CURRENT_COMMIT, ListChangesOption.CURRENT_REVISION);
+ GitPerson author = info.revisions.get(info.currentRevision).commit.author;
+ assertThat(author.name).isEqualTo("Test");
+ assertThat(author.email).isEqualTo("test@example.com");
+ }
+
+ @Test
+ public void updateAuthorRestWithDefaultName() throws Exception {
+ adminRestSession.get(urlEditIdentity(changeId)).assertNotFound();
+ ChangeEdits.EditIdentity.Input in = new ChangeEdits.EditIdentity.Input();
+ in.type = ChangeEditIdentityType.AUTHOR;
+ in.email = "test@example.com";
+ adminRestSession.put(urlEditIdentity(changeId), in).assertNoContent();
+ adminRestSession.post(urlPublish(changeId)).assertNoContent();
+ ChangeInfo info =
+ get(changeId, ListChangesOption.CURRENT_COMMIT, ListChangesOption.CURRENT_REVISION);
+ GitPerson author = info.revisions.get(info.currentRevision).commit.author;
+ assertThat(author.name).isEqualTo("Administrator");
+ assertThat(author.email).isEqualTo("test@example.com");
+ }
+
+ @Test
+ public void updateAuthorRestWithDefaultEmail() throws Exception {
+ adminRestSession.get(urlEditIdentity(changeId)).assertNotFound();
+ ChangeEdits.EditIdentity.Input in = new ChangeEdits.EditIdentity.Input();
+ in.type = ChangeEditIdentityType.AUTHOR;
+ in.name = "John Doe";
+ adminRestSession.put(urlEditIdentity(changeId), in).assertNoContent();
+ adminRestSession.post(urlPublish(changeId)).assertNoContent();
+ ChangeInfo info =
+ get(changeId, ListChangesOption.CURRENT_COMMIT, ListChangesOption.CURRENT_REVISION);
+ GitPerson author = info.revisions.get(info.currentRevision).commit.author;
+ assertThat(author.name).isEqualTo("John Doe");
+ assertThat(author.email).isEqualTo("admin@example.com");
+ }
+
+ @Test
+ public void cannotForgeAuthorWithoutPerm() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(block(Permission.FORGE_AUTHOR).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ createEmptyEditFor(changeId);
+ assertThrows(
+ ResourceConflictException.class,
+ () ->
+ gApi.changes()
+ .id(changeId)
+ .edit()
+ .modifyIdentity("Test", "test@example.com", ChangeEditIdentityType.AUTHOR));
+ }
+
+ @Test
+ public void updateAuthorAsOtherUser() throws Exception {
+ Account.Id otherUser =
+ accountOperations
+ .newAccount()
+ .preferredEmail("otherUser@example.com")
+ .fullname("otherUser")
+ .create();
+ requestScopeOperations.setApiUser(otherUser);
+ createEmptyEditFor(changeId);
+ gApi.changes()
+ .id(changeId)
+ .edit()
+ .modifyIdentity("Test", "test@example.com", ChangeEditIdentityType.AUTHOR);
+
+ PublishChangeEditInput publishInput = new PublishChangeEditInput();
+ publishInput.notify = NotifyHandling.NONE;
+ gApi.changes().id(changeId).edit().publish(publishInput);
+ assertThat(getEdit(changeId)).isAbsent();
+
+ ChangeInfo info =
+ get(changeId, ListChangesOption.CURRENT_COMMIT, ListChangesOption.CURRENT_REVISION);
+ GitPerson author = info.revisions.get(info.currentRevision).commit.author;
+ GitPerson committer = info.revisions.get(info.currentRevision).commit.committer;
+ assertThat(author.name).isEqualTo("Test");
+ assertThat(author.email).isEqualTo("test@example.com");
+ assertThat(committer.name).isEqualTo("otherUser");
+ assertThat(committer.email).isEqualTo("otherUser@example.com");
+ }
+
+ @Test
public void updateMessage() throws Exception {
createEmptyEditFor(changeId);
String msg = String.format("New commit message\n\nChange-Id: %s\n", changeId);
@@ -552,6 +754,7 @@ public class ChangeEditIT extends AbstractDaemonTest {
assertThat(updatedCommitMessage).isEqualTo(in.message);
r = adminRestSession.getJsonAccept(urlEditMessage(changeId, true));
+ r.assertOK();
try (Repository repo = repoManager.openRepository(project);
RevWalk rw = new RevWalk(repo)) {
RevCommit commit = rw.parseCommit(ObjectId.fromString(ps.commitId().name()));
@@ -1068,10 +1271,12 @@ public class ChangeEditIT extends AbstractDaemonTest {
String editCommitId = edit.get().commit.commit;
RestResponse r = adminRestSession.getJsonAccept(urlRevisionFiles(changeId, editCommitId));
+ r.assertOK();
Map<String, FileInfo> files = readContentFromJson(r, new TypeToken<Map<String, FileInfo>>() {});
assertThat(files).containsKey(FILE_NAME);
r = adminRestSession.getJsonAccept(urlRevisionFiles(changeId));
+ r.assertOK();
files = readContentFromJson(r, new TypeToken<Map<String, FileInfo>>() {});
assertThat(files).containsKey(FILE_NAME);
}
@@ -1085,10 +1290,12 @@ public class ChangeEditIT extends AbstractDaemonTest {
String editCommitId = edit.get().commit.commit;
RestResponse r = adminRestSession.getJsonAccept(urlDiff(changeId, editCommitId, FILE_NAME));
+ r.assertOK();
DiffInfo diff = readContentFromJson(r, DiffInfo.class);
assertThat(diff.diffHeader.get(0)).contains(FILE_NAME);
r = adminRestSession.getJsonAccept(urlDiff(changeId, FILE_NAME));
+ r.assertOK();
diff = readContentFromJson(r, DiffInfo.class);
assertThat(diff.diffHeader.get(0)).contains(FILE_NAME);
}
@@ -1162,6 +1369,59 @@ public class ChangeEditIT extends AbstractDaemonTest {
assertThat(info1.currentRevision).isNotEqualTo(info2.currentRevision);
}
+ @Test
+ public void canCombineEdits() throws Exception {
+ createEmptyEditFor(changeId);
+
+ // update author
+ gApi.changes()
+ .id(changeId)
+ .edit()
+ .modifyIdentity("Test Author", "test.author@example.com", ChangeEditIdentityType.AUTHOR);
+
+ // update message
+ String msg = String.format("New commit message\n\nChange-Id: %s\n", changeId);
+ gApi.changes().id(changeId).edit().modifyCommitMessage(msg);
+
+ // add new file
+ String newFile = "foobar";
+ gApi.changes().id(changeId).edit().modifyFile(newFile, RawInputUtil.create(CONTENT_NEW));
+
+ // update committer
+ gApi.changes()
+ .id(changeId)
+ .edit()
+ .modifyIdentity(
+ "Test Committer", "test.committer@example.com", ChangeEditIdentityType.COMMITTER);
+
+ // delete file
+ gApi.changes().id(changeId).edit().deleteFile(FILE_NAME);
+
+ // rename file
+ gApi.changes().id(changeId).edit().renameFile(FILE_NAME2, FILE_NAME3);
+
+ // publish edit
+ PublishChangeEditInput publishInput = new PublishChangeEditInput();
+ publishInput.notify = NotifyHandling.NONE;
+ gApi.changes().id(changeId).edit().publish(publishInput);
+ assertThat(getEdit(changeId)).isAbsent();
+
+ ChangeInfo info =
+ get(
+ changeId,
+ ListChangesOption.CURRENT_COMMIT,
+ ListChangesOption.CURRENT_REVISION,
+ ListChangesOption.ALL_FILES);
+ RevisionInfo currentRevision = info.revisions.get(info.currentRevision);
+ CommitInfo currentCommit = currentRevision.commit;
+ assertThat(currentCommit.committer.name).isEqualTo("Test Committer");
+ assertThat(currentCommit.committer.email).isEqualTo("test.committer@example.com");
+ assertThat(currentCommit.author.name).isEqualTo("Test Author");
+ assertThat(currentCommit.author.email).isEqualTo("test.author@example.com");
+ assertThat(currentCommit.message).isEqualTo(msg);
+ assertThat(currentRevision.files.keySet()).containsExactly(newFile, FILE_NAME3);
+ }
+
private void createArbitraryEditFor(String changeId) throws Exception {
createEmptyEditFor(changeId);
arbitrarilyModifyEditOf(changeId);
@@ -1227,6 +1487,10 @@ public class ChangeEditIT extends AbstractDaemonTest {
return "/changes/" + changeId + "/edit:message" + (base ? "?base" : "");
}
+ private String urlEditIdentity(String changeId) {
+ return "/changes/" + changeId + "/edit:identity";
+ }
+
private String urlEditFile(String changeId, String fileName) {
return urlEditFile(changeId, fileName, false);
}
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractGitOverHttpServlet.java b/javatests/com/google/gerrit/acceptance/git/AbstractGitOverHttpServlet.java
index d3e70b2d06..e2f81e7539 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractGitOverHttpServlet.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractGitOverHttpServlet.java
@@ -104,7 +104,7 @@ public class AbstractGitOverHttpServlet extends AbstractPushForReview {
String remote = "anonymous";
Config cfg = testRepo.git().getRepository().getConfig();
- String uri = server.getUrl() + "/" + project.get();
+ String uri = server.getGitUrl() + "/" + project.get();
cfg.setString("remote", remote, "url", uri);
cfg.setString("remote", remote, "fetch", "+refs/heads/*:refs/remotes/origin/*");
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractImplicitMergeTest.java b/javatests/com/google/gerrit/acceptance/git/AbstractImplicitMergeTest.java
new file mode 100644
index 0000000000..80eec84cc1
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractImplicitMergeTest.java
@@ -0,0 +1,142 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.acceptance.git;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.SubmitType;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.eclipse.jgit.util.RawParseUtils;
+
+/**
+ * Base class for different tests for implicit merge.
+ *
+ * <p>Provides shared methods for tests changes and branches manipulations.
+ */
+public abstract class AbstractImplicitMergeTest extends AbstractDaemonTest {
+
+ /** Creates and pushes a simple approved changes without files and with specified parents. */
+ protected PushOneCommit.Result createApprovedChange(String targetBranch, RevCommit... parents)
+ throws Exception {
+ PushOneCommit.Result result = pushTo("refs/for/" + targetBranch, ImmutableMap.of(), parents);
+ gApi.changes().id(result.getChangeId()).current().review(ReviewInput.approve());
+ return result;
+ }
+
+ /** Creates and pushes simple approved changes without files and with specified parents. */
+ protected PushOneCommit.Result createApprovedChange(
+ String targetBranch, PushOneCommit.Result... parents) throws Exception {
+ return createApprovedChange(
+ targetBranch,
+ Arrays.stream(parents).map(PushOneCommit.Result::getCommit).toArray(RevCommit[]::new));
+ }
+
+ /** Creates and pushes a commit with specified files and parents. */
+ protected PushOneCommit.Result pushTo(
+ String ref, ImmutableMap<String, String> files, RevCommit... parents) throws Exception {
+ PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, "Some commit", files);
+ push.setParents(List.of(parents));
+ PushOneCommit.Result result = push.to(ref);
+ result.assertOkStatus();
+ return result;
+ }
+
+ /**
+ * Creates a change in the in-memory repository but doesn't push it to gerrit.
+ *
+ * <p>The method can be used to create chain of changes. The last change in the chain can be
+ * created using {@link #createApprovedChange} or {@link #pushTo} methods - these method will push
+ * the whole chain to gerrit as a single git push operations.
+ */
+ protected RevCommit createChangeWithoutPush(
+ String changeId, ImmutableMap<String, String> files, RevCommit... parents) throws Exception {
+ TestRepository<?>.CommitBuilder commitBuilder =
+ testRepo
+ .commit()
+ .message("Change " + changeId)
+ // The passed changeId starts with 'I', but insertChangeId expects id without 'I'.
+ .insertChangeId(changeId.substring(1));
+ for (RevCommit parent : parents) {
+ commitBuilder.parent(parent);
+ }
+ for (Map.Entry<String, String> entry : files.entrySet()) {
+ commitBuilder.add(entry.getKey(), entry.getValue());
+ }
+
+ return commitBuilder.create();
+ }
+
+ protected void setRejectImplicitMerges() throws Exception {
+ setRejectImplicitMerges(/*reject=*/ true);
+ }
+
+ protected void setRejectImplicitMerges(boolean reject) throws Exception {
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig()
+ .updateProject(
+ p ->
+ p.setBooleanConfig(
+ BooleanProjectConfig.REJECT_IMPLICIT_MERGES,
+ reject ? InheritableBoolean.TRUE : InheritableBoolean.FALSE));
+ u.save();
+ }
+ }
+
+ protected void setSubmitType(SubmitType submitType) throws Exception {
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().updateProject(p -> p.setSubmitType(submitType));
+ u.save();
+ }
+ }
+
+ protected ImmutableMap<String, String> getRemoteBranchRootPathContent(String refName)
+ throws Exception {
+ String revision = gApi.projects().name(project.get()).branch(refName).get().revision;
+ testRepo.git().fetch().setRemote("origin").call();
+ RevTree revTree =
+ testRepo.getRepository().parseCommit(testRepo.getRepository().resolve(revision)).getTree();
+ try (TreeWalk tw = new TreeWalk(testRepo.getRepository())) {
+ tw.setFilter(TreeFilter.ALL);
+ tw.setRecursive(false);
+ tw.reset(revTree);
+ ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
+ while (tw.next()) {
+ String path = tw.getPathString();
+ String content =
+ RawParseUtils.decode(
+ testRepo.getRepository().open(tw.getObjectId(0)).getCachedBytes(Integer.MAX_VALUE));
+ builder.put(path, content);
+ }
+ return builder.buildOrThrow();
+ }
+ }
+
+ protected PushOneCommit.Result push(String ref, String subject, String fileName, String content)
+ throws Exception {
+ PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content);
+ return push.to(ref);
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 2ab054bcf7..8861a9e33a 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -19,7 +19,6 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
import static com.google.gerrit.acceptance.GitUtil.assertPushRejected;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
@@ -221,6 +220,34 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
}
@Test
+ public void pushMergeForMaster() throws Exception {
+ RevCommit initialHead =
+ testRepo.getRevWalk().parseCommit(testRepo.getRepository().resolve("HEAD"));
+
+ // Create a stable branch.
+ BranchInput in = new BranchInput();
+ in.revision = projectOperations.project(project).getHead("master").name();
+ gApi.projects().name(project.get()).branch("stable").create(in);
+
+ // Create a change on the stable branch and submit it.
+ PushOneCommit.Result r = pushTo("refs/for/stable");
+ r.assertOkStatus();
+ gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(r.getChangeId()).current().submit();
+
+ testRepo.reset(initialHead);
+
+ // Merge stable back into master and push for review.
+ r =
+ pushFactory
+ .create(admin.newIdent(), testRepo)
+ .setParents(ImmutableList.of(initialHead, r.getCommit()))
+ .to("refs/for/master");
+ r.assertOkStatus();
+ r.assertChange(Change.Status.NEW, null);
+ }
+
+ @Test
@TestProjectInput(createEmptyCommit = false)
public void pushInitialCommitForMasterBranch() throws Exception {
RevCommit c = testRepo.commit().message("Initial commit").insertChangeId().create();
@@ -496,6 +523,40 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
}
@Test
+ public void autocloseByChangeIdViaMerge() throws Exception {
+ RevCommit initialHead =
+ testRepo.getRevWalk().parseCommit(testRepo.getRepository().resolve("HEAD"));
+
+ // Create a change
+ PushOneCommit.Result r = pushTo("refs/for/master");
+ r.assertOkStatus();
+
+ // Amend the commit locally
+ RevCommit c = testRepo.amend(r.getCommit()).create();
+ assertThat(c).isNotEqualTo(r.getCommit());
+
+ testRepo.reset(initialHead);
+
+ // Force push a merge commit that integrates the amended commit, closing it
+ pushFactory
+ .create(admin.newIdent(), testRepo)
+ .setParents(ImmutableList.of(initialHead, c))
+ .to("refs/heads/master")
+ .assertOkStatus();
+
+ // Attempt to push the amended commit to the same change
+ testRepo.reset(c);
+ String url = canonicalWebUrl.get() + "c/" + project.get() + "/+/" + r.getChange().getId();
+ r = amendChange(r.getChangeId(), "refs/for/master");
+ r.assertErrorStatus("change " + url + " closed");
+
+ // Check that the new commit was added as a patch set
+ ChangeInfo change = change(r).get();
+ assertThat(change.revisions).hasSize(2);
+ assertThat(change.currentRevision).isEqualTo(c.name());
+ }
+
+ @Test
public void pushForMasterWithTopic() throws Exception {
TopicValidator topicValidator = new TopicValidator();
try (Registration registration = extensionRegistry.newRegistration().add(topicValidator)) {
@@ -1472,7 +1533,7 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
public void pushForMasterWithHashtags() throws Exception {
// specify a single hashtag as option
String hashtag1 = "tag1";
- Set<String> expected = ImmutableSet.of(hashtag1);
+ ImmutableSet<String> expected = ImmutableSet.of(hashtag1);
PushOneCommit.Result r = pushTo("refs/for/master%hashtag=#" + hashtag1);
r.assertOkStatus();
r.assertChange(Change.Status.NEW, null);
@@ -1502,7 +1563,7 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
// specify multiple hashtags as options
String hashtag1 = "tag1";
String hashtag2 = "tag2";
- Set<String> expected = ImmutableSet.of(hashtag1, hashtag2);
+ ImmutableSet<String> expected = ImmutableSet.of(hashtag1, hashtag2);
PushOneCommit.Result r =
pushTo("refs/for/master%hashtag=#" + hashtag1 + ",hashtag=##" + hashtag2);
r.assertOkStatus();
@@ -2322,7 +2383,7 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
addDraft(r.getChangeId(), rev2, newDraft(FILE_NAME, 1, "comment_PS3."));
amendChange(r.getChangeId(), "refs/for/master%publish-comments");
- Collection<CommentInfo> comments = getPublishedComments(r.getChangeId());
+ List<CommentInfo> comments = getPublishedComments(r.getChangeId());
List<ChangeMessageInfo> allMessages = getMessages(r.getChangeId());
assertThat(allMessages.stream().map(m -> m.message).collect(toList()))
@@ -2385,7 +2446,7 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
sender.clear();
amendChange(r.getChangeId(), "refs/for/master%publish-comments");
- Collection<CommentInfo> comments = getPublishedComments(r.getChangeId());
+ List<CommentInfo> comments = getPublishedComments(r.getChangeId());
assertThat(comments.stream().map(c -> c.id)).containsExactly(c1.id, c2.id, c3.id);
assertThat(comments.stream().map(c -> c.message))
.containsExactly("comment1", "comment2", "comment3");
@@ -2450,7 +2511,7 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
r = amendChange(r.getChangeId(), "refs/for/master%publish-comments,m=The_message");
- Collection<CommentInfo> comments = getPublishedComments(r.getChangeId());
+ List<CommentInfo> comments = getPublishedComments(r.getChangeId());
assertThat(comments.stream().map(c -> c.message)).containsExactly("comment1");
assertThat(getLastMessage(r.getChangeId())).isEqualTo("Patch Set 2:\n" + "\n" + "(1 comment)");
}
@@ -2469,7 +2530,7 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
amendChanges(initialHead, commits, "refs/for/master%publish-comments");
- Collection<CommentInfo> cs1 = getPublishedComments(id1);
+ List<CommentInfo> cs1 = getPublishedComments(id1);
List<ChangeMessageInfo> messages1 = getMessages(id1);
assertThat(cs1.stream().map(c -> c.message)).containsExactly("comment1");
assertThat(cs1.stream().map(c -> c.id)).containsExactly(c1.id);
@@ -2478,7 +2539,7 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
.isEqualTo("Uploaded patch set 2: Commit message was updated.");
assertThat(messages1.get(2).message).isEqualTo("Patch Set 2:\n\n(1 comment)");
- Collection<CommentInfo> cs2 = getPublishedComments(id2);
+ List<CommentInfo> cs2 = getPublishedComments(id2);
List<ChangeMessageInfo> messages2 = getMessages(id2);
assertThat(cs2.stream().map(c -> c.message)).containsExactly("comment2");
assertThat(cs2.stream().map(c -> c.id)).containsExactly(c2.id);
@@ -2505,7 +2566,7 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
assertThat(getPublishedComments(id1)).isEmpty();
assertThat(gApi.changes().id(id1).drafts()).hasSize(1);
- Collection<CommentInfo> cs2 = getPublishedComments(id2);
+ List<CommentInfo> cs2 = getPublishedComments(id2);
assertThat(cs2.stream().map(c -> c.message)).containsExactly("comment2");
assertThat(cs2.stream().map(c -> c.id)).containsExactly(c2.id);
@@ -2776,7 +2837,7 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
assertThat(pr.getMessages()).contains("Invalid value in -o notedb=foobar");
assertPushRejected(pr, ref, "NoteDb update requires -o notedb=allow");
- List<String> opts = ImmutableList.of("notedb=allow");
+ ImmutableList<String> opts = ImmutableList.of("notedb=allow");
pr = pushOne(testRepo, c.name(), ref, false, false, opts);
assertPushRejected(pr, ref, "NoteDb update requires access database permission");
@@ -3136,7 +3197,7 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
return gApi.changes().id(changeId).revision(revId).createDraft(in).get();
}
- private Collection<CommentInfo> getPublishedComments(String changeId) throws Exception {
+ private List<CommentInfo> getPublishedComments(String changeId) throws Exception {
return gApi.changes().id(changeId).commentsRequest().get().values().stream()
.flatMap(Collection::stream)
.collect(toList());
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java b/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java
index 8295550e04..1b5b63761f 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java
@@ -20,6 +20,7 @@ import static com.google.gerrit.acceptance.GitUtil.pushHead;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static java.util.stream.Collectors.toList;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
@@ -45,7 +46,6 @@ import com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType;
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.gerrit.testing.RefUpdateContextCollector;
import com.google.inject.Inject;
-import java.util.List;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.InvalidRemoteException;
import org.eclipse.jgit.api.errors.TransportException;
@@ -228,7 +228,7 @@ public abstract class AbstractSubmitOnPush extends AbstractDaemonTest {
PushOneCommit.Result r = push("refs/for/master", PushOneCommit.SUBJECT, "two.txt", "Two");
startEventRecorder();
git().push().setRefSpecs(new RefSpec(r.getCommit().name() + ":refs/heads/master")).call();
- List<ChangeMergedEvent> changeMergedEvents =
+ ImmutableList<ChangeMergedEvent> changeMergedEvents =
eventRecorder.getChangeMergedEvents(project.get(), "refs/heads/master", 2);
assertThat(changeMergedEvents.get(0).newRev).isEqualTo(r.getPatchSet().commitId().name());
assertThat(changeMergedEvents.get(1).newRev).isEqualTo(r.getPatchSet().commitId().name());
diff --git a/javatests/com/google/gerrit/acceptance/git/DirectPushRefUpdateContextIT.java b/javatests/com/google/gerrit/acceptance/git/DirectPushRefUpdateContextIT.java
index e802604bda..c059ae4595 100644
--- a/javatests/com/google/gerrit/acceptance/git/DirectPushRefUpdateContextIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/DirectPushRefUpdateContextIT.java
@@ -15,7 +15,6 @@
package com.google.gerrit.acceptance.git;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.AbstractDaemonTest;
diff --git a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckOnReceiveIT.java
index e352e2d512..90ba7c15c6 100644
--- a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckOnReceiveIT.java
@@ -17,16 +17,14 @@ package com.google.gerrit.acceptance.git;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
-import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.entities.BooleanProjectConfig;
-import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.git.ObjectIds;
import java.util.Locale;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Test;
-public class ImplicitMergeCheckIT extends AbstractDaemonTest {
+/** Checks that gerrit rejects/accepts implicit merges when receives a git push. */
+public class ImplicitMergeCheckOnReceiveIT extends AbstractImplicitMergeTest {
@Test
public void implicitMergeViaFastForward() throws Exception {
@@ -84,21 +82,4 @@ public class ImplicitMergeCheckIT extends AbstractDaemonTest {
return "implicit merge of "
+ ObjectIds.abbreviateName(commit, testRepo.getRevWalk().getObjectReader());
}
-
- private void setRejectImplicitMerges() throws Exception {
- try (ProjectConfigUpdate u = updateProject(project)) {
- u.getConfig()
- .updateProject(
- p ->
- p.setBooleanConfig(
- BooleanProjectConfig.REJECT_IMPLICIT_MERGES, InheritableBoolean.TRUE));
- u.save();
- }
- }
-
- private PushOneCommit.Result push(String ref, String subject, String fileName, String content)
- throws Exception {
- PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content);
- return push.to(ref);
- }
}
diff --git a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitByCherryPickOrRebaseAlwaysIT.java b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitByCherryPickOrRebaseAlwaysIT.java
new file mode 100644
index 0000000000..592220ed17
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitByCherryPickOrRebaseAlwaysIT.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.acceptance.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.testing.ConfigSuite;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests how implicit merges are submitted by the cherry pick and rebase always strategies.
+ *
+ * <p>Verifies that implicit merges can be submitted and that they don't add content from the
+ * implicitly merged branch to the target branch.
+ */
+public class ImplicitMergeOnSubmitByCherryPickOrRebaseAlwaysIT extends AbstractImplicitMergeTest {
+ @ConfigSuite.Configs
+ public static ImmutableMap<String, Config> configs() {
+ // The @RunWith(Parameterized.class) can't be used, because AbstractDaemonClass already
+ // uses @RunWith(ConfigSuite.class). Emulate parameters using configs.
+ ImmutableMap.Builder<String, Config> builder = ImmutableMap.builder();
+ for (SubmitType submitType : SubmitType.values()) {
+ if (submitType != SubmitType.CHERRY_PICK && submitType != SubmitType.REBASE_ALWAYS) {
+ continue;
+ }
+ Config cfg = new Config();
+ cfg.setString("test", null, "submitType", submitType.name());
+ builder.put(String.format("submitType=%s", submitType), cfg);
+ }
+ return builder.buildOrThrow();
+ }
+
+ private String implicitMergeChangeId;
+
+ @Before
+ public void setUp() throws Exception {
+ // The ConfigSuite runner always adds a default config. Ignore it (submitType is not set for
+ // it).
+ String submitType = cfg.getString("test", null, "submitType");
+ assume().that(submitType).isNotEmpty();
+ setSubmitType(SubmitType.valueOf(submitType));
+
+ setRejectImplicitMerges(false);
+ RevCommit base = repo().parseCommit(repo().exactRef("HEAD").getObjectId());
+ RevCommit masterBranchTip =
+ pushTo("refs/heads/master", ImmutableMap.of("master-content", "master-first-line\n"), base)
+ .getCommit();
+ pushTo("refs/heads/stable", ImmutableMap.of("stable-content", "stable-first-line\n"), base);
+ implicitMergeChangeId =
+ pushTo(
+ "refs/for/stable",
+ ImmutableMap.of("master-content2", "added-by-implicit-merge\n"),
+ masterBranchTip)
+ .getChangeId();
+ gApi.changes().id(implicitMergeChangeId).current().review(ReviewInput.approve());
+ }
+
+ @Test
+ public void doesntAddContentFromParentForImplicitMergeChange() throws Exception {
+ gApi.changes().id(implicitMergeChangeId).current().submit();
+
+ ChangeInfo ci = gApi.changes().id(implicitMergeChangeId).info();
+ assertThat(ci.submitted).isNotNull();
+ assertThat(ci.submitter).isNotNull();
+ assertThat(ci.submitter._accountId)
+ .isEqualTo(localCtx.getContext().getUser().getAccountId().get());
+
+ assertThat(getRemoteBranchRootPathContent("refs/heads/stable"))
+ .containsExactly(
+ "master-content2", "added-by-implicit-merge\n",
+ "stable-content", "stable-first-line\n");
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitExperimentsIT.java b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitExperimentsIT.java
new file mode 100644
index 0000000000..d85e613770
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitExperimentsIT.java
@@ -0,0 +1,335 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.acceptance.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.server.util.CommitMessageUtil.generateChangeId;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.testing.ConfigSuite;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Verifies that gerrit correctly rejects or submits implicit merges depending on experiments.
+ *
+ * <p>All tests use the same commit configuration (master branch is one commit ahead of stable
+ * branch):
+ *
+ * <pre>{@code
+ * change[1] (target - stable, explicit merge of stable branch and master branches)
+ * | \
+ * | change[0] (target - stable, i.e. implicit merge of master and stable branches)
+ * | |
+ * | master
+ * | |
+ * stable <--- |
+ * }</pre>
+ */
+public class ImplicitMergeOnSubmitExperimentsIT extends AbstractImplicitMergeTest {
+ @Override
+ protected boolean enableExperimentsRejectImplicitMergesOnMerge() {
+ // Tests uses own experiment setup.
+ return false;
+ }
+
+ @ConfigSuite.Configs
+ public static ImmutableMap<String, Config> configs() {
+ // The @RunWith(Parameterized.class) can't be used, because AbstractDaemonClass already
+ // uses @RunWith(ConfigSuite.class). Emulate parameters using configs.
+ ImmutableMap.Builder<String, Config> builder = ImmutableMap.builder();
+ for (SubmitType submitType : SubmitType.values()) {
+ if (submitType == SubmitType.INHERIT
+ || submitType == SubmitType.CHERRY_PICK
+ || submitType == SubmitType.REBASE_ALWAYS) {
+ continue;
+ }
+ Config cfg = new Config();
+ cfg.setString("test", null, "submitType", submitType.name());
+ builder.put(String.format("submitType=%s", submitType), cfg);
+ }
+ return builder.buildOrThrow();
+ }
+
+ private String implicitMergeChangeId;
+ private String explicitMergeChangeId;
+
+ private SubmitType submitType;
+
+ @Before
+ public void setUp() throws Exception {
+ // The ConfigSuite runner always adds a default config. Ignore it (submitType is not set for
+ // it).
+ assume().that(cfg.getString("test", null, "submitType")).isNotEmpty();
+ RevCommit base = repo().parseCommit(repo().exactRef("HEAD").getObjectId());
+ RevCommit stableBranchTip =
+ pushTo("refs/heads/stable", ImmutableMap.of("stable-content", "stable-first-line\n"), base)
+ .getCommit();
+ RevCommit masterBranchTip =
+ pushTo(
+ "refs/heads/master",
+ ImmutableMap.of("master-content", "master-first-line\n"),
+ stableBranchTip)
+ .getCommit();
+ implicitMergeChangeId = "I" + generateChangeId().name();
+ RevCommit implicitMergeChange =
+ createChangeWithoutPush(
+ implicitMergeChangeId,
+ ImmutableMap.of("master-content2", "added-by-implicit-merge\n"),
+ masterBranchTip);
+ explicitMergeChangeId =
+ pushTo(
+ "refs/for/stable",
+ ImmutableMap.of("stable-content", "stable-first-line\nadded-by-explicit-merge\n"),
+ implicitMergeChange,
+ stableBranchTip)
+ .getChangeId();
+ gApi.changes().id(implicitMergeChangeId).current().review(ReviewInput.approve());
+ gApi.changes().id(explicitMergeChangeId).current().review(ReviewInput.approve());
+ submitType = SubmitType.valueOf(cfg.getString("test", null, "submitType"));
+ setSubmitType(submitType);
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ values = {
+ "GerritBackendFeature__check_implicit_merges_on_merge",
+ "GerritBackendFeature__reject_implicit_merges_on_merge",
+ "GerritBackendFeature__always_reject_implicit_merges_on_merge"
+ })
+ public void alwaysRejectOnMerge_rejectImplicitMergeFalse_rejectImplicitMergeOnSubmit()
+ throws Exception {
+ setRejectImplicitMerges(/*reject=*/ false);
+ assertThatImplicitMergeSubmitRejected();
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ values = {
+ "GerritBackendFeature__check_implicit_merges_on_merge",
+ "GerritBackendFeature__reject_implicit_merges_on_merge",
+ "GerritBackendFeature__always_reject_implicit_merges_on_merge"
+ })
+ public void alwaysRejectOnMerge_rejectImplicitMergeFalse_canSubmitExplicitMerge()
+ throws Exception {
+ setRejectImplicitMerges(/*reject=*/ false);
+ assertThatExcplicitMergeSubmitAllowed();
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ values = {
+ "GerritBackendFeature__check_implicit_merges_on_merge",
+ "GerritBackendFeature__reject_implicit_merges_on_merge",
+ "GerritBackendFeature__always_reject_implicit_merges_on_merge"
+ })
+ public void alwaysRejectOnMerge_rejectImplicitMergeTrue_rejectImplicitMergeOnSubmit()
+ throws Exception {
+ setRejectImplicitMerges(/*reject=*/ true);
+ assertThatImplicitMergeSubmitRejected();
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ values = {
+ "GerritBackendFeature__check_implicit_merges_on_merge",
+ "GerritBackendFeature__reject_implicit_merges_on_merge",
+ "GerritBackendFeature__always_reject_implicit_merges_on_merge"
+ })
+ public void alwaysRejectOnMerge_rejectImplicitMergeTrue_canSubmitExplicitMerge()
+ throws Exception {
+ setRejectImplicitMerges(/*reject=*/ true);
+ assertThatExcplicitMergeSubmitAllowed();
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ values = {
+ "GerritBackendFeature__check_implicit_merges_on_merge",
+ "GerritBackendFeature__reject_implicit_merges_on_merge",
+ })
+ public void rejectOnMerge_rejectImplicitMergeFalse_canSubmitImplicitMerge() throws Exception {
+ setRejectImplicitMerges(/*reject=*/ false);
+ assertThatImplicitMergeSubmitAllowed();
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ values = {
+ "GerritBackendFeature__check_implicit_merges_on_merge",
+ "GerritBackendFeature__reject_implicit_merges_on_merge",
+ })
+ public void rejectOnMerge_rejectImplicitMergeFalse_canSubmitExplicitMerge() throws Exception {
+ setRejectImplicitMerges(/*reject=*/ false);
+ assertThatExcplicitMergeSubmitAllowed();
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ values = {
+ "GerritBackendFeature__check_implicit_merges_on_merge",
+ "GerritBackendFeature__reject_implicit_merges_on_merge",
+ })
+ public void rejectOnMerge_rejectImplicitMergeTrue_rejectImplicitMergeOnSubmit() throws Exception {
+ setRejectImplicitMerges(/*reject=*/ true);
+ assertThatImplicitMergeSubmitRejected();
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ values = {
+ "GerritBackendFeature__check_implicit_merges_on_merge",
+ "GerritBackendFeature__reject_implicit_merges_on_merge",
+ })
+ public void rejectOnMerge_rejectImplicitMergeTrue_canSubmitExplicitMerge() throws Exception {
+ setRejectImplicitMerges(/*reject=*/ true);
+ assertThatExcplicitMergeSubmitAllowed();
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ values = {
+ "GerritBackendFeature__check_implicit_merges_on_merge",
+ })
+ public void checkOnly_rejectImplicitMergeFalse_canSubmitImplicitMerge() throws Exception {
+ setRejectImplicitMerges(/*reject=*/ false);
+ assertThatImplicitMergeSubmitAllowed();
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ values = {
+ "GerritBackendFeature__check_implicit_merges_on_merge",
+ })
+ public void checkOnly_rejectImplicitMergeFalse_canSubmitExplicitMerge() throws Exception {
+ setRejectImplicitMerges(/*reject=*/ false);
+ assertThatExcplicitMergeSubmitAllowed();
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ values = {
+ "GerritBackendFeature__check_implicit_merges_on_merge",
+ })
+ public void checkOnly_rejectImplicitMergeTrue_canSubmitImplicitMerge() throws Exception {
+ setRejectImplicitMerges(/*reject=*/ true);
+ assertThatImplicitMergeSubmitAllowed();
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ values = {
+ "GerritBackendFeature__check_implicit_merges_on_merge",
+ })
+ public void checkOnly_rejectImplicitMergeTrue_canSubmitExplicitMerge() throws Exception {
+ setRejectImplicitMerges(/*reject=*/ true);
+ assertThatExcplicitMergeSubmitAllowed();
+ }
+
+ @Test
+ public void noExperiments_rejectImplicitMergeFalse_canSubmitImplicitMerge() throws Exception {
+ setRejectImplicitMerges(/*reject=*/ false);
+ assertThatImplicitMergeSubmitAllowed();
+ }
+
+ @Test
+ public void noExperiments_rejectImplicitMergeFalse_canSubmitExplicitMerge() throws Exception {
+ setRejectImplicitMerges(/*reject=*/ false);
+ assertThatExcplicitMergeSubmitAllowed();
+ }
+
+ @Test
+ public void noExperiments_rejectImplicitMergeTrue_canSubmitImplicitMerge() throws Exception {
+ setRejectImplicitMerges(/*reject=*/ true);
+ assertThatImplicitMergeSubmitAllowed();
+ }
+
+ @Test
+ public void noExperiments_rejectImplicitMergeTrue_canSubmitExplicitMerge() throws Exception {
+ setRejectImplicitMerges(/*reject=*/ true);
+ assertThatExcplicitMergeSubmitAllowed();
+ }
+
+ private void assertThatImplicitMergeSubmitRejected() throws Exception {
+ ResourceConflictException e =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.changes().id(implicitMergeChangeId).current().submit());
+ assertThat(e.getMessage().toLowerCase()).contains("submit makes implicit merge to the branch");
+ ChangeInfo ci = gApi.changes().id(implicitMergeChangeId).info();
+ assertThat(ci.submitted).isNull();
+ assertThat(getRemoteBranchRootPathContent("refs/heads/stable"))
+ .containsExactly("stable-content", "stable-first-line\n");
+ }
+
+ private void assertThatImplicitMergeSubmitAllowed() throws Exception {
+ gApi.changes().id(implicitMergeChangeId).current().submit();
+
+ ChangeInfo ci = gApi.changes().id(implicitMergeChangeId).info();
+ assertThat(ci.submitted).isNotNull();
+ assertThat(ci.submitter).isNotNull();
+ assertThat(ci.submitter._accountId)
+ .isEqualTo(localCtx.getContext().getUser().getAccountId().get());
+
+ if (submitType != SubmitType.REBASE_ALWAYS) {
+ assertThat(getRemoteBranchRootPathContent("refs/heads/stable"))
+ .containsExactly(
+ "master-content", "master-first-line\n",
+ "master-content2", "added-by-implicit-merge\n",
+ "stable-content", "stable-first-line\n");
+ } else {
+ assertThat(getRemoteBranchRootPathContent("refs/heads/stable"))
+ .containsExactly(
+ "master-content2", "added-by-implicit-merge\n",
+ "stable-content", "stable-first-line\n");
+ }
+ }
+
+ private void assertThatExcplicitMergeSubmitAllowed() throws Exception {
+ gApi.changes().id(explicitMergeChangeId).current().submit();
+
+ ChangeInfo ci = gApi.changes().id(explicitMergeChangeId).info();
+ assertThat(ci.submitted).isNotNull();
+ assertThat(ci.submitter).isNotNull();
+ assertThat(ci.submitter._accountId)
+ .isEqualTo(localCtx.getContext().getUser().getAccountId().get());
+ assertThat(getRemoteBranchRootPathContent("refs/heads/stable"))
+ .containsExactly(
+ "master-content", "master-first-line\n",
+ "master-content2", "added-by-implicit-merge\n",
+ "stable-content", "stable-first-line\nadded-by-explicit-merge\n");
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitIT.java b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitIT.java
new file mode 100644
index 0000000000..ba881b6069
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitIT.java
@@ -0,0 +1,322 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.acceptance.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Verifies that gerrit correctly detects implicit merges on submit..
+ *
+ * <p>The setup creates a repository with 2 branches: master and target. Both branches have common
+ * parent:
+ *
+ * <pre>{@code
+ * master target
+ * | |
+ * ----->base commit <------
+ * }</pre>
+ *
+ * Tests use only MergeAlways strategy. All other submit strategies (except cherry pick and rebase
+ * always) use the same checks on submit. The {@link ImplicitMergeOnSubmitExperimentsIT} validates
+ * that the implicit merge check is applied to all strategies (except cherry pick and rebase always)
+ * and {@link ImplicitMergeOnSubmitByCherryPickOrRebaseAlwaysIT} contains tests for the cherry pick
+ * and rebase always strategies.
+ */
+public class ImplicitMergeOnSubmitIT extends AbstractImplicitMergeTest {
+ private RevCommit masterTip;
+ private RevCommit otherTip;
+ RevCommit baseCommit;
+
+ @Before
+ public void setUp() throws Exception {
+ setSubmitType(SubmitType.MERGE_ALWAYS);
+ gApi.projects().name(project.get()).branch("other").create(new BranchInput());
+ baseCommit =
+ repo()
+ .parseCommit(
+ ObjectId.fromString(
+ gApi.projects().name(project.get()).branch("master").get().revision));
+ masterTip =
+ pushTo("refs/heads/master", ImmutableMap.of("master-file", "master-content"), baseCommit)
+ .getCommit();
+ otherTip =
+ pushTo("refs/heads/other", ImmutableMap.of("target-file", "target1-content"), baseCommit)
+ .getCommit();
+ }
+
+ @Test
+ public void singleChangeImplicitMerge() throws Exception {
+ PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip);
+ assertSubmitRejectedWithImplicitMerge(implicitMerge.getChangeId());
+ }
+
+ @Test
+ public void chainOfChangesImplicitMerge() throws Exception {
+ PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip);
+ PushOneCommit.Result c1 = createApprovedChange("master", implicitMerge);
+ PushOneCommit.Result c2 = createApprovedChange("master", c1);
+ assertSubmitRejectedWithImplicitMerge(implicitMerge.getChangeId());
+ assertSubmitRejectedWithImplicitMerge(c1.getChangeId());
+ assertSubmitRejectedWithImplicitMerge(c2.getChangeId());
+ }
+
+ @Test
+ public void chainOfChangesOnTopOfTargetBranchTipNoImplicitMerge() throws Exception {
+ PushOneCommit.Result c1 = createApprovedChange("master", masterTip);
+ PushOneCommit.Result c2 = createApprovedChange("master", c1);
+ PushOneCommit.Result c3 = createApprovedChange("master", c2);
+ assertThatChangeSubmittable(c1.getChangeId());
+ assertThatChangeSubmittable(c2.getChangeId());
+ assertThatChangeSubmittable(c3.getChangeId());
+ }
+
+ @Test
+ public void chainOfChangesNotOnTopOfTargetBranchTipNoImplicitMerge() throws Exception {
+ // Add one more commit to master branch.
+ pushTo("refs/heads/master", ImmutableMap.of(), masterTip);
+ PushOneCommit.Result c1 = createApprovedChange("master", masterTip);
+ PushOneCommit.Result c2 = createApprovedChange("master", c1);
+ PushOneCommit.Result c3 = createApprovedChange("master", c2);
+ assertThatChangeSubmittable(c1.getChangeId());
+ assertThatChangeSubmittable(c2.getChangeId());
+ assertThatChangeSubmittable(c3.getChangeId());
+ }
+
+ @Test
+ public void chainOfChangesNotOnTopOfTargetBranchTipWithImplicitMerge() throws Exception {
+ // Add one more commit to master branch.
+ pushTo("refs/heads/master", ImmutableMap.of(), masterTip);
+ PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip);
+ PushOneCommit.Result c2 = createApprovedChange("master", implicitMerge);
+ PushOneCommit.Result c3 = createApprovedChange("master", c2);
+ assertSubmitRejectedWithImplicitMerge(implicitMerge.getChangeId());
+ assertSubmitRejectedWithImplicitMerge(c2.getChangeId());
+ assertSubmitRejectedWithImplicitMerge(c3.getChangeId());
+ }
+
+ @Test
+ public void chainOfChangesEndsWithExplicitMerge_onlyExplcitMergeCanBeSubmitted()
+ throws Exception {
+ PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip);
+ PushOneCommit.Result changeInChange = createApprovedChange("master", implicitMerge);
+ PushOneCommit.Result explicitMerge =
+ createApprovedChange("master", changeInChange.getCommit(), masterTip);
+ assertSubmitRejectedWithImplicitMerge(implicitMerge.getChangeId());
+ assertSubmitRejectedWithImplicitMerge(changeInChange.getChangeId());
+ assertThatChangeSubmittable(explicitMerge.getChangeId());
+ }
+
+ @Test
+ public void twoChainOfChangesSameTopic_oneChainImplicitMerge_rejectedOnSubmit() throws Exception {
+ cfg.setBoolean("change", null, "submitWholeTopic", true);
+ PushOneCommit.Result c1 = createApprovedChange("master", masterTip);
+ PushOneCommit.Result c2 = createApprovedChange("master", c1);
+ PushOneCommit.Result c3 = createApprovedChange("master", c2.getCommit());
+ PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip);
+ PushOneCommit.Result im1 = createApprovedChange("master", implicitMerge);
+ PushOneCommit.Result im2 = createApprovedChange("master", im1.getCommit());
+ // The AbstractDaemonTest doesn't fully reset gerrit; it creates a new project for each test
+ // and doesn't remove changes created in tests. As a result, if the same topic is used in
+ // several tests gerrit tries to submit all changes, including changes from other tests.
+ // The name method returns name scoped to this test method .
+ String topic = name("topic");
+ gApi.changes().id(c1.getChangeId()).topic(topic);
+ gApi.changes().id(c2.getChangeId()).topic(topic);
+ gApi.changes().id(c3.getChangeId()).topic(topic);
+ gApi.changes().id(implicitMerge.getChangeId()).topic(topic);
+ gApi.changes().id(im1.getChangeId()).topic(topic);
+ gApi.changes().id(im2.getChangeId()).topic(topic);
+
+ assertSubmitRejectedWithImplicitMerge(c1.getChangeId());
+ }
+
+ @Test
+ public void twoChainOfChangesSameTopic_noImplicitMerge_canSubmit() throws Exception {
+ cfg.setBoolean("change", null, "submitWholeTopic", true);
+ PushOneCommit.Result chain1change1 = createApprovedChange("master", masterTip);
+ PushOneCommit.Result chain1change2 = createApprovedChange("master", chain1change1);
+ PushOneCommit.Result chain1change3 = createApprovedChange("master", chain1change2);
+ PushOneCommit.Result chain2change1 = createApprovedChange("master", masterTip);
+ PushOneCommit.Result chain2change2 = createApprovedChange("master", chain2change1);
+ PushOneCommit.Result chain2change3 = createApprovedChange("master", chain2change2);
+ // The AbstractDaemonTest doesn't fully reset gerrit; it creates a new project for each test
+ // and doesn't remove changes created in tests. As a result, if the same topic is used in
+ // several tests gerrit tries to submit all changes, including changes from other tests.
+ // The name method returns name scoped to this test method .
+ String topic = name("topic");
+ gApi.changes().id(chain1change1.getChangeId()).topic(topic);
+ gApi.changes().id(chain1change2.getChangeId()).topic(topic);
+ gApi.changes().id(chain1change3.getChangeId()).topic(topic);
+ gApi.changes().id(chain2change1.getChangeId()).topic(topic);
+ gApi.changes().id(chain2change2.getChangeId()).topic(topic);
+ gApi.changes().id(chain2change3.getChangeId()).topic(topic);
+
+ assertThatChangeSubmittable(chain1change1.getChangeId());
+ }
+
+ @Test
+ public void twoChainOfChangesSameTopicNotOnTopOfBranch_noImplicitMerge_canSubmit()
+ throws Exception {
+ // Add one more commit to master branch.
+ pushTo("refs/heads/master", ImmutableMap.of(), masterTip);
+ cfg.setBoolean("change", null, "submitWholeTopic", true);
+ PushOneCommit.Result chain1change1 = createApprovedChange("master", masterTip);
+ PushOneCommit.Result chain1change2 = createApprovedChange("master", chain1change1);
+ PushOneCommit.Result chain1change3 = createApprovedChange("master", chain1change2);
+ PushOneCommit.Result chain2change1 = createApprovedChange("master", masterTip);
+ PushOneCommit.Result chain2change2 = createApprovedChange("master", chain2change1);
+ PushOneCommit.Result chain2change3 = createApprovedChange("master", chain2change2);
+ // The AbstractDaemonTest doesn't fully reset gerrit; it creates a new project for each test
+ // and doesn't remove changes created in tests. As a result, if the same topic is used in
+ // several tests gerrit tries to submit all changes, including changes from other tests.
+ // The name method returns name scoped to this test method .
+ String topic = name("topic");
+ gApi.changes().id(chain1change1.getChangeId()).topic(topic);
+ gApi.changes().id(chain1change2.getChangeId()).topic(topic);
+ gApi.changes().id(chain1change3.getChangeId()).topic(topic);
+ gApi.changes().id(chain2change1.getChangeId()).topic(topic);
+ gApi.changes().id(chain2change2.getChangeId()).topic(topic);
+ gApi.changes().id(chain2change3.getChangeId()).topic(topic);
+
+ assertThatChangeSubmittable(chain1change1.getChangeId());
+ }
+
+ @Test
+ public void twoChainOfChangesEndsWithExplicitMergeSameTopicNotTipOfBranches_canBeSubmitted()
+ throws Exception {
+ cfg.setBoolean("change", null, "submitWholeTopic", true);
+ // Add one more commit to master branch.
+ pushTo("refs/heads/master", ImmutableMap.of(), masterTip);
+ PushOneCommit.Result implicitMerge1 = createApprovedChange("master", otherTip);
+ PushOneCommit.Result changeInChain1 = createApprovedChange("master", implicitMerge1);
+ PushOneCommit.Result explicitMerge1 =
+ createApprovedChange("master", changeInChain1.getCommit(), masterTip);
+ PushOneCommit.Result implicitMerge2 = createApprovedChange("master", otherTip);
+ PushOneCommit.Result changeInChain2 = createApprovedChange("master", implicitMerge2);
+ PushOneCommit.Result explicitMerge2 =
+ createApprovedChange("master", changeInChain2.getCommit(), masterTip);
+ // The AbstractDaemonTest doesn't fully reset gerrit; it creates a new project for each test
+ // and doesn't remove changes created in tests. As a result, if the same topic is used in
+ // several tests gerrit tries to submit all changes, including changes from other tests.
+ // The name method returns name scoped to this test method .
+ String topic = name("topic");
+ gApi.changes().id(implicitMerge1.getChangeId()).topic(topic);
+ gApi.changes().id(changeInChain1.getChangeId()).topic(topic);
+ gApi.changes().id(explicitMerge1.getChangeId()).topic(topic);
+ gApi.changes().id(implicitMerge2.getChangeId()).topic(topic);
+ gApi.changes().id(changeInChain2.getChangeId()).topic(topic);
+ gApi.changes().id(explicitMerge2.getChangeId()).topic(topic);
+
+ assertThatChangeSubmittable(explicitMerge2.getChangeId());
+ }
+
+ @Test
+ public void twoChainOfChangesDifferentBranchesSameTopic_oneChainImplicitMerge_rejectedOnSubmit()
+ throws Exception {
+ cfg.setBoolean("change", null, "submitWholeTopic", true);
+ PushOneCommit.Result implicitMerge = createApprovedChange("other", masterTip);
+ PushOneCommit.Result im1 = createApprovedChange("other", implicitMerge);
+ PushOneCommit.Result im2 = createApprovedChange("other", im1.getCommit());
+ PushOneCommit.Result c1 = createApprovedChange("master", masterTip);
+ PushOneCommit.Result c2 = createApprovedChange("master", c1);
+ PushOneCommit.Result c3 = createApprovedChange("master", c2.getCommit());
+ // The AbstractDaemonTest doesn't fully reset gerrit; it creates a new project for each test
+ // and doesn't remove changes created in tests. As a result, if the same topic is used in
+ // several tests gerrit tries to submit all changes, including changes from other tests.
+ // The name method returns name scoped to this test method .
+ String topic = name("topic");
+ gApi.changes().id(implicitMerge.getChangeId()).topic(topic);
+ gApi.changes().id(im1.getChangeId()).topic(topic);
+ gApi.changes().id(im2.getChangeId()).topic(topic);
+ gApi.changes().id(c1.getChangeId()).topic(topic);
+ gApi.changes().id(c2.getChangeId()).topic(topic);
+ gApi.changes().id(c3.getChangeId()).topic(topic);
+
+ assertSubmitRejectedWithImplicitMerge(implicitMerge.getChangeId());
+ }
+
+ @Test
+ public void explicitMergeOnTopOfChain_onlyTopSubmittable() throws Exception {
+ PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip);
+ PushOneCommit.Result im1 = createApprovedChange("master", implicitMerge);
+ PushOneCommit.Result im2 = createApprovedChange("master", im1.getCommit());
+ PushOneCommit.Result explicitMerge = createApprovedChange("master", masterTip, im2.getCommit());
+
+ assertSubmitRejectedWithImplicitMerge(implicitMerge.getChangeId());
+ assertSubmitRejectedWithImplicitMerge(im1.getChangeId());
+ assertSubmitRejectedWithImplicitMerge(im2.getChangeId());
+ assertThatChangeSubmittable(explicitMerge.getChangeId());
+ }
+
+ @Test
+ public void explicitMergeOnTopOfChainParentIsNotBranchTip_onlyTopSubmittable() throws Exception {
+ // Add one more commit to master and other branches.
+ pushTo("refs/heads/master", ImmutableMap.of(), masterTip);
+ pushTo("refs/heads/other", ImmutableMap.of(), otherTip);
+
+ PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip);
+ PushOneCommit.Result im1 = createApprovedChange("master", implicitMerge);
+ PushOneCommit.Result im2 = createApprovedChange("master", im1.getCommit());
+ PushOneCommit.Result explicitMerge = createApprovedChange("master", masterTip, im2.getCommit());
+
+ assertSubmitRejectedWithImplicitMerge(implicitMerge.getChangeId());
+ assertSubmitRejectedWithImplicitMerge(im1.getChangeId());
+ assertSubmitRejectedWithImplicitMerge(im2.getChangeId());
+ assertThatChangeSubmittable(explicitMerge.getChangeId());
+ }
+
+ @Test
+ public void threeBranches_onlyExplicitCommitSubmittable() throws Exception {
+ BranchInput bi = new BranchInput();
+ bi.revision = baseCommit.getName();
+ gApi.projects().name(project.get()).branch("third").create(bi);
+ RevCommit thirdBranchTip =
+ pushTo("refs/heads/third", ImmutableMap.of("third-file", "third-content"), baseCommit)
+ .getCommit();
+
+ PushOneCommit.Result explicitMerge = createApprovedChange("master", masterTip, otherTip);
+ PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip, thirdBranchTip);
+ PushOneCommit.Result explicitMerge2 =
+ createApprovedChange("master", explicitMerge, implicitMerge);
+
+ assertSubmitRejectedWithImplicitMerge(implicitMerge.getChangeId());
+ assertThatChangeSubmittable(explicitMerge.getChangeId());
+ assertThatChangeSubmittable(explicitMerge2.getChangeId());
+ }
+
+ private void assertSubmitRejectedWithImplicitMerge(String changeId) throws Exception {
+ ResourceConflictException e =
+ assertThrows(
+ ResourceConflictException.class, () -> gApi.changes().id(changeId).current().submit());
+ assertThat(e.getMessage()).contains("implicit merge");
+ }
+
+ private void assertThatChangeSubmittable(String changeId) throws Exception {
+ ChangeInfo ci = gApi.changes().id(changeId).current().submit();
+ assertThat(ci.submitted).isNotNull();
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 3bec694d46..6c5febd47c 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -163,8 +163,6 @@ public class RefAdvertisementIT extends AbstractDaemonTest {
// (c3_open) (c4_open)
//
private void setUpChanges() throws Exception {
- gApi.projects().name(project.get()).branch("branch").create(new BranchInput());
-
// First 2 changes are merged, which means the tags pointing to them are
// visible.
projectOperations
@@ -183,6 +181,9 @@ public class RefAdvertisementIT extends AbstractDaemonTest {
metaRef1 = RefNames.changeMetaRef(cd1.getId());
// rcMaster (c1 master) <-- rcBranch (c2 branch)
+ BranchInput branchInput = new BranchInput();
+ branchInput.revision = mr.getCommit().getName();
+ gApi.projects().name(project.get()).branch("branch").create(branchInput);
PushOneCommit.Result br =
pushFactory.create(admin.newIdent(), testRepo).to("refs/for/branch%submit");
br.assertOkStatus();
@@ -196,7 +197,7 @@ public class RefAdvertisementIT extends AbstractDaemonTest {
// \
// (c3_open)
//
- mr = pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master");
+ mr = pushFactory.create(admin.newIdent(), testRepo).setParent(rcMaster).to("refs/for/master");
mr.assertOkStatus();
cd3 = mr.getChange();
psRef3 = cd3.currentPatchSet().id().toRefName();
@@ -205,7 +206,7 @@ public class RefAdvertisementIT extends AbstractDaemonTest {
// rcMaster (c1 master) <-- rcBranch (c2 branch)
// \ \
// (c3_open) (c4_open)
- br = pushFactory.create(admin.newIdent(), testRepo).to("refs/for/branch");
+ br = pushFactory.create(admin.newIdent(), testRepo).setParent(rcBranch).to("refs/for/branch");
br.assertOkStatus();
cd4 = br.getChange();
psRef4 = cd4.currentPatchSet().id().toRefName();
@@ -1405,7 +1406,7 @@ public class RefAdvertisementIT extends AbstractDaemonTest {
.to("refs/for/" + RefNames.REFS_USERS_SELF);
mr.assertOkStatus();
- List<String> expectedNonMetaRefs =
+ ImmutableList<String> expectedNonMetaRefs =
ImmutableList.of(
RefNames.refsUsers(admin.id()),
RefNames.refsUsers(user.id()),
@@ -1547,7 +1548,7 @@ public class RefAdvertisementIT extends AbstractDaemonTest {
return AccountGroup.uuid(gApi.groups().create(groupInput).get().id);
}
- private static Collection<String> names(Collection<Ref> refs) {
+ private static ImmutableList<String> names(Collection<Ref> refs) {
return refs.stream().map(Ref::getName).collect(toImmutableList());
}
}
diff --git a/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java b/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java
index 3cf54f3299..e18c79d4c5 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java
@@ -91,7 +91,7 @@ public class RefOperationValidationIT extends AbstractDaemonTest {
.add(
new RefOperationValidationListener() {
@Override
- public List<ValidationMessage> onRefOperation(RefReceivedEvent refEvent)
+ public ImmutableList<ValidationMessage> onRefOperation(RefReceivedEvent refEvent)
throws ValidationException {
return ImmutableList.of(
new ValidationMessage(message1, ValidationMessage.Type.HINT),
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
index c9607f5a4f..42c239b83b 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -418,7 +418,6 @@ public class SubmoduleSubscriptionsIT extends AbstractSubmoduleSubscription {
Project.NameKey config2Key =
projectOperations.newProject().parent(configKey).submitType(getSubmitType()).create();
grantPush(config2Key);
- cloneProject(config2Key);
subKey = projectOperations.newProject().parent(config2Key).submitType(getSubmitType()).create();
grantPush(subKey);
@@ -734,8 +733,7 @@ public class SubmoduleSubscriptionsIT extends AbstractSubmoduleSubscription {
gApi.projects()
.name(allProjects.get())
.submitRequirement("Block-Submodule-Change")
- .create(input)
- .get();
+ .create(input);
}
private boolean getStatus(ChangeData cd) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index 0d751f1156..dd079de674 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -645,12 +645,19 @@ public class SubmoduleSubscriptionsWholeTopicMergeIT extends AbstractSubmoduleSu
allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
allowMatchingSubmoduleSubscription(superKey, "refs/heads/dev", subKey, "refs/heads/dev");
+ // Create 'dev' branches in both repos by pushing changes.
pushChangeTo(subRepo, "dev");
pushChangeTo(superRepo, "dev");
createSubmoduleSubscription(superRepo, "master", subKey, "master");
createSubmoduleSubscription(subRepo, "dev", superKey, "dev");
+ // Reset the state of local repositories to avoid implicit merge changes.
+ subRepo.git().fetch();
+ subRepo.reset(subRepo.git().getRepository().findRef("origin/master").getObjectId().getName());
+ superRepo.git().fetch();
+ superRepo.reset(superRepo.git().getRepository().findRef("origin/dev").getObjectId().getName());
+
ObjectId subMasterHead =
pushChangeTo(
subRepo, "refs/for/master", "b.txt", "content b", "some message", "same-topic");
diff --git a/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java b/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
index 7386a03d8e..66c40782bb 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
@@ -18,10 +18,10 @@ import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.truth.StreamSubject.streams;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.extensions.client.ListGroupsOption.MEMBERS;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
import com.google.common.io.MoreFiles;
import com.google.common.io.RecursiveDeleteOption;
import com.google.gerrit.acceptance.NoHttpd;
@@ -30,6 +30,7 @@ import com.google.gerrit.acceptance.pgm.IndexUpgradeController.UpgradeAttempt;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.index.IndexDefinition;
import com.google.gerrit.index.Schema;
@@ -45,7 +46,6 @@ import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
import java.nio.file.Files;
import java.util.Collection;
-import java.util.Set;
import java.util.function.Consumer;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.storage.file.FileBasedConfig;
@@ -68,9 +68,45 @@ public abstract class AbstractReindexTests extends StandaloneSiteTest {
Files.createDirectory(sitePaths.index_dir);
assertServerStartupFails();
- runGerrit("reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace");
+ runGerrit("reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace", "--verbose");
assertReady(ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion());
+ assertIndexQueries();
+ }
+
+ @Test
+ public void reindexWithSkipExistingDocumentsEnabled() throws Exception {
+ updateConfig(config -> config.setBoolean("index", null, "reuseExistingDocuments", true));
+ setUpChange();
+
+ MoreFiles.deleteRecursively(sitePaths.index_dir, RecursiveDeleteOption.ALLOW_INSECURE);
+ Files.createDirectory(sitePaths.index_dir);
+ assertServerStartupFails();
+
+ runGerrit("reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace", "--verbose");
+ assertReady(ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion());
+
+ runGerrit("reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace", "--verbose");
+ assertIndexQueries();
+
+ Files.copy(sitePaths.index_dir, sitePaths.resolve("index-backup"));
+ try (ServerContext ctx = startServer()) {
+ GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
+ gApi.changes().id(changeId).revision(1).review(ReviewInput.approve());
+ // Query change index
+ assertThat(gApi.changes().query("label:Code-Review+2").get().stream().map(c -> c.changeId))
+ .containsExactly(changeId);
+ }
+ MoreFiles.deleteRecursively(sitePaths.index_dir, RecursiveDeleteOption.ALLOW_INSECURE);
+ Files.copy(sitePaths.resolve("index-backup"), sitePaths.index_dir);
+ runGerrit("reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace", "--verbose");
+ try (ServerContext ctx = startServer()) {
+ GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
+ assertThat(gApi.changes().query("label:Code-Review+2").get().stream().map(c -> c.changeId))
+ .containsExactly(changeId);
+ }
+ }
+ private void assertIndexQueries() throws Exception {
try (ServerContext ctx = startServer()) {
GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
// Query change index
@@ -290,7 +326,8 @@ public abstract class AbstractReindexTests extends StandaloneSiteTest {
}
private void assertReady(int expectedReady) throws Exception {
- Set<Integer> allVersions = ChangeSchemaDefinitions.INSTANCE.getSchemas().keySet();
+ ImmutableSortedSet<Integer> allVersions =
+ ChangeSchemaDefinitions.INSTANCE.getSchemas().keySet();
GerritIndexStatus status = new GerritIndexStatus(sitePaths);
assertWithMessage("ready state for index versions")
.that(
diff --git a/javatests/com/google/gerrit/acceptance/pgm/InitIT.java b/javatests/com/google/gerrit/acceptance/pgm/InitIT.java
index 073f427997..98228be86d 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/InitIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/InitIT.java
@@ -14,7 +14,7 @@
package com.google.gerrit.acceptance.pgm;
-import static com.google.common.truth.Truth8.assertThat;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import com.google.common.collect.ImmutableSet;
diff --git a/javatests/com/google/gerrit/acceptance/pgm/StartStopDaemonIT.java b/javatests/com/google/gerrit/acceptance/pgm/StartStopDaemonIT.java
index b3ae01fef7..b2dedc0225 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/StartStopDaemonIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/StartStopDaemonIT.java
@@ -21,18 +21,43 @@ import com.google.gerrit.acceptance.Sandboxed;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
@Sandboxed
public class StartStopDaemonIT extends AbstractDaemonTest {
Description suiteDescription = Description.createSuiteDescription(StartStopDaemonIT.class);
+ @Override
+ protected TestRule createTopLevelTestRule() {
+ TestRule innerRules = super.createTopLevelTestRule();
+ return RuleChain.outerRule(
+ new TestRule() {
+ @Override
+ public Statement apply(Statement statement, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ ThreadMXBean thbean = ManagementFactory.getThreadMXBean();
+ int startThreads = thbean.getThreadCount();
+ statement.evaluate();
+ assertThat(Thread.activeCount()).isLessThan(startThreads);
+ }
+ };
+ }
+ })
+ .around(innerRules);
+ }
+
+ @Test
+ public void sandboxedDaemonDoesNotLeakThreads_1() throws Exception {
+ // dummy test - the sandboxed server will be started and then stopped
+ }
+
@Test
- public void sandboxedDaemonDoesNotLeakThreads() throws Exception {
- ThreadMXBean thbean = ManagementFactory.getThreadMXBean();
- int startThreads = thbean.getThreadCount();
- beforeTest(suiteDescription);
- afterTest();
- assertThat(Thread.activeCount()).isLessThan(startThreads);
+ public void sandboxedDaemonDoesNotLeakThreads_2() throws Exception {
+ // dummy test - the sandboxed server will be started and then stopped
}
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java b/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java
index 0393f2b1c5..5c1f7c2bf7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java
@@ -20,6 +20,7 @@ import static com.google.gerrit.httpd.restapi.RestApiServlet.X_GERRIT_UPDATED_RE
import static com.google.gerrit.httpd.restapi.RestApiServlet.X_GERRIT_UPDATED_REF_ENABLED;
import static org.apache.http.HttpStatus.SC_OK;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit.Result;
import com.google.gerrit.acceptance.RestResponse;
@@ -34,7 +35,6 @@ import com.google.gerrit.httpd.restapi.ParameterParser;
import com.google.gerrit.httpd.restapi.RestApiServlet;
import com.google.inject.Inject;
import java.io.IOException;
-import java.util.List;
import java.util.regex.Pattern;
import org.apache.http.message.BasicHeader;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -202,7 +202,7 @@ public class RestApiServletIT extends AbstractDaemonTest {
"/changes/" + change.getChangeId(), X_GERRIT_UPDATED_REF_ENABLED_HEADER);
response.assertNoContent();
- List<String> headers = response.getHeaders(X_GERRIT_UPDATED_REF);
+ ImmutableList<String> headers = response.getHeaders(X_GERRIT_UPDATED_REF);
// The change was deleted, so the refs were deleted which means they are ObjectId.zeroId().
assertThat(headers)
@@ -236,7 +236,7 @@ public class RestApiServletIT extends AbstractDaemonTest {
"/changes/" + change.getChangeId(), X_GERRIT_UPDATED_REF_ENABLED_HEADER);
response.assertNoContent();
- List<String> headers = response.getHeaders(X_GERRIT_UPDATED_REF);
+ ImmutableList<String> headers = response.getHeaders(X_GERRIT_UPDATED_REF);
// The change was deleted, so the refs were deleted which means they are ObjectId.zeroId().
assertThat(headers)
@@ -286,7 +286,7 @@ public class RestApiServletIT extends AbstractDaemonTest {
ObjectId firstMetaRefSha1 = getMetaRefSha1(change1);
ObjectId secondMetaRefSha1 = getMetaRefSha1(change2);
- List<String> headers = response.getHeaders(X_GERRIT_UPDATED_REF);
+ ImmutableList<String> headers = response.getHeaders(X_GERRIT_UPDATED_REF);
String branch = change1.getChange().change().getDest().branch();
String branchSha1 =
@@ -363,7 +363,7 @@ public class RestApiServletIT extends AbstractDaemonTest {
ObjectId firstMetaRefSha1 = getMetaRefSha1(change1);
ObjectId secondMetaRefSha1 = getMetaRefSha1(change2);
- List<String> headers = response.getHeaders(X_GERRIT_UPDATED_REF);
+ ImmutableList<String> headers = response.getHeaders(X_GERRIT_UPDATED_REF);
String branchSha1Project1 =
repository1
@@ -473,13 +473,10 @@ public class RestApiServletIT extends AbstractDaemonTest {
return change.getChange().notes().getRevision();
}
- private RestResponse assertRestResponseWithParameters(int status, String k, String v)
- throws Exception {
+ private void assertRestResponseWithParameters(int status, String k, String v) throws Exception {
RestResponse response =
adminRestSession.getWithHeaders(ANY_REST_API + "?" + k + "=" + v, ACCEPT_STAR_HEADER);
assertThat(response.getStatusCode()).isEqualTo(status);
-
- return response;
}
private RestResponse prettyJsonRestResponse(String ppArgument, int ppValue) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java b/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java
index 79e8ab0496..9b6b260499 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java
@@ -16,6 +16,7 @@ package com.google.gerrit.acceptance.rest.account;
import static com.google.common.truth.Truth.assertThat;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.entities.Account;
@@ -49,7 +50,7 @@ public class AccountAssert {
* @param actual the AccountInfos that should be asserted
*/
public static void assertAccountInfos(List<TestAccount> expected, List<AccountInfo> actual) {
- Iterable<Account.Id> expectedIds = TestAccount.ids(expected);
+ ImmutableList<Account.Id> expectedIds = TestAccount.ids(expected);
Iterable<Account.Id> actualIds = Iterables.transform(actual, a -> Account.id(a._accountId));
assertThat(actualIds).containsExactlyElementsIn(expectedIds).inOrder();
for (int i = 0; i < expected.size(); i++) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/CreateAccountIT.java b/javatests/com/google/gerrit/acceptance/rest/account/CreateAccountIT.java
index 8d801f17da..5fef74c0b3 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/CreateAccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/CreateAccountIT.java
@@ -14,7 +14,7 @@
package com.google.gerrit.acceptance.rest.account;
-import static com.google.common.truth.Truth8.assertThat;
+import static com.google.common.truth.Truth.assertThat;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.RestResponse;
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java b/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
index f40910a8f1..794634f072 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
@@ -15,14 +15,12 @@
package com.google.gerrit.acceptance.rest.account;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.util.stream.Collectors.toSet;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.entities.Account;
@@ -48,6 +46,7 @@ import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.EnablePeerIPInReflogRecord;
+import com.google.gerrit.server.util.ManualRequestContext;
import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -193,15 +192,13 @@ public class EmailIT extends AbstractDaemonTest {
assertThat(externalIds.get(mailtoExtIdKey)).isEmpty();
assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
- Context oldCtx = createContextWithCustomRealm(new RealmWithAdditionalEmails(admin.id(), email));
- try {
+ try (ManualRequestContext newCtx =
+ createContextWithCustomRealm(new RealmWithAdditionalEmails(admin.id(), email))) {
gApi.accounts().self().email(email).setPreferred();
Optional<ExternalId> mailtoExtId = externalIds.get(mailtoExtIdKey);
assertThat(mailtoExtId).isPresent();
assertThat(mailtoExtId.get().accountId()).isEqualTo(admin.id());
assertThat(gApi.accounts().self().get().email).isEqualTo(email);
- } finally {
- atrScope.set(oldCtx);
}
}
@@ -210,16 +207,13 @@ public class EmailIT extends AbstractDaemonTest {
ExternalId mailToExtId = externalIdFactory.createEmail(user.id(), user.email());
assertThat(externalIds.get(mailToExtId.key())).isPresent();
- Context oldCtx =
- createContextWithCustomRealm(new RealmWithAdditionalEmails(admin.id(), user.email()));
- try {
+ try (ManualRequestContext newCtx =
+ createContextWithCustomRealm(new RealmWithAdditionalEmails(admin.id(), user.email()))) {
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class,
() -> gApi.accounts().self().email(user.email()).setPreferred());
assertThat(thrown).hasMessageThat().contains("email in use by another account");
- } finally {
- atrScope.set(oldCtx);
}
}
@@ -280,7 +274,7 @@ public class EmailIT extends AbstractDaemonTest {
r.assertCreated();
}
- private Context createContextWithCustomRealm(Realm realm) {
+ private ManualRequestContext createContextWithCustomRealm(Realm realm) {
IdentifiedUser.GenericFactory userFactory =
new IdentifiedUser.GenericFactory(
authConfig,
@@ -291,7 +285,7 @@ public class EmailIT extends AbstractDaemonTest {
enablePeerIPInReflogRecord,
accountCache,
groupBackend);
- return atrScope.set(atrScope.newContext(null, userFactory.create(admin.id())));
+ return new ManualRequestContext(userFactory.create(admin.id()), localCtx);
}
private class RealmWithAdditionalEmails extends DefaultRealm {
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index 749ca792ca..e62d3657af 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -15,7 +15,6 @@
package com.google.gerrit.acceptance.rest.account;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.GitUtil.fetch;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
@@ -110,7 +109,7 @@ public class ExternalIdIT extends AbstractDaemonTest {
@Test
public void getExternalIds() throws Exception {
- Collection<ExternalId> expectedIds = getAccountState(user.id()).externalIds();
+ ImmutableSet<ExternalId> expectedIds = getAccountState(user.id()).externalIds();
List<AccountExternalIdInfo> expectedIdInfos = toExternalIdInfos(expectedIds);
RestResponse response = userRestSession.get("/accounts/self/external.ids");
@@ -125,7 +124,7 @@ public class ExternalIdIT extends AbstractDaemonTest {
}
@Test
- public void getExternalIdsOfOtherUserNotAllowed() {
+ public void getExternalIdsOfOtherUserNotAllowed() throws Exception {
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
assertThrows(
@@ -140,7 +139,7 @@ public class ExternalIdIT extends AbstractDaemonTest {
.add(allowCapability(GlobalCapability.MODIFY_ACCOUNT).group(REGISTERED_USERS))
.update();
- Collection<ExternalId> expectedIds = getAccountState(admin.id()).externalIds();
+ ImmutableSet<ExternalId> expectedIds = getAccountState(admin.id()).externalIds();
List<AccountExternalIdInfo> expectedIdInfos = toExternalIdInfos(expectedIds);
RestResponse response = userRestSession.get("/accounts/" + admin.id() + "/external.ids");
@@ -510,11 +509,11 @@ public class ExternalIdIT extends AbstractDaemonTest {
insertValidExternalIds();
insertInvalidButParsableExternalIds();
- Set<ExternalId> parseableExtIds = externalIds.all();
+ ImmutableSet<ExternalId> parseableExtIds = externalIds.all();
insertNonParsableExternalIds();
- Set<ExternalId> extIds = externalIds.all();
+ ImmutableSet<ExternalId> extIds = externalIds.all();
assertThat(extIds).containsExactlyElementsIn(parseableExtIds);
for (ExternalId parseableExtId : parseableExtIds) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
index 44771405eb..2e706b804c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -34,7 +34,6 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.RestSession;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.UseLocalDisk;
import com.google.gerrit.acceptance.config.GerritConfig;
@@ -98,13 +97,11 @@ public class ImpersonationIT extends AbstractDaemonTest {
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
- private RestSession anonRestSession;
private TestAccount admin2;
private GroupInfo newGroup;
@Before
public void setUp() throws Exception {
- anonRestSession = new RestSession(server, null);
admin2 = accountCreator.admin2();
GroupInput gi = new GroupInput();
gi.name = name("New-Group");
@@ -759,7 +756,7 @@ public class ImpersonationIT extends AbstractDaemonTest {
@Test
public void runAsNeverPermittedForAnonymousUsers() throws Exception {
allowRunAs();
- RestResponse res = anonRestSession.getWithHeaders("/changes/", runAsHeader(user.id()));
+ RestResponse res = anonymousRestSession.getWithHeaders("/changes/", runAsHeader(user.id()));
res.assertForbidden();
assertThat(res.getEntityContent()).isEqualTo("not permitted to use X-Gerrit-RunAs");
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/auth/AuthenticationCheckIT.java b/javatests/com/google/gerrit/acceptance/rest/auth/AuthenticationCheckIT.java
index 00b1c5502f..1951bdd542 100644
--- a/javatests/com/google/gerrit/acceptance/rest/auth/AuthenticationCheckIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/auth/AuthenticationCheckIT.java
@@ -14,9 +14,12 @@
package com.google.gerrit.acceptance.rest.auth;
+import static com.google.common.truth.Truth.assertThat;
+
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.RestSession;
+import java.io.BufferedReader;
+import java.util.stream.Collectors;
import org.junit.Test;
public class AuthenticationCheckIT extends AbstractDaemonTest {
@@ -29,8 +32,17 @@ public class AuthenticationCheckIT extends AbstractDaemonTest {
@Test
public void authCheck_anonymousUser_returnsForbidden() throws Exception {
- RestSession anonymous = new RestSession(server, null);
- RestResponse r = anonymous.get("/auth-check");
+ RestResponse r = anonymousRestSession.get("/auth-check");
r.assertForbidden();
}
+
+ @Test
+ public void authCheckSvg_loggedInUser_returnsOk() throws Exception {
+ RestResponse r = adminRestSession.get("/auth-check.svg");
+ r.assertOK();
+ BufferedReader br = new BufferedReader(r.getReader());
+ String content = br.lines().collect(Collectors.joining());
+ assertThat(content).contains("<svg xmlns");
+ assertThat(r.getHeader("Content-Type")).isEqualTo("image/svg+xml;charset=utf-8");
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
index 6a5441ce1b..cc86d02f08 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
@@ -78,6 +78,7 @@ public class ChangesRestApiBindingsIT extends AbstractDaemonTest {
RestCall.get("/changes/%s/meta_diff"),
RestCall.post("/changes/%s/merge"),
RestCall.get("/changes/%s/messages"),
+ RestCall.get("/changes/%s/message"),
RestCall.put("/changes/%s/message"),
RestCall.post("/changes/%s/move"),
RestCall.post("/changes/%s/patch:apply"),
@@ -471,7 +472,10 @@ public class ChangesRestApiBindingsIT extends AbstractDaemonTest {
RestApiCallHelper.execute(
adminRestSession,
CHANGE_EDIT_CREATE_ENDPOINTS,
- () -> adminRestSession.delete("/changes/" + changeId + "/edit"),
+ () -> {
+ @SuppressWarnings("unused")
+ var unused = adminRestSession.delete("/changes/" + changeId + "/edit");
+ },
changeId,
FILENAME);
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
index 57279d3c1b..666742101f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
@@ -14,7 +14,7 @@
package com.google.gerrit.acceptance.rest.binding;
-import static com.google.common.truth.Truth8.assertThat;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -25,6 +25,7 @@ import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
import com.google.gerrit.acceptance.rest.util.RestCall;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
import com.google.gerrit.server.project.ProjectCacheImpl;
import com.google.gerrit.server.restapi.config.ListTasks.TaskInfo;
import com.google.gson.reflect.TypeToken;
@@ -51,6 +52,7 @@ public class ConfigRestApiBindingsIT extends AbstractDaemonTest {
RestCall.get("/config/server/capabilities"),
RestCall.post("/config/server/check.consistency"),
RestCall.put("/config/server/email.confirm"),
+ RestCall.get("/config/server/experiments"),
RestCall.post("/config/server/index.changes"),
RestCall.get("/config/server/info"),
RestCall.get("/config/server/preferences"),
@@ -73,6 +75,13 @@ public class ConfigRestApiBindingsIT extends AbstractDaemonTest {
ImmutableList.of(RestCall.get("/config/server/caches/%s"));
/**
+ * Experiment REST endpoints to be tested, the URLs contain a placeholder for the experiment name.
+ * Since there is only a single supported config identifier ('server') it can be hard-coded.
+ */
+ private static final ImmutableList<RestCall> EXPERIMENT_ENDPOINTS =
+ ImmutableList.of(RestCall.get("/config/server/experiments/%s"));
+
+ /**
* Task REST endpoints to be tested, the URLs contain a placeholder for the task identifier. Since
* there is only a single supported config identifier ('server') it can be hard-coded.
*/
@@ -102,6 +111,14 @@ public class ConfigRestApiBindingsIT extends AbstractDaemonTest {
}
@Test
+ public void experimentEndpoints() throws Exception {
+ RestApiCallHelper.execute(
+ adminRestSession,
+ EXPERIMENT_ENDPOINTS,
+ ExperimentFeaturesConstants.ALLOW_FIX_SUGGESTIONS_IN_COMMENTS);
+ }
+
+ @Test
public void taskEndpoints() throws Exception {
RestResponse r = adminRestSession.get("/config/server/tasks/");
List<TaskInfo> result =
@@ -110,7 +127,7 @@ public class ConfigRestApiBindingsIT extends AbstractDaemonTest {
Optional<String> id =
result.stream()
- .filter(t -> "Log File Compressor".equals(t.command))
+ .filter(t -> "Log File Manager".equals(t.command))
.map(t -> t.id)
.findFirst();
assertThat(id).isPresent();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index ecae27e3bd..25af040902 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -17,7 +17,6 @@ package com.google.gerrit.acceptance.rest.change;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
@@ -38,6 +37,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import com.github.rholder.retry.RetryException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
@@ -90,6 +90,7 @@ import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.approval.ApprovalsUtil;
+import com.google.gerrit.server.change.MergeabilityComputationBehavior;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.change.TestSubmitInput;
import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
@@ -126,6 +127,7 @@ import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.RefSpec;
+import org.junit.Before;
import org.junit.Test;
@NoHttpd
@@ -137,6 +139,17 @@ public abstract class AbstractSubmit extends AbstractDaemonTest {
return submitWholeTopicEnabledConfig();
}
+ @ConfigSuite.Config
+ public static Config mergeabilityCheckEnabled() {
+ Config cfg = new Config();
+ cfg.setEnum(
+ "change",
+ null,
+ "mergeabilityComputationBehavior",
+ MergeabilityComputationBehavior.API_REF_UPDATED_AND_CHANGE_REINDEX);
+ return cfg;
+ }
+
@Inject private ApprovalsUtil approvalsUtil;
@Inject private IdentifiedUser.GenericFactory userFactory;
@Inject private ProjectOperations projectOperations;
@@ -147,8 +160,15 @@ public abstract class AbstractSubmit extends AbstractDaemonTest {
@Inject private ChangeIndexer changeIndex;
+ protected MergeabilityComputationBehavior mcb;
+
protected abstract SubmitType getSubmitType();
+ @Before
+ public void setUp() {
+ mcb = MergeabilityComputationBehavior.fromConfig(cfg);
+ }
+
@Test
@TestProjectInput(createEmptyCommit = false)
public void submitToEmptyRepo() throws Throwable {
@@ -668,7 +688,17 @@ public abstract class AbstractSubmit extends AbstractDaemonTest {
List<RevCommit> log = getRemoteLog();
assertThat(log).contains(stable.getCommit());
- assertThat(log).contains(mergeReview.getCommit());
+
+ if (getSubmitType() == SubmitType.REBASE_ALWAYS) {
+ // the merge commit has been rebased
+ RevCommit newHead = projectOperations.project(project).getHead("master");
+ assertThat(newHead.getParentCount()).isEqualTo(2);
+
+ assertThat(newHead.getParent(0).getId()).isEqualTo(master);
+ assertThat(newHead.getParent(1).getId()).isEqualTo(stable.getCommit());
+ } else {
+ assertThat(log).contains(mergeReview.getCommit());
+ }
}
@Test
@@ -712,7 +742,17 @@ public abstract class AbstractSubmit extends AbstractDaemonTest {
List<RevCommit> log = getRemoteLog();
assertThat(log).contains(s1.getCommit());
- assertThat(log).contains(mergeReview.getCommit());
+
+ if (getSubmitType() == SubmitType.REBASE_ALWAYS) {
+ // the merge commit has been rebased
+ RevCommit newHead = projectOperations.project(project).getHead("master");
+ assertThat(newHead.getParentCount()).isEqualTo(2);
+
+ assertThat(newHead.getParent(0).getId()).isEqualTo(m.getCommit());
+ assertThat(newHead.getParent(1).getId()).isEqualTo(s1.getCommit());
+ } else {
+ assertThat(log).contains(mergeReview.getCommit());
+ }
}
@Test
@@ -941,9 +981,18 @@ public abstract class AbstractSubmit extends AbstractDaemonTest {
assertMerged(mergeId);
testRepo.git().fetch().call();
RevWalk rw = testRepo.getRevWalk();
- master = rw.parseCommit(projectOperations.project(project).getHead("master"));
- assertThat(rw.isMergedInto(merge, master)).isTrue();
- assertThat(rw.isMergedInto(fix, master)).isTrue();
+ RevCommit newMaster = rw.parseCommit(projectOperations.project(project).getHead("master"));
+ assertThat(rw.isMergedInto(fix, newMaster)).isTrue();
+
+ if (getSubmitType() == SubmitType.REBASE_ALWAYS) {
+ // the merge commit has been rebased
+ assertThat(newMaster.getParentCount()).isEqualTo(2);
+
+ assertThat(newMaster.getParent(0).getId()).isEqualTo(master);
+ assertThat(newMaster.getParent(1).getId()).isEqualTo(fix);
+ } else {
+ assertThat(rw.isMergedInto(merge, newMaster)).isTrue();
+ }
}
@Test
@@ -989,7 +1038,11 @@ public abstract class AbstractSubmit extends AbstractDaemonTest {
testMetricMaker.reset();
Throwable thrown = assertThrows(StorageException.class, () -> submit(id, input));
- assertThat(thrown.getCause()).hasMessageThat().contains("missing from ChangeSet[][]");
+ assertThat(thrown.getCause()).hasMessageThat().contains("Computing mergeSuperset has failed");
+ assertThat(thrown.getCause()).hasCauseThat().isInstanceOf(RetryException.class);
+ assertThat(thrown.getCause().getCause().getCause())
+ .hasMessageThat()
+ .contains("missing from ChangeSet[][]");
// We retried more than once before giving up
assertThat(
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
index 9d98ecb5fd..c47b3a4918 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
@@ -22,6 +22,7 @@ import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.server.change.MergeabilityComputationBehavior;
import com.google.inject.Inject;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Test;
@@ -140,7 +141,9 @@ public abstract class AbstractSubmitByMerge extends AbstractSubmit {
approve(change2Result.getChangeId());
// submit button is disabled.
- assertSubmitDisabled(change2Result.getChangeId());
+ if (mcb != MergeabilityComputationBehavior.NEVER) {
+ assertSubmitDisabled(change2Result.getChangeId());
+ }
submitWithConflict(
change2Result.getChangeId(),
@@ -191,7 +194,9 @@ public abstract class AbstractSubmitByMerge extends AbstractSubmit {
approve(change2Result.getChangeId());
// submit button is disabled.
- assertSubmitDisabled(change2Result.getChangeId());
+ if (mcb != MergeabilityComputationBehavior.NEVER) {
+ assertSubmitDisabled(change2Result.getChangeId());
+ }
submitWithConflict(
change2Result.getChangeId(),
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
index a30b5c40ec..c34625ebe4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
@@ -36,6 +36,7 @@ import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.server.change.MergeabilityComputationBehavior;
import com.google.gerrit.server.project.testing.TestLabels;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.ObjectId;
@@ -168,17 +169,74 @@ public abstract class AbstractSubmitByRebase extends AbstractSubmit {
}
@Test
- public void submitWithRebaseMergeCommit() throws Throwable {
+ public void submitMergeCommitThatDependsOnNormalChangeViaTheFirstParent() throws Throwable {
/*
- * (HEAD, origin/master, origin/HEAD) Merge changes X,Y
- |\
- | * Merge branch 'master' into origin/master
- | |\
- | | * SHA Added a
+ * change2 (merge, rebased)
+ | \
+ * \ change1 (rebased)
+ | |
+ * | change3 (new tip, rebased if 'Merge Always')
+ | |
+ | * | change2 (merge)
+ | |\|
+ | | |
+ | * | change1
+ \|/
+ * initialHead
+ */
+ RevCommit initialHead = projectOperations.project(project).getHead("master");
+ PushOneCommit.Result change1 = createChange("Added a", "a.txt", "");
+
+ PushOneCommit change2Push =
+ pushFactory.create(admin.newIdent(), testRepo, "Merge to master", "m.txt", "");
+ change2Push.setParents(ImmutableList.of(change1.getCommit(), initialHead));
+ PushOneCommit.Result change2 = change2Push.to("refs/for/master");
+
+ testRepo.reset(initialHead);
+ PushOneCommit.Result change3 = createChange("New tip", "b.txt", "");
+
+ approve(change3.getChangeId());
+ submit(change3.getChangeId());
+
+ approve(change1.getChangeId());
+ approve(change2.getChangeId());
+ submit(change2.getChangeId());
+
+ RevCommit newHead = projectOperations.project(project).getHead("master");
+ assertThat(newHead.getParentCount()).isEqualTo(2);
+
+ RevCommit headParent1 = parse(newHead.getParent(0).getId());
+ RevCommit headParent2 = parse(newHead.getParent(1).getId());
+
+ assertCurrentRevision(change1.getChangeId(), 2, headParent1.getId());
+ assertThat(headParent2.getId()).isEqualTo(initialHead.getId());
+
+ assertThat(headParent1.getParentCount()).isEqualTo(1);
+ RevCommit headGrandparent1 = parse(headParent1.getParent(0).getId());
+ if (getSubmitType() == SubmitType.REBASE_ALWAYS) {
+ assertCurrentRevision(change3.getChangeId(), 2, headGrandparent1.getId());
+ } else {
+ assertThat(change3.getCommit().getId()).isEqualTo(headGrandparent1.getId());
+ }
+
+ assertThat(headGrandparent1.getParentCount()).isEqualTo(1);
+ assertThat(headGrandparent1.getParent(0).getId()).isEqualTo(initialHead.getId());
+ }
+
+ @Test
+ public void submitMergeCommitThatDependsOnNormalChangeViaTheSecondParent() throws Throwable {
+ /*
+ * change2 (merge, rebased)
+ | \
+ * \ change3 (new tip, rebased if 'Rebase Always')
+ | |
+ | * | change2 (merge)
+ | |\|
+ | | * change1
| |/
- * | Before
+ | |
|/
- * Initial empty repository
+ * initialHead
*/
RevCommit initialHead = projectOperations.project(project).getHead("master");
PushOneCommit.Result change1 = createChange("Added a", "a.txt", "");
@@ -189,7 +247,7 @@ public abstract class AbstractSubmitByRebase extends AbstractSubmit {
PushOneCommit.Result change2 = change2Push.to("refs/for/master");
testRepo.reset(initialHead);
- PushOneCommit.Result change3 = createChange("Before", "b.txt", "");
+ PushOneCommit.Result change3 = createChange("New tip", "b.txt", "");
approve(change3.getChangeId());
submit(change3.getChangeId());
@@ -212,14 +270,12 @@ public abstract class AbstractSubmitByRebase extends AbstractSubmit {
assertThat(headParent1.getParentCount()).isEqualTo(1);
assertThat(headParent1.getParent(0)).isEqualTo(initialHead);
- assertThat(headParent2.getId()).isEqualTo(change2.getCommit().getId());
- assertThat(headParent2.getParentCount()).isEqualTo(2);
+ assertThat(headParent2.getId()).isEqualTo(change1.getCommit().getId());
+ assertThat(headParent2.getParentCount()).isEqualTo(1);
- RevCommit headGrandparent1 = parse(headParent2.getParent(0).getId());
- RevCommit headGrandparent2 = parse(headParent2.getParent(1).getId());
+ RevCommit headGrandparent = parse(headParent2.getParent(0).getId());
- assertThat(headGrandparent1.getId()).isEqualTo(initialHead.getId());
- assertThat(headGrandparent2.getId()).isEqualTo(change1.getCommit().getId());
+ assertThat(headGrandparent.getId()).isEqualTo(initialHead.getId());
}
@Test
@@ -410,7 +466,9 @@ public abstract class AbstractSubmitByRebase extends AbstractSubmit {
approve(change2Result.getChangeId());
// submit button is disabled.
- assertSubmitDisabled(change2Result.getChangeId());
+ if (mcb != MergeabilityComputationBehavior.NEVER) {
+ assertSubmitDisabled(change2Result.getChangeId());
+ }
submitWithConflict(
change2Result.getChangeId(),
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index fbcc5fa59a..78d6737a80 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -33,6 +33,7 @@ import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ActionInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.server.change.MergeabilityComputationBehavior;
import com.google.gerrit.server.change.RevisionJson;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.testing.ConfigSuite;
@@ -42,6 +43,7 @@ import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
import org.junit.Test;
public class ActionsIT extends AbstractDaemonTest {
@@ -50,9 +52,22 @@ public class ActionsIT extends AbstractDaemonTest {
return submitWholeTopicEnabledConfig();
}
+ @ConfigSuite.Config
+ public static Config mergeabilityCheckEnabled() {
+ Config cfg = new Config();
+ cfg.setEnum(
+ "change",
+ null,
+ "mergeabilityComputationBehavior",
+ MergeabilityComputationBehavior.API_REF_UPDATED_AND_CHANGE_REINDEX);
+ return cfg;
+ }
+
@Inject private RevisionJson.Factory revisionJsonFactory;
@Inject private ExtensionRegistry extensionRegistry;
+ private MergeabilityComputationBehavior mcb;
+
protected Map<String, ActionInfo> getActions(String id) throws Exception {
return gApi.changes().id(id).revision(1).actions();
}
@@ -61,6 +76,11 @@ public class ActionsIT extends AbstractDaemonTest {
return gApi.changes().id(id).get().actions;
}
+ @Before
+ public void setUp() {
+ mcb = MergeabilityComputationBehavior.fromConfig(cfg);
+ }
+
@Test
public void changeActionOneMergedChangeHasOnlyNormalRevert() throws Exception {
String changeId = createChangeWithTopic().getChangeId();
@@ -160,10 +180,12 @@ public class ActionsIT extends AbstractDaemonTest {
commonActionsAssertions(actions);
if (isSubmitWholeTopicEnabled()) {
ActionInfo info = actions.get("submit");
- assertThat(info.enabled).isNull();
+ if (mcb != MergeabilityComputationBehavior.NEVER) {
+ assertThat(info.enabled).isNull();
+ assertThat(info.title).isEqualTo("Problems with change(s): " + changeNum2);
+ }
assertThat(info.label).isEqualTo("Submit whole topic");
assertThat(info.method).isEqualTo("POST");
- assertThat(info.title).isEqualTo("Problems with change(s): " + changeNum2);
} else {
noSubmitWholeTopicAssertions(actions, 1);
}
@@ -323,7 +345,11 @@ public class ActionsIT extends AbstractDaemonTest {
// ...via ChangeJson directly.
ChangeData cd = changeDataFactory.create(project, changeId);
- revisionJsonFactory.create(opts).getRevisionInfo(cd, cd.patchSet(PatchSet.id(changeId, 1)));
+ revisionInfo =
+ revisionJsonFactory
+ .create(opts)
+ .getRevisionInfo(cd, cd.patchSet(PatchSet.id(changeId, revisionInfo._number)));
+ visitedCurrentRevisionActionsAssertions(origActions, revisionInfo.actions);
}
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
index cbc3f9da4c..c7672d19c3 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
@@ -48,6 +48,7 @@ import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.RefNames;
@@ -138,7 +139,7 @@ public class AttentionSetIT extends AbstractDaemonTest {
}
@Test
- public void addUser() throws Exception {
+ public void addUser_updateReason() throws Exception {
PushOneCommit.Result r = createChange();
requestScopeOperations.setApiUser(user.id());
int accountId =
@@ -149,19 +150,62 @@ public class AttentionSetIT extends AbstractDaemonTest {
fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.ADD, "first");
assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
- // Second add is ignored.
+ // Check that email was sent
+ String emailBody = Iterables.getOnlyElement(sender.getMessages()).body();
+ assertThat(emailBody)
+ .contains(
+ String.format(
+ "%s requires the attention of %s to this change.\n The reason is: first.",
+ user.fullName(), admin.fullName()));
+
+ // Update the reason
+ sender.clear();
accountId =
change(r).addToAttentionSet(new AttentionSetInput(admin.email(), "second"))._accountId;
assertThat(accountId).isEqualTo(admin.id().get());
+ expectedAttentionSetUpdate =
+ AttentionSetUpdate.createFromRead(
+ fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.ADD, "second");
assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
- // Only one email since the second add was ignored.
+ // Check that email was sent
+ emailBody = Iterables.getOnlyElement(sender.getMessages()).body();
+ assertThat(emailBody)
+ .contains(
+ String.format(
+ "%s requires the attention of %s to this change.\n The reason is: second.",
+ user.fullName(), admin.fullName()));
+ }
+
+ @Test
+ public void addUser_addWithSameReasonIsIgnored() throws Exception {
+ PushOneCommit.Result r = createChange();
+ requestScopeOperations.setApiUser(user.id());
+ int accountId =
+ change(r).addToAttentionSet(new AttentionSetInput(admin.email(), "reason"))._accountId;
+ assertThat(accountId).isEqualTo(admin.id().get());
+ AttentionSetUpdate expectedAttentionSetUpdate =
+ AttentionSetUpdate.createFromRead(
+ fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.ADD, "reason");
+ assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
+
+ // Check that email was sent
String emailBody = Iterables.getOnlyElement(sender.getMessages()).body();
assertThat(emailBody)
.contains(
String.format(
- "%s requires the attention of %s to this change.\n The reason is: first.",
+ "%s requires the attention of %s to this change.\n The reason is: reason.",
user.fullName(), admin.fullName()));
+
+ // Second add with the same reason is ignored.
+ sender.clear();
+ accountId =
+ change(r).addToAttentionSet(new AttentionSetInput(admin.email(), "reason"))._accountId;
+ assertThat(accountId).isEqualTo(admin.id().get());
+ assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
+
+ // Check that no email was sent
+ assertThat(sender.getMessages()).isEmpty();
}
@Test
@@ -987,14 +1031,180 @@ public class AttentionSetIT extends AbstractDaemonTest {
}
@Test
- public void repliesAddsOwner() throws Exception {
+ public void repliesAddsOwner_voteAdded() throws Exception {
+ PushOneCommit.Result r = createChange();
+
+ requestScopeOperations.setApiUser(user.id());
+ change(r).current().review(ReviewInput.dislike());
+
+ // The change owner has been added to the attention set
+ AttentionSetUpdate attentionSet =
+ Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin));
+ assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id());
+ assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD);
+ assertThat(attentionSet).hasReasonThat().isEqualTo("Someone else replied on the change");
+ }
+
+ @Test
+ public void repliesAddsOwner_voteCopied() throws Exception {
+ // Define a label with a copy condition that copies all votes.
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType.Builder fooReview =
+ labelBuilder(
+ "Foo-Review",
+ value(1, "Looks good to me, but someone else must approve"),
+ value(0, "No score"),
+ value(-1, "I would prefer this is not submitted as is"))
+ .setCopyCondition("is:ANY");
+ u.getConfig().upsertLabelType(fooReview.build());
+ u.save();
+ }
+
+ // Allow voting on the label.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabel("Foo-Review")
+ .ref(RefNames.REFS_HEADS + "*")
+ .group(REGISTERED_USERS)
+ .range(-1, 1))
+ .update();
+
+ // Create a change with 2 patch sets.
+ PushOneCommit.Result r = createChange();
+ PatchSet patchSet1 = r.getChange().currentPatchSet();
+ r = amendChange(r.getChangeId());
+ r.assertOkStatus();
+
+ // Apply a negative vote on the first patch set which is copied since the label has a copy
+ // condition that copies all votes
+ requestScopeOperations.setApiUser(user.id());
+ change(r).revision(patchSet1.number()).review(new ReviewInput().label("Foo-Review", -1));
+
+ // The change owner has been added to the attention set
+ AttentionSetUpdate attentionSet =
+ Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin));
+ assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id());
+ assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD);
+ assertThat(attentionSet).hasReasonThat().isEqualTo("Someone else replied on the change");
+ }
+
+ @Test
+ public void repliesDoesntAddOwner_voteNotCopied_userNotRemovedReasonUpdated() throws Exception {
+ // Define a label without any copy condition.
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType.Builder fooReview =
+ labelBuilder(
+ "Foo-Review",
+ value(1, "Looks good to me, but someone else must approve"),
+ value(0, "No score"),
+ value(-1, "I would prefer this is not submitted as is"));
+ u.getConfig().upsertLabelType(fooReview.build());
+ u.save();
+ }
+
+ // Allow voting on the label.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabel("Foo-Review")
+ .ref(RefNames.REFS_HEADS + "*")
+ .group(REGISTERED_USERS)
+ .range(-1, 1))
+ .update();
+
+ // Create a change with 2 patch sets.
PushOneCommit.Result r = createChange();
+ PatchSet patchSet1 = r.getChange().currentPatchSet();
+ r = amendChange(r.getChangeId());
+ r.assertOkStatus();
+
+ // Add user as a reviewer so that the user is in the attention set
+ change(r).addReviewer(user.email());
+ assertThat(getAttentionSetUpdatesForUser(r, user)).isNotEmpty();
+ // Apply a negative vote on the first patch set which is not copied since the label doesn't have
+ // a copy condition
requestScopeOperations.setApiUser(user.id());
+ change(r).revision(patchSet1.number()).review(new ReviewInput().label("Foo-Review", -1));
+
+ // The change owner has not been added to the attention set
+ assertThat(getAttentionSetUpdatesForUser(r, admin)).isEmpty();
+ // The reason for the user to be in the attention set has been updated
+ AttentionSetUpdate attentionSet =
+ Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user));
+ assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id());
+ assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD);
+ assertThat(attentionSet)
+ .hasReasonThat()
+ .isEqualTo("Some votes were not copied to the current patch set");
+ }
+
+ @Test
+ public void repliesDoesntAddOwner_voteNotCopied_userAdded() throws Exception {
+ // Define a label without any copy condition.
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType.Builder fooReview =
+ labelBuilder(
+ "Foo-Review",
+ value(1, "Looks good to me, but someone else must approve"),
+ value(0, "No score"),
+ value(-1, "I would prefer this is not submitted as is"));
+ u.getConfig().upsertLabelType(fooReview.build());
+ u.save();
+ }
+
+ // Allow voting on the label.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabel("Foo-Review")
+ .ref(RefNames.REFS_HEADS + "*")
+ .group(REGISTERED_USERS)
+ .range(-1, 1))
+ .update();
+
+ // Create a change with 2 patch sets.
+ PushOneCommit.Result r = createChange();
+ PatchSet patchSet1 = r.getChange().currentPatchSet();
+ r = amendChange(r.getChangeId());
+ r.assertOkStatus();
+
+ // User is not in the attention set yet
+ assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty();
+
+ // Apply a negative vote on the first patch set which is not copied since the label doesn't have
+ // a copy condition
+ requestScopeOperations.setApiUser(user.id());
+ change(r).revision(patchSet1.number()).review(new ReviewInput().label("Foo-Review", -1));
+
+ // The change owner has not been added to the attention set
+ assertThat(getAttentionSetUpdatesForUser(r, admin)).isEmpty();
+
+ // The user has been added to the attention set because the vote was not copied
+ AttentionSetUpdate attentionSet =
+ Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user));
+ assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id());
+ assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD);
+ assertThat(attentionSet)
+ .hasReasonThat()
+ .isEqualTo("Some votes were not copied to the current patch set");
+ }
+
+ @Test
+ public void repliesAddsOwner_changeMessagePosted() throws Exception {
+ PushOneCommit.Result r = createChange();
+
+ requestScopeOperations.setApiUser(user.id());
ReviewInput reviewInput = new ReviewInput();
+ reviewInput.message = "A message";
change(r).current().review(reviewInput);
+ // The change owner has been added to the attention set
AttentionSetUpdate attentionSet =
Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin));
assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id());
@@ -1003,6 +1213,57 @@ public class AttentionSetIT extends AbstractDaemonTest {
}
@Test
+ public void repliesAddsOwner_commentAdded() throws Exception {
+ PushOneCommit.Result r = createChange();
+
+ requestScopeOperations.setApiUser(user.id());
+ ReviewInput reviewInput = new ReviewInput();
+ ReviewInput.CommentInput comment = new ReviewInput.CommentInput();
+ comment.side = Side.REVISION;
+ comment.path = Patch.COMMIT_MSG;
+ comment.message = "comment";
+ reviewInput.comments = ImmutableMap.of(Patch.COMMIT_MSG, ImmutableList.of(comment));
+ change(r).current().review(reviewInput);
+
+ // The change owner has been added to the attention set
+ AttentionSetUpdate attentionSet =
+ Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin));
+ assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id());
+ assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD);
+ assertThat(attentionSet).hasReasonThat().isEqualTo("Someone else replied on the change");
+ }
+
+ @Test
+ public void repliesAddsOwner_markedAsReady() throws Exception {
+ PushOneCommit.Result r = createChange();
+ change(r).setWorkInProgress();
+
+ requestScopeOperations.setApiUser(accountCreator.admin2().id());
+ ReviewInput reviewInput = ReviewInput.create().setReady(true);
+ change(r).current().review(reviewInput);
+
+ // The change owner has been added to the attention set
+ AttentionSetUpdate attentionSet =
+ Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin));
+ assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id());
+ assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD);
+ assertThat(attentionSet).hasReasonThat().isEqualTo("Someone else replied on the change");
+ }
+
+ @Test
+ public void noOpRepliesDontAddOwner() throws Exception {
+ PushOneCommit.Result r = createChange();
+
+ // post a no-op reply (a reply that does neither add any vote, change message, comment nor
+ // changes the change to ready)
+ requestScopeOperations.setApiUser(user.id());
+ change(r).current().review(new ReviewInput());
+
+ // The change owner has not been added to the attention set
+ assertThat(getAttentionSetUpdatesForUser(r, admin)).isEmpty();
+ }
+
+ @Test
public void robotRepliesDoNotAddToAttentionSet() throws Exception {
PushOneCommit.Result r = createChange();
change(r).addReviewer(user.email());
@@ -1076,18 +1337,16 @@ public class AttentionSetIT extends AbstractDaemonTest {
@Test
public void repliesAddsOwnerAndUploader() throws Exception {
- // Create change with owner: admin
+ // Create change with admin as the owner and user as the uploader
PushOneCommit.Result r = createChange();
r = amendChangeWithUploader(r, project, user);
+ change(r).attention(user.email()).remove(new AttentionSetInput("reason"));
+ // Post a comment by another user
TestAccount user2 = accountCreator.user2();
requestScopeOperations.setApiUser(user2.id());
-
- change(r).attention(user.email()).remove(new AttentionSetInput("reason"));
ReviewInput reviewInput = new ReviewInput();
- change(r).current().review(reviewInput);
-
- reviewInput = new ReviewInput();
+ reviewInput.message = "A message";
change(r).current().review(reviewInput);
// Uploader added
@@ -1105,6 +1364,25 @@ public class AttentionSetIT extends AbstractDaemonTest {
}
@Test
+ public void noOpRepliesDontAddOwnerAndUploader() throws Exception {
+ // Create change with admin as the owner and user as the uploader
+ PushOneCommit.Result r = createChange();
+ r = amendChangeWithUploader(r, project, user);
+ change(r).attention(user.email()).remove(new AttentionSetInput("reason"));
+
+ // Post a no-op reply by another user (a reply that does neither add any vote, change message,
+ // comment nor changes the change to ready)
+ TestAccount user2 = accountCreator.user2();
+ requestScopeOperations.setApiUser(user2.id());
+ ReviewInput reviewInput = new ReviewInput();
+ change(r).current().review(reviewInput);
+
+ // Neither the change owner nor the uploader have been added to the attention set
+ assertThat(getAttentionSetUpdatesForUser(r, admin)).isEmpty();
+ assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty();
+ }
+
+ @Test
public void reviewIgnoresRobotCommentsForAttentionSet() throws Exception {
PushOneCommit.Result r = createChange();
requestScopeOperations.setApiUser(user.id());
@@ -1339,7 +1617,9 @@ public class AttentionSetIT extends AbstractDaemonTest {
requestScopeOperations.setApiUser(user.id());
- change(r).current().review(new ReviewInput());
+ reviewInput = new ReviewInput();
+ reviewInput.message = "A message";
+ change(r).current().review(reviewInput);
// Reviewer and CC not added since the uploader didn't reply to their comments
assertThat(getAttentionSetUpdatesForUser(r, cc)).isEmpty();
@@ -1530,6 +1810,99 @@ public class AttentionSetIT extends AbstractDaemonTest {
}
@Test
+ public void robotReviewWithNegativeLabelOnOutdatedPatchSetAddsOwnerIfVoteWasCopied()
+ throws Exception {
+ // Define a label with a copy condition that copies all votes.
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType.Builder fooReview =
+ labelBuilder(
+ "Foo-Review",
+ value(1, "Looks good to me, but someone else must approve"),
+ value(0, "No score"),
+ value(-1, "I would prefer this is not submitted as is"))
+ .setCopyCondition("is:ANY");
+ u.getConfig().upsertLabelType(fooReview.build());
+ u.save();
+ }
+
+ // Allow voting on the label.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabel("Foo-Review")
+ .ref(RefNames.REFS_HEADS + "*")
+ .group(REGISTERED_USERS)
+ .range(-1, 1))
+ .update();
+
+ // Create a change with 2 patch sets.
+ PushOneCommit.Result r = createChange();
+ PatchSet patchSet1 = r.getChange().currentPatchSet();
+ r = amendChange(r.getChangeId());
+ r.assertOkStatus();
+
+ // Create a service user and apply a negative vote on the first patch set which is copied since
+ // the label has a copy condition that copies all votes
+ TestAccount robot =
+ accountCreator.create(
+ "robot2", "robot2@example.com", "Ro Bot", "Ro", ServiceUserClassifier.SERVICE_USERS);
+ requestScopeOperations.setApiUser(robot.id());
+ change(r).revision(patchSet1.number()).review(new ReviewInput().label("Foo-Review", -1));
+
+ // The change owner has been added to the attention set
+ AttentionSetUpdate attentionSet =
+ Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin));
+ assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id());
+ assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD);
+ assertThat(attentionSet).hasReasonThat().isEqualTo("A robot voted negatively on a label");
+ }
+
+ @Test
+ public void robotReviewWithNegativeLabelOnOutdatedPatchSetDoesntAddOwnerIfVoteWasNotCopied()
+ throws Exception {
+ // Define a label without any copy condition.
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType.Builder fooReview =
+ labelBuilder(
+ "Foo-Review",
+ value(1, "Looks good to me, but someone else must approve"),
+ value(0, "No score"),
+ value(-1, "I would prefer this is not submitted as is"));
+ u.getConfig().upsertLabelType(fooReview.build());
+ u.save();
+ }
+
+ // Allow voting on the label.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabel("Foo-Review")
+ .ref(RefNames.REFS_HEADS + "*")
+ .group(REGISTERED_USERS)
+ .range(-1, 1))
+ .update();
+
+ // Create a change with 2 patch sets.
+ PushOneCommit.Result r = createChange();
+ PatchSet patchSet1 = r.getChange().currentPatchSet();
+ r = amendChange(r.getChangeId());
+ r.assertOkStatus();
+
+ // Create a service user and apply a negative vote on the first patch set which is not copied
+ // since the label doesn't have a copy condition
+ TestAccount robot =
+ accountCreator.create(
+ "robot2", "robot2@example.com", "Ro Bot", "Ro", ServiceUserClassifier.SERVICE_USERS);
+ requestScopeOperations.setApiUser(robot.id());
+ change(r).revision(patchSet1.number()).review(new ReviewInput().label("Foo-Review", -1));
+
+ // The change owner has not been added to the attention set
+ assertThat(getAttentionSetUpdatesForUser(r, admin)).isEmpty();
+ }
+
+ @Test
public void robotReviewWithNegativeLabelDoesntAddOwnerIfChangeIsMerged() throws Exception {
TestAccount robot =
accountCreator.create(
@@ -1835,7 +2208,8 @@ public class AttentionSetIT extends AbstractDaemonTest {
setEmailStrategyForUser(EmailStrategy.ATTENTION_SET_ONLY);
// Ensure emails that don't relate to changes are still sent.
- gApi.accounts().id(user.id().get()).generateHttpPassword();
+ @SuppressWarnings("unused")
+ var unused = gApi.accounts().id(user.id().get()).generateHttpPassword();
assertThat(sender.getMessages()).isNotEmpty();
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeIncludedInIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeIncludedInIT.java
index dd85cb0de6..0dd2c46097 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeIncludedInIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeIncludedInIT.java
@@ -19,20 +19,38 @@ import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.a
import static org.eclipse.jgit.lib.Constants.R_TAGS;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit.Result;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.projects.TagInput;
+import com.google.gerrit.server.change.FilterIncludedIn;
import com.google.inject.Inject;
+import java.util.function.Predicate;
+import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Test;
@NoHttpd
public class ChangeIncludedInIT extends AbstractDaemonTest {
@Inject private ProjectOperations projectOperations;
+ @Inject private ExtensionRegistry extensionRegistry;
+
+ private static class TestFilter implements FilterIncludedIn {
+ @Override
+ public Predicate<String> getBranchFilter(Project.NameKey project, RevCommit commit) {
+ return branch -> !branch.startsWith("t");
+ }
+
+ @Override
+ public Predicate<String> getTagFilter(Project.NameKey project, RevCommit commit) {
+ return tag -> !tag.startsWith("bad");
+ }
+ }
@Test
public void includedInOpenChange() throws Exception {
@@ -41,8 +59,7 @@ public class ChangeIncludedInIT extends AbstractDaemonTest {
assertThat(gApi.changes().id(result.getChangeId()).includedIn().tags).isEmpty();
}
- @Test
- public void includedInMergedChange() throws Exception {
+ private String baseTestCase() throws Exception {
Result result = createChange();
gApi.changes()
.id(result.getChangeId())
@@ -61,12 +78,30 @@ public class ChangeIncludedInIT extends AbstractDaemonTest {
.update();
gApi.projects().name(project.get()).tag("test-tag").create(new TagInput());
- assertThat(gApi.changes().id(result.getChangeId()).includedIn().tags)
- .containsExactly("test-tag");
-
createBranch(BranchNameKey.create(project, "test-branch"));
+ return result.getChangeId();
+ }
- assertThat(gApi.changes().id(result.getChangeId()).includedIn().branches)
+ @Test
+ public void includedInMergedChange() throws Exception {
+ String changeId = baseTestCase();
+ assertThat(gApi.changes().id(changeId).includedIn().tags).containsExactly("test-tag");
+
+ assertThat(gApi.changes().id(changeId).includedIn().branches)
.containsExactly("master", "test-branch");
}
+
+ @Test
+ public void includedInFiltered() throws Exception {
+ String changeId = baseTestCase();
+ gApi.projects().name(project.get()).tag("bad-tag").create(new TagInput());
+ assertThat(gApi.changes().id(changeId).includedIn().tags)
+ .containsExactly("bad-tag", "test-tag");
+ try (ExtensionRegistry.Registration registration =
+ extensionRegistry.newRegistration().add(new TestFilter())) {
+ assertThat(gApi.changes().id(changeId).includedIn().tags).containsExactly("test-tag");
+
+ assertThat(gApi.changes().id(changeId).includedIn().branches).containsExactly("master");
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
index a6f391764d..32b9010fe0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
@@ -16,7 +16,6 @@ package com.google.gerrit.acceptance.rest.change;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_ACCOUNTS;
@@ -53,7 +52,6 @@ import com.google.gerrit.server.util.AccountTemplateUtil;
import com.google.inject.Inject;
import java.nio.charset.Charset;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
@@ -180,7 +178,7 @@ public class ChangeMessagesIT extends AbstractDaemonTest {
messageTemplate,
Iterables.getLast(gApi.changes().id(changeId).get(MESSAGES).messages).message);
- Collection<ChangeMessageInfo> listMessages = gApi.changes().id(changeId).messages();
+ List<ChangeMessageInfo> listMessages = gApi.changes().id(changeId).messages();
assertThat(listMessages).hasSize(2);
ChangeMessageInfo changeMessageApi = Iterables.getLast(gApi.changes().id(changeId).messages());
assertMessage("Review by " + admin.getNameEmail(), changeMessageApi.message);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
index 6a748a5bf9..009ec081f4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
@@ -20,7 +20,6 @@ import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.l
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.TestProjectInput;
@@ -32,6 +31,7 @@ import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.util.ManualRequestContext;
import com.google.inject.Inject;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
@@ -118,11 +118,8 @@ public class ChangeOwnerIT extends AbstractDaemonTest {
}
private void approve(TestAccount a, String changeId) throws Exception {
- Context old = requestScopeOperations.setApiUser(a.id());
- try {
+ try (ManualRequestContext newCtx = requestScopeOperations.setNestedApiUser(a.id())) {
gApi.changes().id(changeId).current().review(ReviewInput.approve());
- } finally {
- atrScope.set(old);
}
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
index 70a3cf2a92..ca3a345e79 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
@@ -16,6 +16,9 @@ package com.google.gerrit.acceptance.rest.change;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
+import static com.google.gerrit.extensions.client.ReviewerState.CC;
+import static com.google.gerrit.extensions.client.ReviewerState.REMOVED;
+import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -34,10 +37,12 @@ import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ReviewerUpdateInfo;
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
import java.lang.reflect.Type;
+import java.util.Iterator;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
@@ -54,7 +59,7 @@ public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
@Test
public void addByEmail() throws Exception {
- AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
+ AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@example.com");
for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
PushOneCommit.Result r = createChange();
@@ -72,8 +77,49 @@ public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
}
@Test
+ public void addByEmailToReviewerUpdateInfo() throws Exception {
+ AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@example.com");
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ ReviewerInput input = new ReviewerInput();
+ input.reviewer = toRfcAddressString(acc);
+ input.state = CC;
+ gApi.changes().id(r.getChangeId()).addReviewer(input);
+
+ input.state = REVIEWER;
+ gApi.changes().id(r.getChangeId()).addReviewer(input);
+
+ adminRestSession.delete("/changes/" + changeId + "/reviewers/" + acc.email).assertNoContent();
+
+ ChangeInfo c = gApi.changes().id(changeId).get();
+ assertThat(c.reviewerUpdates).isNotNull();
+ assertThat(c.reviewerUpdates).hasSize(3);
+
+ Iterator<ReviewerUpdateInfo> it = c.reviewerUpdates.iterator();
+ ReviewerUpdateInfo reviewerUpdateInfo = it.next();
+ assertThat(reviewerUpdateInfo.state).isEqualTo(CC);
+ assertThat(reviewerUpdateInfo.reviewer._accountId).isNull();
+ assertThat(reviewerUpdateInfo.reviewer.email).isEqualTo(acc.email);
+ assertThat(reviewerUpdateInfo.updatedBy._accountId).isEqualTo(admin.id().get());
+
+ reviewerUpdateInfo = it.next();
+ assertThat(reviewerUpdateInfo.state).isEqualTo(REVIEWER);
+ assertThat(reviewerUpdateInfo.reviewer._accountId).isNull();
+ assertThat(reviewerUpdateInfo.reviewer.email).isEqualTo(acc.email);
+ assertThat(reviewerUpdateInfo.updatedBy._accountId).isEqualTo(admin.id().get());
+
+ reviewerUpdateInfo = it.next();
+ assertThat(reviewerUpdateInfo.state).isEqualTo(REMOVED);
+ assertThat(reviewerUpdateInfo.reviewer._accountId).isNull();
+ assertThat(reviewerUpdateInfo.reviewer.email).isEqualTo(acc.email);
+ assertThat(reviewerUpdateInfo.updatedBy._accountId).isEqualTo(admin.id().get());
+ }
+
+ @Test
public void addByEmailAndById() throws Exception {
- AccountInfo byEmail = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
+ AccountInfo byEmail = new AccountInfo("Foo Bar", "foo.bar@example.com");
AccountInfo byId = new AccountInfo(user.id().get());
for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
@@ -98,7 +144,7 @@ public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
@Test
public void listReviewersByEmail() throws Exception {
- AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
+ AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@example.com");
for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
PushOneCommit.Result r = createChange();
@@ -125,7 +171,7 @@ public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
@Test
public void removeByEmail() throws Exception {
- AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
+ AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@example.com");
for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
PushOneCommit.Result r = createChange();
@@ -144,7 +190,7 @@ public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
@Test
public void convertFromCCToReviewer() throws Exception {
- AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
+ AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@example.com");
PushOneCommit.Result r = createChange();
@@ -165,7 +211,7 @@ public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
@Test
public void addedReviewersGetNotified() throws Exception {
- AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
+ AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@example.com");
for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
PushOneCommit.Result r = createChange();
@@ -175,7 +221,7 @@ public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
input.state = state;
gApi.changes().id(r.getChangeId()).addReviewer(input);
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
assertThat(messages.get(0).rcpt()).containsExactly(Address.parse(input.reviewer));
sender.clear();
@@ -184,7 +230,7 @@ public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
@Test
public void removingReviewerTriggersNotification() throws Exception {
- AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
+ AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@example.com");
for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
PushOneCommit.Result r = createChange();
@@ -206,7 +252,7 @@ public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
// Delete as admin
gApi.changes().id(r.getChangeId()).reviewer(addInput.reviewer).remove();
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
assertThat(messages.get(0).rcpt())
.containsExactly(Address.parse(addInput.reviewer), user.getNameEmail());
@@ -216,7 +262,7 @@ public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
@Test
public void reviewerAndCCReceiveRegularNotification() throws Exception {
- AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
+ AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@example.com");
for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
PushOneCommit.Result r = createChange();
@@ -242,7 +288,7 @@ public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
for (int i = 0; i < 10; i++) {
ReviewerInput input = new ReviewerInput();
- input.reviewer = String.format("%s-%s@gerritcodereview.com", state, i);
+ input.reviewer = String.format("%s-%s@example.com", state, i);
input.state = state;
gApi.changes().id(r.getChangeId()).addReviewer(input);
}
@@ -266,7 +312,7 @@ public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
ReviewInput reviewInput = new ReviewInput();
for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
for (int i = 0; i < 10; i++) {
- reviewInput.reviewer(String.format("%s-%s@gerritcodereview.com", state, i), state, true);
+ reviewInput.reviewer(String.format("%s-%s@example.com", state, i), state, true);
}
}
assertThat(reviewInput.reviewers).hasSize(20);
@@ -277,11 +323,19 @@ public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
}
@Test
- public void rejectMissingEmail() throws Exception {
+ public void rejectIfReviewerUserIdentifierIsMissing() throws Exception {
PushOneCommit.Result r = createChange();
- ReviewerResult result = gApi.changes().id(r.getChangeId()).addReviewer("");
- assertThat(result.error).isEqualTo(" is not a valid user identifier");
+ ReviewerResult result = gApi.changes().id(r.getChangeId()).addReviewer((String) null);
+ assertThat(result.error).isEqualTo("reviewer user identifier is required");
+ assertThat(result.reviewers).isNull();
+
+ result = gApi.changes().id(r.getChangeId()).addReviewer("");
+ assertThat(result.error).isEqualTo("reviewer user identifier is required");
+ assertThat(result.reviewers).isNull();
+
+ result = gApi.changes().id(r.getChangeId()).addReviewer(" ");
+ assertThat(result.error).isEqualTo("reviewer user identifier is required");
assertThat(result.reviewers).isNull();
}
@@ -303,18 +357,18 @@ public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
PushOneCommit.Result r = createChange();
ReviewerResult result =
- gApi.changes().id(r.getChangeId()).addReviewer("Foo Bar <foo.bar@gerritcodereview.com>");
+ gApi.changes().id(r.getChangeId()).addReviewer("Foo Bar <foo.bar@example.com>");
assertThat(result.error)
.isEqualTo(
- "Account 'Foo Bar <foo.bar@gerritcodereview.com>' not found\n"
- + "Foo Bar <foo.bar@gerritcodereview.com> does not identify a registered user or"
+ "Account 'Foo Bar <foo.bar@example.com>' not found\n"
+ + "Foo Bar <foo.bar@example.com> does not identify a registered user or"
+ " group");
assertThat(result.reviewers).isNull();
}
@Test
public void reviewersByEmailAreServedFromIndex() throws Exception {
- AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
+ AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@example.com");
for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
PushOneCommit.Result r = createChange();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index 6fc9b2b205..cf88b5a135 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -154,7 +154,7 @@ public class ChangeReviewersIT extends AbstractDaemonTest {
assertReviewers(c, CC, user);
// Verify email was sent to CCed account.
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(user.getNameEmail());
@@ -232,7 +232,7 @@ public class ChangeReviewersIT extends AbstractDaemonTest {
assertReviewers(c, CC, firstUsers);
// Verify emails were sent to each of the group's accounts.
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
List<Address> expectedAddresses = new ArrayList<>(firstUsers.size());
@@ -444,7 +444,7 @@ public class ChangeReviewersIT extends AbstractDaemonTest {
assertReviewers(c, CC, observer);
// Verify emails were sent to added reviewers.
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(2);
Message m = messages.get(0);
@@ -540,7 +540,7 @@ public class ChangeReviewersIT extends AbstractDaemonTest {
}
@Test
- public void addReviewerToReviewerChangeInfo() throws Exception {
+ public void addReviewerToReviewerUpdateInfo() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
ReviewerInput in = new ReviewerInput();
@@ -564,20 +564,20 @@ public class ChangeReviewersIT extends AbstractDaemonTest {
assertThat(c.reviewerUpdates).hasSize(3);
Iterator<ReviewerUpdateInfo> it = c.reviewerUpdates.iterator();
- ReviewerUpdateInfo reviewerChange = it.next();
- assertThat(reviewerChange.state).isEqualTo(CC);
- assertThat(reviewerChange.reviewer._accountId).isEqualTo(user.id().get());
- assertThat(reviewerChange.updatedBy._accountId).isEqualTo(admin.id().get());
-
- reviewerChange = it.next();
- assertThat(reviewerChange.state).isEqualTo(REVIEWER);
- assertThat(reviewerChange.reviewer._accountId).isEqualTo(user.id().get());
- assertThat(reviewerChange.updatedBy._accountId).isEqualTo(admin.id().get());
-
- reviewerChange = it.next();
- assertThat(reviewerChange.state).isEqualTo(REMOVED);
- assertThat(reviewerChange.reviewer._accountId).isEqualTo(user.id().get());
- assertThat(reviewerChange.updatedBy._accountId).isEqualTo(admin.id().get());
+ ReviewerUpdateInfo reviewerUpdateInfo = it.next();
+ assertThat(reviewerUpdateInfo.state).isEqualTo(CC);
+ assertThat(reviewerUpdateInfo.reviewer._accountId).isEqualTo(user.id().get());
+ assertThat(reviewerUpdateInfo.updatedBy._accountId).isEqualTo(admin.id().get());
+
+ reviewerUpdateInfo = it.next();
+ assertThat(reviewerUpdateInfo.state).isEqualTo(REVIEWER);
+ assertThat(reviewerUpdateInfo.reviewer._accountId).isEqualTo(user.id().get());
+ assertThat(reviewerUpdateInfo.updatedBy._accountId).isEqualTo(admin.id().get());
+
+ reviewerUpdateInfo = it.next();
+ assertThat(reviewerUpdateInfo.state).isEqualTo(REMOVED);
+ assertThat(reviewerUpdateInfo.reviewer._accountId).isEqualTo(user.id().get());
+ assertThat(reviewerUpdateInfo.updatedBy._accountId).isEqualTo(admin.id().get());
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index f8eccb5b52..698eac81b7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -51,6 +51,7 @@ import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.converter.ChangeInputProtoConverter;
import com.google.gerrit.extensions.api.accounts.AccountInput;
import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
import com.google.gerrit.extensions.api.changes.ChangeApi;
@@ -82,14 +83,17 @@ import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.restapi.change.ApplyPatchUtil;
+import com.google.gerrit.server.restapi.change.CreateChange;
+import com.google.gerrit.server.restapi.change.CreateChange.CommitTreeSupplier;
import com.google.gerrit.server.submit.ChangeAlreadyMergedException;
+import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.gson.stream.JsonReader;
import com.google.inject.Inject;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.List;
-import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
@@ -111,9 +115,13 @@ import org.junit.Test;
@UseClockStep
public class CreateChangeIT extends AbstractDaemonTest {
+ private static final ChangeInputProtoConverter CHANGE_INPUT_PROTO_CONVERTER =
+ ChangeInputProtoConverter.INSTANCE;
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ExtensionRegistry extensionRegistry;
+ @Inject private CreateChange createChangeImpl;
+ @Inject private BatchUpdate.Factory updateFactory;;
@Before
public void addNonCommitHead() throws Exception {
@@ -371,7 +379,7 @@ public class CreateChangeIT extends AbstractDaemonTest {
requestScopeOperations.setApiUser(admin.id());
assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(user.getNameEmail());
@@ -564,7 +572,7 @@ public class CreateChangeIT extends AbstractDaemonTest {
@Test
public void createChangeWithParentCommit() throws Exception {
- Map<String, PushOneCommit.Result> setup =
+ ImmutableMap<String, PushOneCommit.Result> setup =
changeInTwoBranches("foo", "foo.txt", "bar", "bar.txt");
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.baseCommit = setup.get("master").getCommit().getId().name();
@@ -603,7 +611,7 @@ public class CreateChangeIT extends AbstractDaemonTest {
@Test
public void createChangeWithParentCommitOnWrongBranchFails() throws Exception {
- Map<String, PushOneCommit.Result> setup =
+ ImmutableMap<String, PushOneCommit.Result> setup =
changeInTwoBranches("foo", "foo.txt", "bar", "bar.txt");
ChangeInput input = newChangeInput(ChangeStatus.NEW);
input.branch = "foo";
@@ -638,7 +646,7 @@ public class CreateChangeIT extends AbstractDaemonTest {
@Test
public void createChangeWithoutAccessToParentCommitFails() throws Exception {
- Map<String, PushOneCommit.Result> results =
+ ImmutableMap<String, PushOneCommit.Result> results =
changeInTwoBranches("invisible-branch", "a.txt", "visible-branch", "b.txt");
projectOperations
.project(project)
@@ -1158,6 +1166,26 @@ public class CreateChangeIT extends AbstractDaemonTest {
}
@Test
+ public void createChangeWithCommitTreeSupplier_success() throws Exception {
+ createBranch(BranchNameKey.create(project, "other"));
+ ChangeInput input = newChangeInput(ChangeStatus.NEW);
+ input.branch = "other";
+ input.subject = "custom commit message";
+ ApplyPatchInput applyPatchInput = new ApplyPatchInput();
+ applyPatchInput.patch = PATCH_INPUT;
+ CommitTreeSupplier commitTreeSupplier =
+ (repo, oi, in, mergeTip) ->
+ ApplyPatchUtil.applyPatch(repo, oi, applyPatchInput, mergeTip).getTreeId();
+
+ ChangeInfo info = assertCreateWithCommitTreeSupplierSucceeds(input, commitTreeSupplier);
+
+ DiffInfo diff = gApi.changes().id(info.id).current().file(PATCH_FILE_NAME).diff();
+ assertDiffForNewFile(diff, info.currentRevision, PATCH_FILE_NAME, PATCH_NEW_FILE_CONTENT);
+ assertThat(info.revisions.get(info.currentRevision).commit.message)
+ .isEqualTo("custom commit message\n\nChange-Id: " + info.changeId + "\n");
+ }
+
+ @Test
@UseSystemTime
public void sha1sOfTwoNewChangesDiffer() throws Exception {
ChangeInput changeInput = newChangeInput(ChangeStatus.NEW);
@@ -1352,6 +1380,18 @@ public class CreateChangeIT extends AbstractDaemonTest {
return out;
}
+ private ChangeInfo assertCreateWithCommitTreeSupplierSucceeds(
+ ChangeInput input, CommitTreeSupplier commitTreeSupplier) throws Exception {
+ ChangeInfo res =
+ createChangeImpl
+ .execute(updateFactory, CHANGE_INPUT_PROTO_CONVERTER.toProto(input), commitTreeSupplier)
+ .value();
+ // The original result doesn't contain any revision data.
+ ChangeInfo out = gApi.changes().id(res.changeId).get(ALL_REVISIONS, CURRENT_COMMIT);
+ validateCreateSucceeds(input, out);
+ return out;
+ }
+
private static <T> T readContentFromJson(RestResponse r, Class<T> clazz) throws Exception {
try (JsonReader jsonReader = new JsonReader(r.getReader())) {
return newGson().fromJson(jsonReader, clazz);
@@ -1470,7 +1510,7 @@ public class CreateChangeIT extends AbstractDaemonTest {
* @param fileB name of file to commit to branchB
* @return A {@code Map} of branchName => commit result.
*/
- private Map<String, Result> changeInTwoBranches(
+ private ImmutableMap<String, Result> changeInTwoBranches(
String branchA, String fileA, String branchB, String fileB) throws Exception {
return changeInTwoBranches(
branchA, "change A", fileA, "A content", branchB, "change B", fileB, "B content");
@@ -1489,7 +1529,7 @@ public class CreateChangeIT extends AbstractDaemonTest {
* @param contentB file content to commit to branchB
* @return A {@code Map} of branchName => commit result.
*/
- private Map<String, Result> changeInTwoBranches(
+ private ImmutableMap<String, Result> changeInTwoBranches(
String branchA,
String subjectA,
String fileA,
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java b/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
index 6491202571..b1e8ba13f4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
@@ -22,6 +22,7 @@ import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.b
import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -42,7 +43,6 @@ import com.google.gerrit.testing.FakeEmailSender;
import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
import java.util.Collection;
-import java.util.List;
import java.util.Map;
import org.junit.Test;
@@ -192,7 +192,7 @@ public class DeleteVoteIT extends AbstractDaemonTest {
RestResponse response = userRestSession.delete(deleteAdminVoteEndPoint);
response.assertNoContent();
- List<FakeEmailSender.Message> messages = sender.getMessages();
+ ImmutableList<FakeEmailSender.Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
FakeEmailSender.Message msg = messages.get(0);
assertThat(msg.rcpt()).containsExactly(admin.getNameEmail(), user2.getNameEmail());
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
index d5c7610059..7cc72af11e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
@@ -333,7 +333,7 @@ public class MoveChangeIT extends AbstractDaemonTest {
assertLabelVote(user, changeId, testLabelA, (short) 1);
requestScopeOperations.setApiUser(admin.id());
- assertThat(atrScope.get().getUser().getAccountId()).isEqualTo(admin.id());
+ assertThat(localCtx.getContext().getUser().getAccountId()).isEqualTo(admin.id());
// Move the change to the destination branch.
assertThat(info(changeId).branch).isEqualTo(sourceBranch.shortName());
@@ -396,7 +396,7 @@ public class MoveChangeIT extends AbstractDaemonTest {
assertLabelVote(user, changeId, testLabelA, (short) 2);
requestScopeOperations.setApiUser(admin.id());
- assertThat(atrScope.get().getUser().getAccountId()).isEqualTo(admin.id());
+ assertThat(localCtx.getContext().getUser().getAccountId()).isEqualTo(admin.id());
// Move the change to the destination branch.
assertThat(info(changeId).branch).isEqualTo(sourceBranch.shortName());
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
index a60f75735b..1d8e0b8af2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -27,6 +27,7 @@ import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.server.change.MergeabilityComputationBehavior;
import com.google.inject.Inject;
import java.util.Map;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -157,7 +158,9 @@ public class SubmitByFastForwardIT extends AbstractSubmit {
assertThat(actions).containsKey("submit");
ActionInfo info = actions.get("submit");
- assertThat(info.enabled).isNull();
+ if (mcb != MergeabilityComputationBehavior.NEVER) {
+ assertThat(info.enabled).isNull();
+ }
submitWithConflict(
change2.getChangeId(),
@@ -226,8 +229,10 @@ public class SubmitByFastForwardIT extends AbstractSubmit {
approve(changeResult.getChangeId());
approve(change2Result.getChangeId());
- // submit button is disabled.
- assertSubmitDisabled(change2Result.getChangeId());
+ if (mcb != MergeabilityComputationBehavior.NEVER) {
+ // submit button is disabled.
+ assertSubmitDisabled(change2Result.getChangeId());
+ }
submitWithConflict(
change2Result.getChangeId(),
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index ac3622f5d8..dccc057e4e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -26,6 +26,7 @@ import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.entities.BranchNameKey;
@@ -42,6 +43,7 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.inject.Inject;
import java.util.List;
import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.RefSpec;
import org.junit.Test;
@@ -280,6 +282,10 @@ public class SubmitByMergeIfNecessaryIT extends AbstractSubmitByMerge {
}
@Test
+ @GerritConfig(
+ name = "experiments.disabled",
+ // The test intentionally create an implicit merge change.
+ value = "GerritBackendFeature__reject_implicit_merges_on_merge")
public void submitWithMergedAncestorsOnOtherBranch() throws Throwable {
RevCommit initialHead = projectOperations.project(project).getHead("master");
@@ -329,6 +335,10 @@ public class SubmitByMergeIfNecessaryIT extends AbstractSubmitByMerge {
}
@Test
+ @GerritConfig(
+ name = "experiments.disabled",
+ // The test intentionally create an implicit merge change.
+ value = "GerritBackendFeature__reject_implicit_merges_on_merge")
public void submitWithOpenAncestorsOnOtherBranch() throws Throwable {
RevCommit initialHead = projectOperations.project(project).getHead("master");
PushOneCommit.Result change1 =
@@ -549,19 +559,27 @@ public class SubmitByMergeIfNecessaryIT extends AbstractSubmitByMerge {
PushOneCommit.Result changeResult = change.to("refs/for/master");
approve(changeResult.getChangeId());
- // Create a successor change.
+ // Create a destination branch that later will be made non-visible to user.
+ BranchNameKey secretBranch = BranchNameKey.create(project, "secretBranch");
+ String secretBranchTip =
+ gApi.projects()
+ .name(secretBranch.project().get())
+ .branch(secretBranch.branch())
+ .create(new BranchInput())
+ .get()
+ .revision;
+
+ // Create a successor change which merges visible and non-visible branch. This change
+ // is created as an explicit merge - otherwise Gerrit rejects it on submit as implicit merge.
PushOneCommit change2 =
pushFactory.create(admin.newIdent(), testRepo, "feature", "b.txt", "bar");
+ change2.setParents(
+ List.of(
+ changeResult.getCommit(), repo().parseCommit(ObjectId.fromString(secretBranchTip))));
PushOneCommit.Result change2Result = change2.to("refs/for/master");
-
- // Move the first change to a destination branch that is non-visible to user so that user cannot
- // this change anymore.
- BranchNameKey secretBranch = BranchNameKey.create(project, "secretBranch");
- gApi.projects()
- .name(secretBranch.project().get())
- .branch(secretBranch.branch())
- .create(new BranchInput());
gApi.changes().id(changeResult.getChangeId()).move(secretBranch.branch());
+
+ // Hide branch from the user so that user cannot this change anymore.
projectOperations
.project(project)
.forUpdate()
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index 6dfa82b926..d3a622f7e4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -26,6 +26,7 @@ import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.util.stream.Collectors.toList;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.TestAccount;
@@ -48,7 +49,6 @@ import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.inject.Inject;
import java.util.List;
import java.util.Locale;
-import java.util.Set;
import java.util.stream.IntStream;
import org.junit.Before;
import org.junit.Test;
@@ -476,17 +476,17 @@ public class SuggestReviewersIT extends AbstractDaemonTest {
String name = name("foo");
TestAccount foo1 = accountCreator.create(name + "-1");
reviewChange(changeIdReviewed, foo1);
- assertThat(gApi.accounts().id(foo1.username()).getActive()).isTrue();
+ assertThat(gApi.accounts().id(foo1.id().get()).getActive()).isTrue();
TestAccount foo2 = accountCreator.create(name + "-2");
reviewChange(changeIdReviewed, foo2);
- assertThat(gApi.accounts().id(foo2.username()).getActive()).isTrue();
+ assertThat(gApi.accounts().id(foo2.id().get()).getActive()).isTrue();
assertReviewers(
suggestReviewers(changeId, name), ImmutableList.of(foo1, foo2), ImmutableList.of());
requestScopeOperations.setApiUser(user.id());
- gApi.accounts().id(foo2.username()).setActive(false);
+ gApi.accounts().id(foo2.id().get()).setActive(false);
assertThat(gApi.accounts().id(foo2.id().get()).getActive()).isFalse();
assertReviewers(suggestReviewers(changeId, name), ImmutableList.of(foo1), ImmutableList.of());
assertReviewers(
@@ -654,7 +654,7 @@ public class SuggestReviewersIT extends AbstractDaemonTest {
}
private AccountGroup.UUID createGroupWithArbitraryMembers(int numMembers) {
- Set<Account.Id> members =
+ ImmutableSet<Account.Id> members =
IntStream.rangeClosed(1, numMembers)
.mapToObj(i -> accountOperations.newAccount().create())
.collect(toImmutableSet());
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/BUILD b/javatests/com/google/gerrit/acceptance/rest/config/BUILD
index 8550423841..7a168411a6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/config/BUILD
@@ -6,5 +6,6 @@ acceptance_tests(
labels = ["rest"],
deps = [
"//java/com/google/gerrit/server/restapi",
+ "//lib/lucene:lucene-core",
],
)
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java b/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
index 164f683f22..03c17cf30e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
@@ -33,7 +33,8 @@ public class FlushCacheIT extends AbstractDaemonTest {
@Test
public void flushCache() throws Exception {
// access the admin group once so that it is loaded into the group cache
- adminGroup();
+ @SuppressWarnings("unused")
+ var unused = adminGroup();
RestResponse r = adminRestSession.get("/config/server/caches/groups_byname");
CacheInfo result = newGson().fromJson(r.getReader(), CacheInfo.class);
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/GetTaskIT.java b/javatests/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
index a9e3cf666e..9ed6d151fe 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
@@ -33,7 +33,7 @@ public class GetTaskIT extends AbstractDaemonTest {
TaskInfo info = newGson().fromJson(r.getReader(), new TypeToken<TaskInfo>() {}.getType());
assertThat(info.id).isNotNull();
Long.parseLong(info.id, 16);
- assertThat(info.command).isEqualTo("Log File Compressor");
+ assertThat(info.command).isEqualTo("Log File Manager");
assertThat(info.startTime).isNotNull();
}
@@ -49,7 +49,7 @@ public class GetTaskIT extends AbstractDaemonTest {
newGson().fromJson(r.getReader(), new TypeToken<List<TaskInfo>>() {}.getType());
r.consume();
for (TaskInfo info : result) {
- if ("Log File Compressor".equals(info.command)) {
+ if ("Log File Manager".equals(info.command)) {
return info.id;
}
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/IndexSnapshotsIT.java b/javatests/com/google/gerrit/acceptance/rest/config/IndexSnapshotsIT.java
new file mode 100644
index 0000000000..904de9a487
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/config/IndexSnapshotsIT.java
@@ -0,0 +1,228 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.acceptance.rest.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.index.Index;
+import com.google.gerrit.index.IndexType;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.config.IndexResource;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.account.AccountIndexDefinition;
+import com.google.gerrit.server.index.change.ChangeIndexDefinition;
+import com.google.gerrit.server.index.group.GroupIndexDefinition;
+import com.google.gerrit.server.index.project.ProjectIndexDefinition;
+import com.google.gerrit.server.restapi.config.SnapshotIndex;
+import com.google.gerrit.server.restapi.config.SnapshotIndexes;
+import com.google.gerrit.server.restapi.config.SnapshotInfo;
+import com.google.gerrit.testing.SystemPropertiesTestRule;
+import com.google.inject.Inject;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collection;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+public class IndexSnapshotsIT extends AbstractDaemonTest {
+
+ @ClassRule
+ public static SystemPropertiesTestRule systemProperties =
+ new SystemPropertiesTestRule(IndexType.SYS_PROP, "lucene");
+
+ @Inject private SnapshotIndex snapshotIndex;
+ @Inject private SnapshotIndexes snapshotIndexes;
+ @Inject private AccountIndexDefinition accountIndexDefinition;
+ @Inject private ChangeIndexDefinition changeIndexDefinition;
+ @Inject private GroupIndexDefinition groupIndexDefinition;
+ @Inject private ProjectIndexDefinition projectIndexDefinition;
+
+ @Inject private SitePaths sitePaths;
+
+ @Test
+ @UseLocalDisk
+ public void createAccountsIndexSnapshot() throws Exception {
+ Query query = new TermQuery(new Term("is", "active"));
+ createAndVerifySnapshot(new IndexResource(accountIndexDefinition), "accounts", query);
+ }
+
+ @Test
+ @UseLocalDisk
+ public void createFullSnapshot() throws Exception {
+ File snapshot = createSnapshotOfAllIndexes();
+ File[] members = snapshot.listFiles();
+ for (File member : members) {
+ assertThat(member.isDirectory()).isTrue();
+ verifyIndexCanBeOpen(member);
+ }
+ }
+
+ @Test
+ @UseLocalDisk
+ public void createChangesIndexSnapshot() throws Exception {
+ Query query = new TermQuery(new Term("status", "open"));
+ createAndVerifySnapshot(new IndexResource(changeIndexDefinition), "changes", query);
+ }
+
+ @Test
+ @UseLocalDisk
+ public void createGroupsIndexSnapshot() throws Exception {
+ Query query = new TermQuery(new Term("is", "active"));
+ createAndVerifySnapshot(new IndexResource(groupIndexDefinition), "groups", query);
+ }
+
+ @Test
+ @UseLocalDisk
+ public void createProjectsIndexSnapshot() throws Exception {
+ Query query = new TermQuery(new Term("name", "foo"));
+ createAndVerifySnapshot(new IndexResource(projectIndexDefinition), "projects", query);
+ }
+
+ private File createAndVerifySnapshot(IndexResource rsrc, String prefix, Query query)
+ throws IOException {
+ File snapshot = createSnapshot(rsrc);
+
+ File[] subdirs = snapshot.listFiles();
+ Collection<? extends Index<?, ?>> indexes =
+ rsrc.getIndexDefinition().getIndexCollection().getWriteIndexes();
+ assertThat(subdirs).hasLength(indexes.size());
+ for (Index<?, ?> i : indexes) {
+ String indexDirName = String.format("%s_%04d", prefix, i.getSchema().getVersion());
+ File[] result = snapshot.listFiles((d, n) -> n.equals(indexDirName));
+ assertThat(result).hasLength(1);
+ File accountsIndexSnapshot = result[0];
+ openIndexAndQuery(accountsIndexSnapshot, query);
+ }
+ return snapshot;
+ }
+
+ private File createSnapshot(IndexResource rsrc) throws IOException {
+ Response<?> rsp = snapshotIndex.apply(rsrc, new SnapshotIndex.Input());
+ return verifySnapshot(rsp);
+ }
+
+ private File createSnapshotOfAllIndexes() throws IOException {
+ Response<?> rsp = snapshotIndexes.apply(new ConfigResource(), new SnapshotIndexes.Input());
+ return verifySnapshot(rsp);
+ }
+
+ private File verifySnapshot(Response<?> rsp) {
+ assertThat(rsp.value()).isInstanceOf(SnapshotInfo.class);
+ SnapshotInfo snapshotInfo = (SnapshotInfo) rsp.value();
+ Path snapshotDir = sitePaths.index_dir.resolve("snapshots").resolve(snapshotInfo.id);
+ File snapshot = snapshotDir.toFile();
+ assertThat(snapshot.exists()).isTrue();
+ assertThat(snapshot.isDirectory()).isTrue();
+ return snapshot;
+ }
+
+ private void verifyIndexCanBeOpen(File indexDir) throws IOException {
+ createIndex(indexDir).tryOpen();
+ }
+
+ private void openIndexAndQuery(File indexDir, Query query) throws IOException {
+ BaseIndex index = createIndex(indexDir);
+ index.openAndQuery(query);
+ }
+
+ private BaseIndex createIndex(File indexDir) {
+ BaseIndex index;
+ if (indexDir.getName().startsWith("changes")) {
+ index = new ChangeIndex(indexDir);
+ } else {
+ index = new SimpleIndex(indexDir);
+ }
+ return index;
+ }
+
+ private abstract static class BaseIndex {
+ protected File indexDir;
+
+ BaseIndex(File indexDir) {
+ this.indexDir = indexDir;
+ }
+
+ abstract void tryOpen() throws IOException;
+
+ abstract void openAndQuery(Query query) throws IOException;
+ }
+
+ private static class SimpleIndex extends BaseIndex {
+ SimpleIndex(File indexDir) {
+ super(indexDir);
+ }
+
+ @Override
+ void tryOpen() throws IOException {
+ Directory index = FSDirectory.open(indexDir.toPath());
+ try (IndexReader reader = DirectoryReader.open(index)) {}
+ }
+
+ @Override
+ void openAndQuery(Query query) throws IOException {
+ Directory index = FSDirectory.open(indexDir.toPath());
+ try (IndexReader reader = DirectoryReader.open(index)) {
+ IndexSearcher searcher = new IndexSearcher(reader);
+ TopDocs result = searcher.search(query, 10);
+ System.out.printf("query result length = %d\n", result.scoreDocs.length);
+ }
+ }
+ }
+
+ private static class ChangeIndex extends BaseIndex {
+ private SimpleIndex open;
+ private SimpleIndex closed;
+
+ ChangeIndex(File indexDir) {
+ super(indexDir);
+ File[] subDirs = indexDir.listFiles();
+ for (File subDir : subDirs) {
+ String name = subDir.getName();
+ if (name.equals("open")) {
+ this.open = new SimpleIndex(subDir);
+ } else if (name.equals("closed")) {
+ this.closed = new SimpleIndex(subDir);
+ } else {
+ throw new IllegalStateException("Unexpected subdir in changes index " + name);
+ }
+ }
+ }
+
+ @Override
+ void tryOpen() throws IOException {
+ open.tryOpen();
+ closed.tryOpen();
+ }
+
+ @Override
+ void openAndQuery(Query query) throws IOException {
+ open.openAndQuery(query);
+ closed.openAndQuery(query);
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/KillTaskIT.java b/javatests/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
index 2a891aad0a..ab3689bc2c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
@@ -15,7 +15,6 @@
package com.google.gerrit.acceptance.rest.config;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static java.util.stream.Collectors.toSet;
import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -37,7 +36,7 @@ public class KillTaskIT extends AbstractDaemonTest {
Optional<String> id =
result.stream()
- .filter(t -> "Log File Compressor".equals(t.command))
+ .filter(t -> "Log File Manager".equals(t.command))
.map(t -> t.id)
.findFirst();
assertThat(id).isPresent();
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ListTasksIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ListTasksIT.java
index 674ca7927c..cad0875ba3 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ListTasksIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ListTasksIT.java
@@ -34,7 +34,7 @@ public class ListTasksIT extends AbstractDaemonTest {
assertThat(result).isNotEmpty();
boolean foundLogFileCompressorTask = false;
for (TaskInfo info : result) {
- if ("Log File Compressor".equals(info.command)) {
+ if ("Log File Manager".equals(info.command)) {
foundLogFileCompressorTask = true;
}
assertThat(info.id).isNotNull();
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index d45c90b858..03af621af5 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -15,7 +15,6 @@
package com.google.gerrit.acceptance.rest.project;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
import static com.google.gerrit.acceptance.GitUtil.assertPushRejected;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
@@ -127,7 +126,7 @@ public class CreateBranchIT extends AbstractDaemonTest {
.add(
new RefOperationValidationListener() {
@Override
- public List<ValidationMessage> onRefOperation(RefReceivedEvent refEvent)
+ public ImmutableList<ValidationMessage> onRefOperation(RefReceivedEvent refEvent)
throws ValidationException {
try (Repository repo = repoManager.openRepository(project)) {
RefUpdate u = repo.updateRef(testBranch.branch());
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java
index 7b42d93fbd..1f7454d6c3 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java
@@ -15,7 +15,6 @@
package com.google.gerrit.acceptance.rest.project;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.entities.RefNames.REFS_HEADS;
import com.google.gerrit.acceptance.AbstractDaemonTest;
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index 59e23a9f59..755581c679 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -16,7 +16,6 @@ package com.google.gerrit.acceptance.rest.project;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectInfo;
import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectOwners;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
@@ -351,7 +350,7 @@ public class CreateProjectIT extends AbstractDaemonTest {
@GerritConfig(name = "gerrit.defaultBranch", value = "main")
public void createProject_WhenDefaultBranchIsSet() throws Exception {
String newProjectName = name("newProject");
- gApi.projects().create(newProjectName).get();
+ gApi.projects().create(newProjectName);
ImmutableMap<String, BranchInfo> branches = getProjectBranches(newProjectName);
// HEAD symbolic ref is set to the default, but the actual ref is not created.
assertThat(branches.keySet()).containsExactly("HEAD", "refs/meta/config");
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index d8b0cb14d7..f7fa8d9d15 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -113,7 +113,9 @@ public class DeleteBranchIT extends AbstractDaemonTest {
RestResponse r =
userRestSession.delete("/projects/" + project.get() + "/branches/" + testBranch.branch());
r.assertNotFound();
- branch(testBranch).get();
+
+ @SuppressWarnings("unused")
+ var unused = branch(testBranch).get();
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteLabelIT.java
index 491a0d593e..6a8c9d809e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteLabelIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteLabelIT.java
@@ -120,6 +120,7 @@ public class DeleteLabelIT extends AbstractDaemonTest {
amendChange(changeId);
// Assert no throws.
- gApi.changes().id(changeId).get(DETAILED_LABELS);
+ @SuppressWarnings("unused")
+ var unused = gApi.changes().id(changeId).get(DETAILED_LABELS);
}
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/GetBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/GetBranchIT.java
index cf3bf89914..a05f0994b0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/GetBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/GetBranchIT.java
@@ -83,7 +83,7 @@ public class GetBranchIT extends AbstractDaemonTest {
}
@Test
- public void cannotGetNonVisibleBranch() {
+ public void cannotGetNonVisibleBranch() throws Exception {
String branchName = "master";
// block read access to the branch
@@ -98,7 +98,7 @@ public class GetBranchIT extends AbstractDaemonTest {
}
@Test
- public void cannotGetNonVisibleBranchByShortName() {
+ public void cannotGetNonVisibleBranchByShortName() throws Exception {
String branchName = "master";
// block read access to the branch
@@ -441,7 +441,7 @@ public class GetBranchIT extends AbstractDaemonTest {
}
@Test
- public void cannotGetSymbolicRefThatPointsToNonVisibleBranch() {
+ public void cannotGetSymbolicRefThatPointsToNonVisibleBranch() throws Exception {
// block read access to the branch to which HEAD points by default
projectOperations
.project(project)
@@ -562,8 +562,7 @@ public class GetBranchIT extends AbstractDaemonTest {
assertBranchNotFound(project, RefNames.refsCacheAutomerge(mergeRevision));
}
- private void testGetRefWithAccessDatabase(Project.NameKey project, String ref)
- throws RestApiException {
+ private void testGetRefWithAccessDatabase(Project.NameKey project, String ref) throws Exception {
projectOperations
.project(allProjects)
.forUpdate()
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
index 21a4c98d12..191444fc88 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -37,7 +37,6 @@ import com.google.gerrit.server.restapi.project.ListBranches;
import com.google.gerrit.server.restapi.project.ProjectsCollection;
import com.google.inject.Inject;
import com.google.inject.Provider;
-import java.util.List;
import org.junit.Test;
@NoHttpd
@@ -290,7 +289,7 @@ public class ListBranchesIT extends AbstractDaemonTest {
listBranches.setNextPageToken(ListBranches.encodeToken("refs/heads/someBranch1"));
Response<ImmutableList<BranchInfo>> response =
listBranches.apply(projects.parse(project.get()));
- List<String> continuationToken =
+ ImmutableList<String> continuationToken =
response.headers().get(ListBranches.NEXT_PAGE_TOKEN_HEADER).asList();
// Since branch1 does not exist, the server continues from branch2.
assertRefs(ImmutableList.of(branch2), response.value());
@@ -307,7 +306,7 @@ public class ListBranchesIT extends AbstractDaemonTest {
listBranches.setNextPageToken(ListBranches.encodeToken("refs/heads/someBranch4"));
Response<ImmutableList<BranchInfo>> response =
listBranches.apply(projects.parse(project.get()));
- List<String> continuationToken =
+ ImmutableList<String> continuationToken =
response.headers().get(ListBranches.NEXT_PAGE_TOKEN_HEADER).asList();
// Since branch1 does not exist, the server continues from branch2.
assertRefs(ImmutableList.of(), response.value());
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListLabelsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListLabelsIT.java
index 7a717d1851..463f9cf2aa 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListLabelsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListLabelsIT.java
@@ -192,7 +192,8 @@ public class ListLabelsIT extends AbstractDaemonTest {
requestScopeOperations.setApiUser(user.id());
// can list labels without inheritance
- gApi.projects().name(project.get()).labels().get();
+ @SuppressWarnings("unused")
+ var unused = gApi.projects().name(project.get()).labels().get();
// cannot list labels with inheritance
AuthException thrown =
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index de9b57962a..0a13935c2a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -350,7 +350,8 @@ public class ListProjectsIT extends AbstractDaemonTest {
assertThat(info.state).isEqualTo(input.state);
// Project is still accessible directly
- gApi.projects().name(hidden.get()).get();
+ @SuppressWarnings("unused")
+ var unused = gApi.projects().name(hidden.get()).get();
// Hidden project is not included in the list
assertThatNameList(gApi.projects().list().get())
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ProjectAssert.java b/javatests/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
index 3f583a220a..f2679580f7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
@@ -26,12 +26,11 @@ import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.common.ProjectInfo;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.server.project.ProjectState;
-import java.util.List;
import java.util.Set;
public class ProjectAssert {
public static IterableSubject assertThatNameList(Iterable<ProjectInfo> actualIt) {
- List<ProjectInfo> actual = ImmutableList.copyOf(actualIt);
+ ImmutableList<ProjectInfo> actual = ImmutableList.copyOf(actualIt);
for (ProjectInfo info : actual) {
assertWithMessage("missing project name").that(info.name).isNotNull();
assertWithMessage("project name does not match id")
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/SuggestBranchReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/project/SuggestBranchReviewersIT.java
index 98f5716a47..8311d96f80 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/SuggestBranchReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/SuggestBranchReviewersIT.java
@@ -150,15 +150,15 @@ public class SuggestBranchReviewersIT extends AbstractDaemonTest {
String name = name("foo");
TestAccount foo1 = accountCreator.create(name + "-1");
- assertThat(gApi.accounts().id(foo1.username()).getActive()).isTrue();
+ assertThat(gApi.accounts().id(foo1.id().get()).getActive()).isTrue();
TestAccount foo2 = accountCreator.create(name + "-2");
- assertThat(gApi.accounts().id(foo2.username()).getActive()).isTrue();
+ assertThat(gApi.accounts().id(foo2.id().get()).getActive()).isTrue();
assertReviewers(suggestReviewers(name), ImmutableList.of(foo1, foo2), ImmutableList.of());
requestScopeOperations.setApiUser(user.id());
- gApi.accounts().id(foo2.username()).setActive(false);
+ gApi.accounts().id(foo2.id().get()).setActive(false);
assertThat(gApi.accounts().id(foo2.id().get()).getActive()).isFalse();
assertReviewers(suggestReviewers(name), ImmutableList.of(foo1), ImmutableList.of());
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
index 795e22c817..e2809b0b1a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -23,9 +23,11 @@ import static org.eclipse.jgit.lib.Constants.R_TAGS;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.entities.AccessSection;
@@ -34,6 +36,7 @@ import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
import com.google.gerrit.extensions.api.projects.TagApi;
import com.google.gerrit.extensions.api.projects.TagInfo;
import com.google.gerrit.extensions.api.projects.TagInput;
+import com.google.gerrit.extensions.common.ListTagSortOption;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -51,7 +54,7 @@ import org.junit.Test;
@NoHttpd
public class TagsIT extends AbstractDaemonTest {
- private static final List<String> testTags =
+ private static final ImmutableList<String> testTags =
ImmutableList.of("tag-A", "tag-B", "tag-C", "tag-D", "tag-E", "tag-F", "tag-G", "tag-H");
private static final String SIGNED_ANNOTATION =
@@ -119,6 +122,7 @@ public class TagsIT extends AbstractDaemonTest {
}
@Test
+ @UseClockStep
public void listTags() throws Exception {
createTags();
@@ -155,6 +159,23 @@ public class TagsIT extends AbstractDaemonTest {
// With conflicting options
assertBadRequest(getTags().withSubstring("ag-B").withRegex("^tag-[c|d]$"));
+
+ // with descending order
+ result = getTags().withDescendingOrder(true).get();
+ assertTagList(FluentIterable.from(Lists.reverse(testTags)), result);
+
+ // with sortBy creation time
+ result = getTags().withSortBy(ListTagSortOption.CREATION_TIME).get();
+ assertTagList(FluentIterable.from(Lists.reverse(testTags)), result);
+
+ // with sortBy, descending order and limit
+ result =
+ getTags()
+ .withDescendingOrder(true)
+ .withLimit(2)
+ .withSortBy(ListTagSortOption.CREATION_TIME)
+ .get();
+ assertTagList(FluentIterable.from(ImmutableList.of("tag-A", "tag-B")), result);
}
@Test
@@ -476,9 +497,10 @@ public class TagsIT extends AbstractDaemonTest {
TagInput input = new TagInput();
input.revision = revision;
- for (String tagname : testTags) {
+ // Creating the tags in reverse order to allow testing the sortBy option
+ for (String tagname : Lists.reverse(testTags)) {
+ input.message = tagname; // This updates the 'created' time of the tag
TagInfo result = tag(tagname).create(input).get();
- assertThat(result.revision).isEqualTo(input.revision);
assertThat(result.ref).isEqualTo(R_TAGS + tagname);
}
}
diff --git a/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java b/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
index b150491541..2476f00250 100644
--- a/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
@@ -16,7 +16,6 @@ package com.google.gerrit.acceptance.server.account;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.base.Splitter;
@@ -106,7 +105,7 @@ public class AccountResolverIT extends AbstractDaemonTest {
gApi.accounts().id(user.id().get()).setActive(false);
requestScopeOperations.setApiUser(user.id());
- assertThat(gApi.accounts().id("self").getActive()).isFalse();
+ assertThat(gApi.accounts().self().getActive()).isFalse();
Result result = resolveAsResult("self");
assertThat(result.asIdSet()).containsExactly(user.id());
diff --git a/javatests/com/google/gerrit/acceptance/server/change/ApprovalCopierIT.java b/javatests/com/google/gerrit/acceptance/server/change/ApprovalCopierIT.java
index 379a712d7b..6bfd9884ce 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/ApprovalCopierIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/ApprovalCopierIT.java
@@ -33,7 +33,7 @@ import com.google.common.truth.FailureMetadata;
import com.google.common.truth.StandardSubjectBuilder;
import com.google.common.truth.StringSubject;
import com.google.common.truth.Subject;
-import com.google.common.truth.Truth8;
+import com.google.common.truth.Truth;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
@@ -49,9 +49,9 @@ import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.client.ChangeKind;
-import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.approval.ApprovalCopier;
import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.update.RepoView;
import com.google.gerrit.truth.ListSubject;
import com.google.gerrit.truth.NullAwareCorrespondence;
import com.google.inject.Inject;
@@ -59,6 +59,8 @@ import java.io.IOException;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.Before;
@@ -122,10 +124,12 @@ public class ApprovalCopierIT extends AbstractDaemonTest {
public void forInitialPatchSet_noApprovals() throws Exception {
ChangeData changeData = createChange().getChange();
try (Repository repo = repoManager.openRepository(project);
- RevWalk revWalk = new RevWalk(repo)) {
+ ObjectInserter ins = repo.newObjectInserter();
+ ObjectReader reader = ins.newReader();
+ RevWalk revWalk = new RevWalk(reader)) {
ApprovalCopier.Result approvalCopierResult =
approvalCopier.forPatchSet(
- changeData.notes(), changeData.currentPatchSet(), revWalk, repo.getConfig());
+ changeData.notes(), changeData.currentPatchSet(), new RepoView(repo, revWalk, ins));
assertThat(approvalCopierResult.copiedApprovals()).isEmpty();
assertThat(approvalCopierResult.outdatedApprovals()).isEmpty();
}
@@ -436,7 +440,7 @@ public class ApprovalCopierIT extends AbstractDaemonTest {
}
private void vote(String changeId, TestAccount testAccount, String label, int value)
- throws RestApiException {
+ throws Exception {
requestScopeOperations.setApiUser(testAccount.id());
gApi.changes().id(changeId).current().review(new ReviewInput().label(label, value));
requestScopeOperations.setApiUser(admin.id());
@@ -453,9 +457,11 @@ public class ApprovalCopierIT extends AbstractDaemonTest {
ChangeData changeData = changeDataFactory.create(project, changeId);
assertThat(changeData.currentPatchSet().id().get()).isEqualTo(expectedCurrentPatchSetNum);
try (Repository repo = repoManager.openRepository(project);
- RevWalk revWalk = new RevWalk(repo)) {
+ ObjectInserter ins = repo.newObjectInserter();
+ ObjectReader reader = ins.newReader();
+ RevWalk revWalk = new RevWalk(reader)) {
return approvalCopier.forPatchSet(
- changeData.notes(), changeData.currentPatchSet(), revWalk, repo.getConfig());
+ changeData.notes(), changeData.currentPatchSet(), new RepoView(repo, revWalk, ins));
}
}
@@ -483,7 +489,7 @@ public class ApprovalCopierIT extends AbstractDaemonTest {
approvalData.patchSetApproval().label().equals(labelId)
&& approvalData.patchSetApproval().accountId().equals(accountId))
.findAny();
- Truth8.assertThat(approvalDataForLabelAndAccount).isPresent();
+ Truth.assertThat(approvalDataForLabelAndAccount).isPresent();
return assertAbout(approvalDatas()).that(approvalDataForLabelAndAccount.get());
}
diff --git a/javatests/com/google/gerrit/acceptance/server/change/BUILD b/javatests/com/google/gerrit/acceptance/server/change/BUILD
index 4514ea36d5..33dfe67a2f 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/BUILD
+++ b/javatests/com/google/gerrit/acceptance/server/change/BUILD
@@ -13,8 +13,9 @@ acceptance_tests(
java_library(
name = "util",
+ testonly = 1,
srcs = ["CommentsUtil.java"],
- visibility = ["//javatests/com/google/gerrit/acceptance/api/change:__subpackages__"],
+ visibility = ["//visibility:public"],
deps = [
"//java/com/google/gerrit/acceptance:lib",
"//java/com/google/gerrit/entities",
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 7938ab9021..a6cdfa19f7 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -16,9 +16,9 @@ package com.google.gerrit.acceptance.server.change;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.acceptance.server.change.CommentsUtil.createRange;
import static com.google.gerrit.entities.Patch.COMMIT_MSG;
import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
@@ -32,6 +32,7 @@ import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.ExtensionRegistry;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.change.TestHumanComment;
@@ -63,6 +64,7 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
import com.google.gerrit.server.notedb.ChangeNoteUtil;
import com.google.gerrit.server.notedb.DeleteCommentRewriter;
import com.google.gerrit.server.restapi.change.ChangesCollection;
@@ -119,7 +121,7 @@ public class CommentsIT extends AbstractDaemonTest {
private final Integer[] lines = {0, 1};
@Before
- public void setUp() {
+ public void setUp() throws Exception {
requestScopeOperations.setApiUser(user.id());
}
@@ -155,6 +157,130 @@ public class CommentsIT extends AbstractDaemonTest {
}
@Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ values = {ExperimentFeaturesConstants.ALLOW_FIX_SUGGESTIONS_IN_COMMENTS})
+ public void createDraftWithFixSuggestions() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ String revId = r.getCommit().getName();
+ String path = "file1";
+ DraftInput comment = CommentsUtil.newDraft(path, Side.REVISION, 0, "comment 1");
+ comment.fixSuggestions =
+ ImmutableList.of(
+ CommentsUtil.createFixSuggestionInfo(CommentsUtil.createFixReplacementInfo()));
+ addDraft(changeId, revId, comment);
+ Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
+ assertThat(result).hasSize(1);
+ CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
+ // FixId is generated, use the one provided by the server.
+ comment.fixSuggestions.get(0).fixId = actual.fixSuggestions.get(0).fixId;
+ assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
+
+ actual =
+ Iterables.getOnlyElement(
+ Iterables.getOnlyElement(gApi.changes().id(changeId).drafts().values()));
+ comment.fixSuggestions.get(0).fixId = actual.fixSuggestions.get(0).fixId;
+ assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
+
+ List<CommentInfo> list = getDraftCommentsAsList(changeId);
+ assertThat(list).hasSize(1);
+ actual = list.get(0);
+ assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
+
+ // Publish draft comment
+ ReviewInput reviewInput = new ReviewInput();
+ reviewInput.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
+ reviewInput.message = "bar";
+ gApi.changes().id(changeId).current().review(reviewInput);
+
+ actual = gApi.changes().id(changeId).commentsRequest().getAsList().get(0);
+ assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ values = {ExperimentFeaturesConstants.ALLOW_FIX_SUGGESTIONS_IN_COMMENTS})
+ public void createDraftWithoutFixSuggestionsThenUpdateWithFixSuggestions() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ String revId = r.getCommit().getName();
+ String path = "file1";
+ DraftInput comment = CommentsUtil.newDraft(path, Side.REVISION, 0, "comment 1");
+ addDraft(changeId, revId, comment);
+ Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
+ assertThat(result).hasSize(1);
+ CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
+ // FixId is generated, use the one provided by the server.
+ assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
+
+ List<CommentInfo> list = getDraftCommentsAsList(changeId);
+ assertThat(list).hasSize(1);
+ actual = list.get(0);
+ assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
+
+ // Try to update draft comment
+ comment.fixSuggestions =
+ ImmutableList.of(
+ CommentsUtil.createFixSuggestionInfo(CommentsUtil.createFixReplacementInfo()));
+ updateDraft(changeId, revId, comment, actual.id);
+
+ // FixId is generated, use the one provided by the server.
+ actual =
+ Iterables.getOnlyElement(
+ Iterables.getOnlyElement(gApi.changes().id(changeId).drafts().values()));
+ comment.fixSuggestions.get(0).fixId = actual.fixSuggestions.get(0).fixId;
+ assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
+
+ list = getDraftCommentsAsList(changeId);
+ assertThat(list).hasSize(1);
+ actual = list.get(0);
+ assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
+
+ // Publish draft comment
+ ReviewInput reviewInput = new ReviewInput();
+ reviewInput.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
+ reviewInput.message = "bar";
+ gApi.changes().id(changeId).current().review(reviewInput);
+
+ actual = gApi.changes().id(changeId).commentsRequest().getAsList().get(0);
+ assertThat(comment).isEqualTo(infoToDraft(path).apply(actual));
+ }
+
+ @Test
+ public void createDraftWithFixSuggestionsFailsWithoutExperimentFlag() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ String revId = r.getCommit().getName();
+ String path = "file1";
+ DraftInput comment = CommentsUtil.newDraft(path, Side.REVISION, 0, "comment 1");
+ comment.fixSuggestions =
+ ImmutableList.of(
+ CommentsUtil.createFixSuggestionInfo(CommentsUtil.createFixReplacementInfo()));
+ IllegalStateException thrown =
+ assertThrows(IllegalStateException.class, () -> addDraft(changeId, revId, comment));
+ assertThat(thrown).hasMessageThat().contains("feature flag prohibits setting fixSuggestions");
+ }
+
+ @Test
+ public void createDraftWithFixInvalidSuggestions() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ String revId = r.getCommit().getName();
+ String path = "file1";
+ DraftInput comment = CommentsUtil.newDraft(path, Side.REVISION, 0, "comment 1");
+ comment.fixSuggestions =
+ ImmutableList.of(
+ CommentsUtil.createFixSuggestionInfo(CommentsUtil.createFixReplacementInfo()));
+ // Invalid range
+ comment.fixSuggestions.get(0).replacements.get(0).range = createRange(13, 9, 5, 10);
+ BadRequestException thrown =
+ assertThrows(BadRequestException.class, () -> addDraft(changeId, revId, comment));
+ assertThat(thrown).hasMessageThat().contains("Range (13:9 - 5:10)");
+ }
+
+ @Test
public void fireEventsForOperationsOnDrafts() throws Exception {
TestGitReferenceUpdatedListener listener = new TestGitReferenceUpdatedListener();
requestScopeOperations.setApiUser(user.id());
@@ -1078,7 +1204,10 @@ public class CommentsIT extends AbstractDaemonTest {
ChangeResource changeRsrc =
changes.get().parse(TopLevelResource.INSTANCE, IdString.fromDecoded(changeId));
RevisionResource revRsrc = revisions.parse(changeRsrc, IdString.fromDecoded(revId));
- postReview.get().apply(revRsrc, input, timestamp);
+
+ @SuppressWarnings("unused")
+ var unused = postReview.get().apply(revRsrc, input, timestamp);
+
Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
assertThat(result).isNotEmpty();
CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
@@ -2233,6 +2362,7 @@ public class CommentsIT extends AbstractDaemonTest {
DraftInput draftInput = new DraftInput();
draftInput.path = path;
draftInput.unresolved = info.unresolved;
+ draftInput.fixSuggestions = info.fixSuggestions;
copy(info, draftInput);
return draftInput;
};
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsUtil.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsUtil.java
index f32cf32730..e25ae7483f 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsUtil.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsUtil.java
@@ -27,6 +27,8 @@ import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.client.Comment;
import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.FixReplacementInfo;
+import com.google.gerrit.extensions.common.FixSuggestionInfo;
import java.util.Arrays;
import java.util.HashMap;
@@ -190,4 +192,31 @@ public class CommentsUtil {
in.omitDuplicateComments = omitDuplicateComments;
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
}
+
+ public static FixSuggestionInfo createFixSuggestionInfo(
+ FixReplacementInfo... fixReplacementInfos) {
+ FixSuggestionInfo newFixSuggestionInfo = new FixSuggestionInfo();
+ newFixSuggestionInfo.fixId = "An ID which must be overwritten.";
+ newFixSuggestionInfo.description = "A description for a suggested fix.";
+ newFixSuggestionInfo.replacements = Arrays.asList(fixReplacementInfos);
+ return newFixSuggestionInfo;
+ }
+
+ public static FixReplacementInfo createFixReplacementInfo() {
+ FixReplacementInfo newFixReplacementInfo = new FixReplacementInfo();
+ newFixReplacementInfo.path = FILE_NAME;
+ newFixReplacementInfo.replacement = "some replacement code";
+ newFixReplacementInfo.range = createRange(3, 9, 8, 4);
+ return newFixReplacementInfo;
+ }
+
+ public static Comment.Range createRange(
+ int startLine, int startCharacter, int endLine, int endCharacter) {
+ Comment.Range range = new Comment.Range();
+ range.startLine = startLine;
+ range.startCharacter = startCharacter;
+ range.endLine = endLine;
+ range.endCharacter = endCharacter;
+ return range;
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/server/experiments/ExperimentFeaturesIT.java b/javatests/com/google/gerrit/acceptance/server/experiments/ExperimentFeaturesIT.java
index e011ffc053..e172153786 100644
--- a/javatests/com/google/gerrit/acceptance/server/experiments/ExperimentFeaturesIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/experiments/ExperimentFeaturesIT.java
@@ -29,6 +29,11 @@ public class ExperimentFeaturesIT extends AbstractDaemonTest {
@Inject ExperimentFeatures experimentFeatures;
+ @Override
+ public boolean enableExperimentsRejectImplicitMergesOnMerge() {
+ return false;
+ }
+
@Test
public void emptyConfig_defaultFeatures_enabled() {
for (String defaultFeature : ExperimentFeaturesConstants.DEFAULT_ENABLED_FEATURES) {
diff --git a/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsChangeIdValidationIT.java b/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsChangeIdValidationIT.java
index b2836fdf27..6945329d5c 100644
--- a/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsChangeIdValidationIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsChangeIdValidationIT.java
@@ -14,8 +14,13 @@
package com.google.gerrit.acceptance.server.git.receive;
+import static com.google.common.truth.Truth.assertThat;
+
import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.config.GerritConfig;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Test;
@@ -24,10 +29,7 @@ public class ReceiveCommitsChangeIdValidationIT extends AbstractDaemonTest {
@Test
public void disallowTruncatingChangeIdAcrossPatchSets() throws Exception {
- // Create the parent.
- RevCommit parent =
- commitBuilder().add("foo.txt", "foo content").message("base commit").create();
- testRepo.reset(parent);
+ RevCommit parent = createParentCommit();
String changeId = "I0000000000000000000000000000000000000012";
String truncatedChangeId = "I000000000000000000000000000000000000001";
@@ -55,4 +57,49 @@ public class ReceiveCommitsChangeIdValidationIT extends AbstractDaemonTest {
.to("refs/for/master")
.assertErrorStatus("invalid Change-Id");
}
+
+ @Test
+ public void pushWithMissingChangeId_rejectedWithDefaultCommitMessageHook() throws Exception {
+ createParentCommit();
+ PushOneCommit.Result pushResult =
+ pushFactory
+ .create(admin.newIdent(), testRepo, /* insertChangeIdIfNotExist= */ false)
+ .to("refs/for/master");
+ String missingChangeIdRegex =
+ "^commit [a-z0-9]+: missing Change-Id in message footer[\\s\\S]+"
+ + "Hint: to automatically insert a Change-Id, install the hook:\n"
+ + "f=\"\\$\\(git rev-parse --git-dir\\)/hooks/commit-msg\"; "
+ + "curl -o \"\\$f\" "
+ + "http://localhost:[0-9]+/tools/hooks/commit-msg ; "
+ + "chmod \\+x \"\\$f\"\n"
+ + "and then amend the commit:\n"
+ + " git commit --amend --no-edit\n"
+ + "Finally, push your changes again\n\n$";
+ assertThat(pushResult.getMessage()).matches(missingChangeIdRegex);
+ }
+
+ @Test
+ @GerritConfig(name = "gerrit.installCommitMsgHookCommand", value = "Install custom hook")
+ public void pushWithMissingChangeId_rejectedWithCustomCommitMessageHook() throws Exception {
+ createParentCommit();
+ PushOneCommit.Result pushResult =
+ pushFactory
+ .create(admin.newIdent(), testRepo, /* insertChangeIdIfNotExist= */ false)
+ .to("refs/for/master");
+ String missingChangeIdRegex =
+ "^commit [a-z0-9]+: missing Change-Id in message footer[\\s\\S]+"
+ + "Hint: to automatically insert a Change-Id, install the hook:\n"
+ + "Install custom hook\n"
+ + "and then amend the commit:\n"
+ + " git commit --amend --no-edit\n"
+ + "Finally, push your changes again\n\n$";
+ assertThat(pushResult.getMessage()).matches(missingChangeIdRegex);
+ }
+
+ @CanIgnoreReturnValue
+ private RevCommit createParentCommit() throws Exception {
+ RevCommit parent = commitBuilder().add("f.txt", "content").message("base commit").create();
+ testRepo.reset(parent);
+ return parent;
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java b/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java
index 83a01535e2..807fd5bc67 100644
--- a/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java
@@ -48,7 +48,6 @@ import com.google.gerrit.testing.FakeEmailSender;
import com.google.gerrit.testing.TestCommentHelper;
import com.google.inject.Inject;
import com.google.inject.Module;
-import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
@@ -131,7 +130,7 @@ public class ReceiveCommitsCommentValidationIT extends AbstractDaemonTest {
testCommentHelper.addDraft(changeId, revId, comment);
amendChange(changeId, "refs/for/master%publish-comments", admin, testRepo);
- List<FakeEmailSender.Message> messages = sender.getMessages();
+ ImmutableList<FakeEmailSender.Message> messages = sender.getMessages();
assertThat(messages).hasSize(2);
FakeEmailSender.Message newPatchsetMessage = messages.get(0);
diff --git a/javatests/com/google/gerrit/acceptance/server/index/BUILD b/javatests/com/google/gerrit/acceptance/server/index/BUILD
new file mode 100644
index 0000000000..1d4ef02f6a
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/index/BUILD
@@ -0,0 +1,7 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+ srcs = glob(["**/*IT.java"]),
+ group = "server_index",
+ labels = ["server"],
+)
diff --git a/javatests/com/google/gerrit/acceptance/server/index/ReindexIndexVersionIT.java b/javatests/com/google/gerrit/acceptance/server/index/ReindexIndexVersionIT.java
new file mode 100644
index 0000000000..d433ca7c21
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/index/ReindexIndexVersionIT.java
@@ -0,0 +1,87 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.acceptance.server.index;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.extensions.events.ChangeIndexedListener;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.index.Index;
+import com.google.gerrit.index.IndexDefinition;
+import com.google.gerrit.server.config.IndexVersionResource;
+import com.google.gerrit.server.restapi.config.ReindexIndexVersion;
+import com.google.inject.Inject;
+import java.util.Collection;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ReindexIndexVersionIT extends AbstractDaemonTest {
+
+ @Inject private ReindexIndexVersion reindexIndexVersion;
+ @Inject private Collection<IndexDefinition<?, ?, ?>> indexDefs;
+ @Inject private ExtensionRegistry extensionRegistry;
+
+ private IndexDefinition<?, ?, ?> def;
+ private Index<?, ?> changeIndex;
+ private Change.Id C1;
+ private Change.Id C2;
+
+ private ChangeIndexedListener changeIndexedListener;
+ private ReindexIndexVersion.Input input = new ReindexIndexVersion.Input();
+
+ @Before
+ public void setUp() throws Exception {
+ def = indexDefs.stream().filter(i -> i.getName().equals("changes")).findFirst().get();
+ changeIndex = def.getIndexCollection().getSearchIndex();
+ C1 = createChange().getChange().getId();
+ C2 = createChange().getChange().getId();
+ changeIndexedListener = mock(ChangeIndexedListener.class);
+ input = new ReindexIndexVersion.Input();
+ }
+
+ @Test
+ public void reindexWithListenerNotification() throws Exception {
+ input.notifyListeners = true;
+ reindex();
+ verify(changeIndexedListener, times(1)).onChangeIndexed(project.get(), C1.get());
+ verify(changeIndexedListener, times(1)).onChangeIndexed(project.get(), C2.get());
+ }
+
+ @Test
+ public void reindexWithoutListenerNotification() throws Exception {
+ input.notifyListeners = false;
+ reindex();
+ verifyNoInteractions(changeIndexedListener);
+ }
+
+ private void reindex() throws ResourceNotFoundException {
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(changeIndexedListener)) {
+ Response<?> rsp =
+ reindexIndexVersion.apply(new IndexVersionResource(def, changeIndex), input);
+ assertThat(rsp.statusCode()).isEqualTo(HttpServletResponse.SC_ACCEPTED);
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/index/change/LuceneChangeIndexerIT.java b/javatests/com/google/gerrit/acceptance/server/index/change/LuceneChangeIndexerIT.java
new file mode 100644
index 0000000000..b8af367a4b
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/index/change/LuceneChangeIndexerIT.java
@@ -0,0 +1,137 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.acceptance.server.index.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ChangeIndexedCounter;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.index.IndexDefinition;
+import com.google.gerrit.index.RefState;
+import com.google.gerrit.index.SiteIndexer.Result;
+import com.google.gerrit.server.index.change.AllChangesIndexer;
+import com.google.gerrit.server.index.change.ChangeIndex;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Inject;
+import java.util.Collection;
+import java.util.Set;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Before;
+import org.junit.Test;
+
+public class LuceneChangeIndexerIT extends AbstractDaemonTest {
+ @ConfigSuite.Default
+ public static Config defaultConfig() {
+ Config cfg = new Config();
+ cfg.setBoolean("index", null, "autoReindexIfStale", false);
+ cfg.setString("index", null, "type", "lucene");
+ return cfg;
+ }
+
+ @Inject private ExtensionRegistry extensionRegistry;
+
+ @Inject private Collection<IndexDefinition<?, ?, ?>> indexDefs;
+ private AllChangesIndexer allChangesIndexer;
+ private ChangeIndex index;
+
+ @Before
+ public void setup() {
+ IndexDefinition<?, ?, ?> changeIndex =
+ indexDefs.stream().filter(i -> i.getName().equals("changes")).findFirst().get();
+ allChangesIndexer = (AllChangesIndexer) changeIndex.getSiteIndexer();
+ index = (ChangeIndex) changeIndex.getIndexCollection().getWriteIndexes().iterator().next();
+ }
+
+ @Test
+ @GerritConfig(name = "index.reuseExistingDocuments", value = "false")
+ public void testReindexWithoutReuse() throws Exception {
+ ChangeIndexedCounter changeIndexedCounter = new ChangeIndexedCounter();
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(changeIndexedCounter)) {
+ createChange();
+ assertThat(changeIndexedCounter.getTotalCount()).isEqualTo(1);
+ changeIndexedCounter.clear();
+ reindexChanges();
+ assertThat(changeIndexedCounter.getTotalCount()).isEqualTo(1);
+
+ createIndexWithMissingChangeAndReindex(changeIndexedCounter);
+ assertThat(changeIndexedCounter.getTotalCount()).isEqualTo(2);
+
+ createIndexWithStaleChangeAndReindex(changeIndexedCounter);
+ assertThat(changeIndexedCounter.getTotalCount()).isEqualTo(3);
+ }
+ }
+
+ @Test
+ @GerritConfig(name = "index.reuseExistingDocuments", value = "true")
+ public void testReindexWithReuse() throws Exception {
+ ChangeIndexedCounter changeIndexedCounter = new ChangeIndexedCounter();
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(changeIndexedCounter)) {
+ createChange();
+ assertThat(changeIndexedCounter.getTotalCount()).isEqualTo(1);
+ changeIndexedCounter.clear();
+ reindexChanges();
+ assertThat(changeIndexedCounter.getTotalCount()).isEqualTo(0);
+
+ createIndexWithMissingChangeAndReindex(changeIndexedCounter);
+ assertThat(changeIndexedCounter.getTotalCount()).isEqualTo(1);
+
+ createIndexWithStaleChangeAndReindex(changeIndexedCounter);
+ assertThat(changeIndexedCounter.getTotalCount()).isEqualTo(1);
+ }
+ }
+
+ private void createIndexWithMissingChangeAndReindex(ChangeIndexedCounter changeIndexedCounter)
+ throws Exception {
+ PushOneCommit.Result res = createChange();
+ index.delete(res.getChange().getId());
+ changeIndexedCounter.clear();
+ reindexChanges();
+ }
+
+ private void createIndexWithStaleChangeAndReindex(ChangeIndexedCounter changeIndexedCounter)
+ throws Exception {
+ PushOneCommit.Result res = createChange();
+ ChangeData wrongChangeData = res.getChange();
+ ListMultimap<NameKey, RefState> refStates =
+ LinkedListMultimap.create(wrongChangeData.getRefStates());
+ refStates.replaceValues(
+ project,
+ Set.of(
+ RefState.create(
+ "refs/changes/abcd",
+ ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))));
+ wrongChangeData.setRefStates(ImmutableSetMultimap.copyOf(refStates));
+ index.replace(wrongChangeData);
+ changeIndexedCounter.clear();
+ reindexChanges();
+ }
+
+ private void reindexChanges() throws Exception {
+ Result res = allChangesIndexer.indexAll(index);
+ assertThat(res.success()).isTrue();
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index b606271fe4..2ee5360072 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -294,11 +294,7 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
TestAccount reviewer = accountCreator.create("added", "added@example.com", "added", null);
addReviewer(adder, sc.changeId, sc.owner, reviewer.email());
// TODO(logan): Should CCs be included?
- assertThat(sender)
- .sent("newchange", sc)
- .to(reviewer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
- .noOneElse();
+ assertThat(sender).sent("newchange", sc).to(reviewer).noOneElse();
assertThat(sender).didNotSend();
}
@@ -336,12 +332,7 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
TestAccount reviewer = accountCreator.create("added", "added@example.com", "added", null);
addReviewer(adder, sc.changeId, sc.owner, reviewer.email(), CC_ON_OWN_COMMENTS, null);
// TODO(logan): Should CCs be included?
- assertThat(sender)
- .sent("newchange", sc)
- .to(reviewer)
- .cc(sc.owner)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
- .noOneElse();
+ assertThat(sender).sent("newchange", sc).to(reviewer).cc(sc.owner).noOneElse();
assertThat(sender).didNotSend();
}
@@ -361,11 +352,7 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
TestAccount reviewer = accountCreator.create("added", "added@example.com", "added", null);
addReviewer(adder, sc.changeId, other, reviewer.email());
// TODO(logan): Should CCs be included?
- assertThat(sender)
- .sent("newchange", sc)
- .to(reviewer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
- .noOneElse();
+ assertThat(sender).sent("newchange", sc).to(reviewer).noOneElse();
assertThat(sender).didNotSend();
}
@@ -385,12 +372,7 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
TestAccount reviewer = accountCreator.create("added", "added@example.com", "added", null);
addReviewer(adder, sc.changeId, other, reviewer.email(), CC_ON_OWN_COMMENTS, null);
// TODO(logan): Should CCs be included?
- assertThat(sender)
- .sent("newchange", sc)
- .to(reviewer)
- .cc(other)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
- .noOneElse();
+ assertThat(sender).sent("newchange", sc).to(reviewer).cc(other).noOneElse();
assertThat(sender).didNotSend();
}
@@ -409,11 +391,7 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
StagedChange sc = stageReviewableChange();
addReviewer(adder, sc.changeId, sc.owner, email);
// TODO(logan): Should CCs be included?
- assertThat(sender)
- .sent("newchange", sc)
- .to(email)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
- .noOneElse();
+ assertThat(sender).sent("newchange", sc).to(email).noOneElse();
assertThat(sender).didNotSend();
}
@@ -464,11 +442,7 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
// For a review-started WIP change, same as in the notify=ALL case. It's not especially
// important to notify just because a reviewer is added, but we do want to notify in the other
// case that hits this codepath: posting an actual review.
- assertThat(sender)
- .sent("newchange", sc)
- .to(reviewer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
- .noOneElse();
+ assertThat(sender).sent("newchange", sc).to(reviewer).noOneElse();
}
private void addReviewerToWipChangeNotifyAll(Adder adder) throws Exception {
@@ -476,11 +450,7 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
TestAccount reviewer = accountCreator.create("added", "added@example.com", "added", null);
addReviewer(adder, sc.changeId, sc.owner, reviewer.email(), NotifyHandling.ALL);
// TODO(logan): Should CCs be included?
- assertThat(sender)
- .sent("newchange", sc)
- .to(reviewer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
- .noOneElse();
+ assertThat(sender).sent("newchange", sc).to(reviewer).noOneElse();
assertThat(sender).didNotSend();
}
@@ -499,11 +469,7 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
TestAccount reviewer = accountCreator.create("added", "added@example.com", "added", null);
addReviewer(adder, sc.changeId, sc.owner, reviewer.email(), OWNER_REVIEWERS);
// TODO(logan): Should CCs be included?
- assertThat(sender)
- .sent("newchange", sc)
- .to(reviewer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
- .noOneElse();
+ assertThat(sender).sent("newchange", sc).to(reviewer).noOneElse();
assertThat(sender).didNotSend();
}
@@ -556,11 +522,7 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
private void addNonUserReviewerByEmail(Adder adder) throws Exception {
StagedChange sc = stageReviewableChange();
addReviewer(adder, sc.changeId, sc.owner, "nonexistent@example.com");
- assertThat(sender)
- .sent("newchange", sc)
- .to("nonexistent@example.com")
- .cc(StagedUsers.CC_BY_EMAIL, StagedUsers.REVIEWER_BY_EMAIL)
- .noOneElse();
+ assertThat(sender).sent("newchange", sc).to("nonexistent@example.com").noOneElse();
assertThat(sender).didNotSend();
}
@@ -577,11 +539,7 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
private void addNonUserCcByEmail(Adder adder) throws Exception {
StagedChange sc = stageReviewableChange();
addReviewer(adder, sc.changeId, sc.owner, "nonexistent@example.com");
- assertThat(sender)
- .sent("newchange", sc)
- .cc("nonexistent@example.com")
- .cc(StagedUsers.CC_BY_EMAIL, StagedUsers.REVIEWER_BY_EMAIL)
- .noOneElse();
+ assertThat(sender).sent("newchange", sc).cc("nonexistent@example.com").noOneElse();
assertThat(sender).didNotSend();
}
@@ -1023,11 +981,7 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
.bcc(ALL_COMMENTS)
.noOneElse();
// TODO(logan): Should CCs be included?
- assertThat(sender)
- .sent("newchange", sc)
- .to(other)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
- .noOneElse();
+ assertThat(sender).sent("newchange", sc).to(other).noOneElse();
assertThat(sender).didNotSend();
}
@@ -1932,7 +1886,6 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
.sent("newpatchset", sc)
.to(sc.reviewer)
.cc(sc.ccer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
@@ -1948,7 +1901,6 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
.notTo(sc.owner) // TODO(logan): This shouldn't be sent *from* the owner.
.to(sc.reviewer, other)
.cc(sc.ccer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
@@ -1964,7 +1916,6 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
.notTo(sc.owner) // TODO(logan): This shouldn't be sent *from* the owner.
.to(sc.reviewer, other)
.cc(sc.ccer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
@@ -1981,7 +1932,6 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
.to(sc.reviewer)
.cc(other)
.cc(sc.ccer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
.noOneElse();
assertThat(sender).didNotSend();
}
@@ -1997,7 +1947,6 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
.to(sc.reviewer)
.cc(other)
.cc(sc.ccer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
.noOneElse();
assertThat(sender).didNotSend();
}
@@ -2056,7 +2005,6 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
.sent("newpatchset", sc)
.to(sc.reviewer)
.cc(sc.ccer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
@@ -2071,7 +2019,6 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
.sent("newpatchset", sc)
.to(sc.reviewer)
.cc(sc.ccer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
@@ -2094,7 +2041,6 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
.sent("newpatchset", sc)
.to(sc.reviewer, newReviewer)
.cc(sc.ccer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
@@ -2118,7 +2064,6 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
.sent("newpatchset", sc)
.to(sc.reviewer, newReviewer)
.cc(sc.ccer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
@@ -2133,7 +2078,6 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
.sent("newpatchset", sc)
.to(sc.reviewer)
.cc(sc.ccer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
@@ -2170,7 +2114,6 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
.sent("newpatchset", sc)
.to(sc.reviewer)
.cc(sc.ccer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
@@ -2185,7 +2128,6 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
.sent("newpatchset", sc)
.to(sc.owner, sc.reviewer)
.cc(sc.ccer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
@@ -2200,7 +2142,6 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
.sent("newpatchset", sc)
.to(sc.owner, sc.reviewer, other)
.cc(sc.ccer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
@@ -2211,12 +2152,7 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
public void editCommitMessageByOtherOnReviewableChangeNotifyOwnerReviewers() throws Exception {
StagedChange sc = stageReviewableChange();
editCommitMessage(sc, other, OWNER_REVIEWERS);
- assertThat(sender)
- .sent("newpatchset", sc)
- .to(sc.owner, sc.reviewer)
- .cc(sc.ccer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
- .noOneElse();
+ assertThat(sender).sent("newpatchset", sc).to(sc.owner, sc.reviewer).cc(sc.ccer).noOneElse();
assertThat(sender).didNotSend();
}
@@ -2229,7 +2165,6 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
.sent("newpatchset", sc)
.to(sc.owner, sc.reviewer)
.cc(sc.ccer, other)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
.noOneElse();
assertThat(sender).didNotSend();
}
@@ -2295,7 +2230,6 @@ public class ChangeNotificationsIT extends AbstractNotificationTest {
.sent("newpatchset", sc)
.to(sc.reviewer)
.cc(sc.ccer)
- .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
index 1ad27eb670..d7f19aa43d 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
@@ -16,6 +16,7 @@ package com.google.gerrit.acceptance.server.mail;
import static com.google.common.truth.Truth.assertThat;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
@@ -35,7 +36,6 @@ import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
import org.junit.Test;
@@ -50,7 +50,7 @@ public class MailMetadataIT extends AbstractDaemonTest {
PushOneCommit.Result newChange = createChange();
gApi.changes().id(newChange.getChangeId()).addReviewer(user.id().toString());
- List<FakeEmailSender.Message> emails = sender.getMessages();
+ ImmutableList<FakeEmailSender.Message> emails = sender.getMessages();
assertThat(emails).hasSize(1);
FakeEmailSender.Message message = emails.get(0);
@@ -87,7 +87,7 @@ public class MailMetadataIT extends AbstractDaemonTest {
gApi.changes().id(newChange.getChangeId()).get().messages;
assertThat(result).isNotEmpty();
- List<FakeEmailSender.Message> emails = sender.getMessages();
+ ImmutableList<FakeEmailSender.Message> emails = sender.getMessages();
assertThat(emails).hasSize(1);
FakeEmailSender.Message message = emails.get(0);
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
index 5e00230c67..6197132064 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
@@ -17,6 +17,7 @@ package com.google.gerrit.acceptance.server.mail;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.collect.ImmutableMap;
import com.google.gerrit.acceptance.Sandboxed;
import com.google.gerrit.acceptance.UseLocalDisk;
import com.google.gerrit.acceptance.config.GerritConfig;
@@ -35,14 +36,14 @@ public class MailSenderIT extends AbstractMailIT {
@Inject private SitePaths sitePaths;
@Test
- @GerritConfig(name = "sendemail.replyToAddress", value = "custom@gerritcodereview.com")
+ @GerritConfig(name = "sendemail.replyToAddress", value = "custom@example.com")
@GerritConfig(name = "receiveemail.protocol", value = "POP3")
public void outgoingMailHasCustomReplyToHeader() throws Exception {
createChangeWithReview(user);
// Check that the custom address was added as Reply-To
assertThat(sender.getMessages()).hasSize(1);
- Map<String, EmailHeader> headers = sender.getMessages().iterator().next().headers();
- assertThat(headerString(headers, "Reply-To")).isEqualTo("custom@gerritcodereview.com");
+ ImmutableMap<String, EmailHeader> headers = sender.getMessages().iterator().next().headers();
+ assertThat(headerString(headers, "Reply-To")).isEqualTo("custom@example.com");
}
@Test
@@ -50,7 +51,7 @@ public class MailSenderIT extends AbstractMailIT {
createChangeWithReview(user);
// Check that the user's email was added as Reply-To
assertThat(sender.getMessages()).hasSize(1);
- Map<String, EmailHeader> headers = sender.getMessages().iterator().next().headers();
+ ImmutableMap<String, EmailHeader> headers = sender.getMessages().iterator().next().headers();
assertThat(headerString(headers, "Reply-To")).contains(user.email());
}
@@ -59,7 +60,7 @@ public class MailSenderIT extends AbstractMailIT {
String changeId = createChangeWithReview(user);
// Check that the mail has the expected headers
assertThat(sender.getMessages()).hasSize(1);
- Map<String, EmailHeader> headers = sender.getMessages().iterator().next().headers();
+ ImmutableMap<String, EmailHeader> headers = sender.getMessages().iterator().next().headers();
String hostname = URI.create(canonicalWebUrl.get()).getHost();
String listId = String.format("<gerrit-%s.%s>", project.get(), hostname);
String unsubscribeLink = String.format("<%ssettings?usp=email>", canonicalWebUrl.get());
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java b/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java
index 65b1d4ff96..f17013d0c4 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java
@@ -39,7 +39,7 @@ public class NotificationMailFormatIT extends AbstractDaemonTest {
// Set user preference to receive only plaintext content
GeneralPreferencesInfo i = new GeneralPreferencesInfo();
i.emailFormat = EmailFormat.PLAINTEXT;
- gApi.accounts().id(admin.id().toString()).setPreferences(i);
+ gApi.accounts().id(admin.id().get()).setPreferences(i);
// Create change as admin and review as user
PushOneCommit.Result r = createChange();
@@ -57,7 +57,7 @@ public class NotificationMailFormatIT extends AbstractDaemonTest {
// Reset user preference
requestScopeOperations.setApiUser(admin.id());
i.emailFormat = EmailFormat.HTML_PLAINTEXT;
- gApi.accounts().id(admin.id().toString()).setPreferences(i);
+ gApi.accounts().id(admin.id().get()).setPreferences(i);
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
index fc746adcea..3cc88f9ade 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
@@ -16,7 +16,6 @@ package com.google.gerrit.acceptance.server.notedb;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
diff --git a/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java b/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java
index 7a55ecbfd2..9488c5f49a 100644
--- a/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java
@@ -53,7 +53,6 @@ import com.google.gerrit.server.project.ProjectState;
import com.google.inject.AbstractModule;
import com.google.inject.Module;
import java.util.Collection;
-import java.util.Set;
import java.util.stream.StreamSupport;
import javax.inject.Inject;
import org.eclipse.jgit.lib.Ref;
@@ -152,7 +151,7 @@ public class ExternalUserPermissionIT extends AbstractDaemonTest {
}
@Override
- public Set<AccountGroup.UUID> intersection(
+ public ImmutableSet<AccountGroup.UUID> intersection(
Iterable<AccountGroup.UUID> groupIds) {
return StreamSupport.stream(groupIds.spliterator(), /* parallel= */ false)
.filter(g -> contains(g))
@@ -160,7 +159,7 @@ public class ExternalUserPermissionIT extends AbstractDaemonTest {
}
@Override
- public Set<AccountGroup.UUID> getKnownGroups() {
+ public ImmutableSet<AccountGroup.UUID> getKnownGroups() {
return ImmutableSet.of();
}
};
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectCacheIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectCacheIT.java
index 0fb6b9e514..2d198b7848 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectCacheIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectCacheIT.java
@@ -15,7 +15,6 @@
package com.google.gerrit.acceptance.server.project;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import com.google.common.cache.LoadingCache;
import com.google.gerrit.acceptance.AbstractDaemonTest;
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index 432a6c6332..014933f6fb 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -36,7 +36,6 @@ import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.inject.Inject;
import java.util.EnumSet;
-import java.util.List;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.junit.Test;
@@ -90,7 +89,7 @@ public class ProjectWatchIT extends AbstractDaemonTest {
.to("refs/for/master");
r.assertOkStatus();
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactlyElementsIn(watchers.build());
@@ -248,7 +247,7 @@ public class ProjectWatchIT extends AbstractDaemonTest {
r.assertOkStatus();
// assert email notification
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(user.getNameEmail());
@@ -280,7 +279,7 @@ public class ProjectWatchIT extends AbstractDaemonTest {
r.assertOkStatus();
// assert email notification for user
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(user.getNameEmail());
@@ -329,7 +328,7 @@ public class ProjectWatchIT extends AbstractDaemonTest {
r.assertOkStatus();
// assert email notification for user
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(user.getNameEmail());
@@ -393,7 +392,7 @@ public class ProjectWatchIT extends AbstractDaemonTest {
gApi.changes().id(r.getChangeId()).addReviewer(user2.email());
// assert email notification
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertNotifyTo(user2);
@@ -419,7 +418,7 @@ public class ProjectWatchIT extends AbstractDaemonTest {
r.assertOkStatus();
// assert email notification for user
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(user.getNameEmail());
@@ -455,7 +454,7 @@ public class ProjectWatchIT extends AbstractDaemonTest {
// change user can see the non-visible account.
// Even if watching by the non-visible account was not possible, user could just watch all
// changes that are visible to them and then filter them by the non-visible account locally.
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(user.getNameEmail());
@@ -494,7 +493,7 @@ public class ProjectWatchIT extends AbstractDaemonTest {
// is sent to the admin user
requestScopeOperations.setApiUser(user.id());
gApi.changes().id(r.getChangeId()).current().review(reviewInput);
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(admin.getNameEmail());
@@ -531,7 +530,7 @@ public class ProjectWatchIT extends AbstractDaemonTest {
r.assertOkStatus();
// assert email notification
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(user.getNameEmail());
@@ -559,7 +558,7 @@ public class ProjectWatchIT extends AbstractDaemonTest {
r.assertOkStatus();
// assert email notification for user
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(user.getNameEmail());
@@ -608,7 +607,7 @@ public class ProjectWatchIT extends AbstractDaemonTest {
r.assertOkStatus();
// assert email notification for user
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(user.getNameEmail());
@@ -688,7 +687,7 @@ public class ProjectWatchIT extends AbstractDaemonTest {
r.assertOkStatus();
// assert email notification
- List<Message> messages = sender.getMessages();
+ ImmutableList<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(userThatCanViewPrivateChanges.getNameEmail());
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java b/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
index df668a5ee0..9093412a0f 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
@@ -144,12 +144,16 @@ public class ReflogIT extends AbstractDaemonTest {
.update();
requestScopeOperations.setApiUser(user.id());
- gApi.projects().name(project.get()).branch("master").reflog();
+
+ @SuppressWarnings("unused")
+ var unused = gApi.projects().name(project.get()).branch("master").reflog();
}
@Test
public void adminUserIsAllowedToGetReflog() throws Exception {
requestScopeOperations.setApiUser(admin.id());
- gApi.projects().name(project.get()).branch("master").reflog();
+
+ @SuppressWarnings("unused")
+ var unused = gApi.projects().name(project.get()).branch("master").reflog();
}
}
diff --git a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
index 15cd1a94a7..0c24b14267 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
@@ -20,6 +20,7 @@ import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.a
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.project.testing.TestLabels.value;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.MoreCollectors;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.ExtensionRegistry;
@@ -53,7 +54,6 @@ import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.query.change.SubmitRequirementPredicate;
import com.google.inject.Inject;
import com.google.inject.Provider;
-import java.util.Map;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
@@ -167,7 +167,7 @@ public class SubmitRequirementsEvaluatorIT extends AbstractDaemonTest {
/* overrideExpr= */ "", /*allowOverrideInChildProjects*/
false);
configSubmitRequirement(project, projectSubmitRequirement);
- Map<SubmitRequirement, SubmitRequirementResult> results =
+ ImmutableMap<SubmitRequirement, SubmitRequirementResult> results =
evaluator.evaluateAllRequirements(changeData);
assertThat(results).hasSize(2);
assertThat(results.get(globalSubmitRequirement).status())
@@ -198,7 +198,7 @@ public class SubmitRequirementsEvaluatorIT extends AbstractDaemonTest {
/* overrideExpr= */ "", /*allowOverrideInChildProjects*/
false);
configSubmitRequirement(project, projectSubmitRequirement);
- Map<SubmitRequirement, SubmitRequirementResult> results =
+ ImmutableMap<SubmitRequirement, SubmitRequirementResult> results =
evaluator.evaluateAllRequirements(changeData);
assertThat(results).hasSize(1);
assertThat(results.get(projectSubmitRequirement).status())
@@ -227,7 +227,7 @@ public class SubmitRequirementsEvaluatorIT extends AbstractDaemonTest {
/* overrideExpr= */ "", /*allowOverrideInChildProjects*/
false);
configSubmitRequirement(project, projectSubmitRequirement);
- Map<SubmitRequirement, SubmitRequirementResult> results =
+ ImmutableMap<SubmitRequirement, SubmitRequirementResult> results =
evaluator.evaluateAllRequirements(changeData);
assertThat(results).hasSize(1);
assertThat(results.get(globalSubmitRequirement).status())
diff --git a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsValidationIT.java b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsValidationIT.java
index 9170214064..e680c8a2c9 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsValidationIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsValidationIT.java
@@ -505,7 +505,8 @@ public class SubmitRequirementsValidationIT extends AbstractDaemonTest {
private void assertServerUser() {
try {
- currentUser.asIdentifiedUser();
+ @SuppressWarnings("unused")
+ var unused = currentUser.asIdentifiedUser();
throw new IllegalStateException("is an identified user");
} catch (UnsupportedOperationException e) {
// as expected.
diff --git a/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java b/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
index 4ce62d27f9..58e9fb987b 100644
--- a/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
@@ -32,7 +32,10 @@ import com.google.gerrit.server.change.ChangeKindCache;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.query.approval.ApprovalContext;
import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
+import com.google.gerrit.server.update.RepoView;
import com.google.inject.Inject;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.Test;
@@ -298,7 +301,9 @@ public class ApprovalQueryIT extends AbstractDaemonTest {
changeKindCache.getChangeKind(
changeNotes.getChange(), changeNotes.getPatchSets().get(newPsId));
try (Repository repo = repoManager.openRepository(project);
- RevWalk rw = new RevWalk(repo.newObjectReader())) {
+ ObjectInserter ins = repo.newObjectInserter();
+ ObjectReader reader = ins.newReader();
+ RevWalk rw = new RevWalk(reader)) {
return ApprovalContext.create(
changeNotes,
psId,
@@ -308,8 +313,7 @@ public class ApprovalQueryIT extends AbstractDaemonTest {
changeNotes.getPatchSets().get(newPsId),
changeKind,
/* isMerge= */ false,
- rw,
- repo.getConfig());
+ new RepoView(repo, rw, ins));
}
}
}
diff --git a/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java b/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java
index c6b09ccc5a..24767cbab2 100644
--- a/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/quota/MultipleQuotaPluginsIT.java
@@ -15,7 +15,6 @@
package com.google.gerrit.acceptance.server.quota;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java b/javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java
index 4ce2debbe8..5a6f16a9eb 100644
--- a/javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java
@@ -15,7 +15,6 @@
package com.google.gerrit.acceptance.server.rules;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/prolog/PrologRuleEvaluatorIT.java b/javatests/com/google/gerrit/acceptance/server/rules/prolog/PrologRuleEvaluatorIT.java
index 850fe8e220..1ab70e9998 100644
--- a/javatests/com/google/gerrit/acceptance/server/rules/prolog/PrologRuleEvaluatorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/rules/prolog/PrologRuleEvaluatorIT.java
@@ -45,7 +45,7 @@ public class PrologRuleEvaluatorIT extends AbstractDaemonTest {
StructureTerm verifiedLabel = makeLabel(LabelId.VERIFIED, "may");
StructureTerm labels = new StructureTerm("label", verifiedLabel);
- List<Term> terms = ImmutableList.of(makeTerm("ok", labels));
+ ImmutableList<Term> terms = ImmutableList.of(makeTerm("ok", labels));
SubmitRecord record = evaluator.resultsToSubmitRecord(null, terms);
assertThat(record.status).isEqualTo(SubmitRecord.Status.OK);
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/prolog/RulesIT.java b/javatests/com/google/gerrit/acceptance/server/rules/prolog/RulesIT.java
index 74bdb56df1..772812f1e3 100644
--- a/javatests/com/google/gerrit/acceptance/server/rules/prolog/RulesIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/rules/prolog/RulesIT.java
@@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.change.IndexOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.entities.RefNames;
@@ -31,8 +32,7 @@ import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import java.util.Arrays;
-import java.util.Collection;
-import java.util.Map;
+import java.util.List;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Repository;
import org.junit.Test;
@@ -171,6 +171,29 @@ public class RulesIT extends AbstractDaemonTest {
assertThat(statusForRuleAddFile("foo")).isEqualTo(SubmitRecord.Status.RULE_ERROR);
}
+ @Test
+ @GerritConfig(name = "rules.enable", value = "false")
+ public void prologRule_noEffectWhenRulesDisabled() throws Exception {
+ modifySubmitRules("gerrit:commit_message_matches('foo.*')");
+ String changeId = createChange().getChangeId();
+ // Default rules don't allow submission
+ assertThat(gApi.changes().id(changeId).get().submittable).isFalse();
+ // Satisfy default rules
+ approve(changeId);
+
+ assertThat(gApi.changes().id(changeId).get().submittable).isTrue();
+ }
+
+ @Test
+ @GerritConfig(name = "rules.enable", value = "true")
+ public void prologRule_takesEffectWhenRulesEnabled() throws Exception {
+ modifySubmitRules("gerrit:commit_message_matches('foo.*')");
+ String changeId = createChange().getChangeId();
+ approve(changeId);
+
+ assertThat(gApi.changes().id(changeId).get().submittable).isFalse();
+ }
+
private SubmitRecord.Status statusForRule() throws Exception {
String oldHead = projectOperations.project(project).getHead("master").name();
PushOneCommit.Result result =
@@ -180,7 +203,7 @@ public class RulesIT extends AbstractDaemonTest {
}
private SubmitRecord.Status statusForRuleAddFile(String... filenames) throws Exception {
- Map<String, String> fileToContentMap =
+ ImmutableMap<String, String> fileToContentMap =
Arrays.stream(filenames).collect(ImmutableMap.toImmutableMap(f -> f, f -> "file content"));
String oldHead = projectOperations.project(project).getHead("master").name();
PushOneCommit push =
@@ -243,7 +266,7 @@ public class RulesIT extends AbstractDaemonTest {
private SubmitRecord.Status getStatus(PushOneCommit.Result result) throws Exception {
ChangeData cd = result.getChange();
- Collection<SubmitRecord> records;
+ List<SubmitRecord> records;
try (AutoCloseable ignored1 = changeIndexOperations.disableReadsAndWrites();
AutoCloseable ignored2 = accountIndexOperations.disableReadsAndWrites()) {
SubmitRuleEvaluator ruleEvaluator = evaluatorFactory.create(SubmitRuleOptions.defaults());
diff --git a/javatests/com/google/gerrit/acceptance/server/util/TaskListenerIT.java b/javatests/com/google/gerrit/acceptance/server/util/TaskListenerIT.java
index fdfef879fe..809cee9fb0 100644
--- a/javatests/com/google/gerrit/acceptance/server/util/TaskListenerIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/util/TaskListenerIT.java
@@ -150,7 +150,7 @@ public class TaskListenerIT extends AbstractDaemonTest {
@Override
public void configure() {
// Forwarder.delegate is empty on start to protect test listener from non test tasks
- // (such as the "Log File Compressor") interference
+ // (such as the "Log File Manager") interference
forwarder = new ForwardingListener(); // Only gets bound once for all tests
bind(TaskListener.class).annotatedWith(Exports.named("listener")).toInstance(forwarder);
}
@@ -161,7 +161,7 @@ public class TaskListenerIT extends AbstractDaemonTest {
public void setupExecutorAndForwarder() throws InterruptedException {
executor = workQueue.createQueue(1, "TaskListeners");
- // "Log File Compressor"s are likely running and will interfere with tests
+ // "Log File Manager"s are likely running and will interfere with tests
while (0 != workQueue.getTasks().size()) {
for (Task<?> t : workQueue.getTasks()) {
@SuppressWarnings("unused")
@@ -278,8 +278,8 @@ public class TaskListenerIT extends AbstractDaemonTest {
private void assertAwaitQueueSize(int size) throws InterruptedException {
long i = 0;
do {
- TimeUnit.NANOSECONDS.sleep(10);
- assertThat(i++).isLessThan(100);
+ TimeUnit.NANOSECONDS.sleep(100);
+ assertThat(i++).isLessThan(1000);
} while (size != workQueue.getTasks().size());
}
}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
index 5429131c97..8367e25fcf 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
@@ -15,7 +15,6 @@
package com.google.gerrit.acceptance.ssh;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.UseSsh;
@@ -31,7 +30,7 @@ public class CreateProjectIT extends AbstractDaemonTest {
@Test
public void withValidGroupName() throws Exception {
String newGroupName = "newGroup";
- adminRestSession.put("/groups/" + newGroupName);
+ adminRestSession.put("/groups/" + newGroupName).assertCreated();
String newProjectName = "newProject";
adminSshSession.exec(
"gerrit create-project --branch master --owner " + newGroupName + " " + newProjectName);
@@ -43,7 +42,7 @@ public class CreateProjectIT extends AbstractDaemonTest {
@Test
public void withInvalidGroupName() throws Exception {
String newGroupName = "newGroup";
- adminRestSession.put("/groups/" + newGroupName);
+ adminRestSession.put("/groups/" + newGroupName).assertCreated();
String wrongGroupName = "newG";
String newProjectName = "newProject";
adminSshSession.exec(
@@ -56,7 +55,7 @@ public class CreateProjectIT extends AbstractDaemonTest {
@Test
public void withDotGit() throws Exception {
String newGroupName = "newGroup";
- adminRestSession.put("/groups/" + newGroupName);
+ adminRestSession.put("/groups/" + newGroupName).assertCreated();
String newProjectName = name("newProject");
adminSshSession.exec(
"gerrit create-project --branch master --owner "
@@ -73,7 +72,7 @@ public class CreateProjectIT extends AbstractDaemonTest {
@Test
public void withTrailingSlash() throws Exception {
String newGroupName = "newGroup";
- adminRestSession.put("/groups/" + newGroupName);
+ adminRestSession.put("/groups/" + newGroupName).assertCreated();
String newProjectName = name("newProject");
adminSshSession.exec(
"gerrit create-project --branch master --owner "
diff --git a/javatests/com/google/gerrit/acceptance/ssh/CustomIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/CustomIndexIT.java
index fee413ab8d..7fead5c791 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/CustomIndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/CustomIndexIT.java
@@ -16,6 +16,7 @@ package com.google.gerrit.acceptance.ssh;
import static com.google.common.truth.Truth.assertThat;
+import com.google.common.collect.ImmutableMap;
import com.google.gerrit.index.IndexType;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.project.ProjectIndex;
@@ -34,7 +35,6 @@ import com.google.gerrit.testing.ConfigSuite;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.assistedinject.Assisted;
-import java.util.Map;
import org.eclipse.jgit.lib.Config;
import org.junit.Test;
@@ -67,10 +67,11 @@ public class CustomIndexIT extends AbstractIndexTests {
class CustomIndexModule extends AbstractIndexModule {
public static CustomIndexModule latestVersion(boolean secondary) {
- return new CustomIndexModule(null, -1 /* direct executor */, secondary);
+ return new CustomIndexModule(/* singleVersions= */ null, -1 /* direct executor */, secondary);
}
- private CustomIndexModule(Map<String, Integer> singleVersions, int threads, boolean secondary) {
+ private CustomIndexModule(
+ ImmutableMap<String, Integer> singleVersions, int threads, boolean secondary) {
super(singleVersions, threads, secondary);
}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshDaemonIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshDaemonIT.java
index 28d2a28c9c..865cf51ea0 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshDaemonIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshDaemonIT.java
@@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritServerTestRule;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.Sandboxed;
import com.google.gerrit.acceptance.UseSsh;
@@ -35,8 +36,6 @@ import org.junit.runner.RunWith;
@Sandboxed
@RunWith(ConfigSuite.class)
public class SshDaemonIT extends AbstractDaemonTest {
- @ConfigSuite.Parameter protected Config config;
-
@ConfigSuite.Config
public static Config gracefulConfig() {
Config config = new Config();
@@ -52,7 +51,7 @@ public class SshDaemonIT extends AbstractDaemonTest {
@Test
public void nonGracefulCommandIsStoppedImmediately() throws Exception {
Future<Integer> future = startCommand(false);
- restart();
+ ((GerritServerTestRule) server).restartKeepSessionOpen();
assertThat(future.get()).isEqualTo(-1);
}
@@ -61,7 +60,7 @@ public class SshDaemonIT extends AbstractDaemonTest {
assume().that(isGracefulStopEnabled()).isTrue();
Future<Integer> future = startCommand(true);
- restart();
+ ((GerritServerTestRule) server).restartKeepSessionOpen();
assertThat(future.get()).isEqualTo(0);
}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
index 84c39362c6..c4497dc079 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
@@ -32,6 +32,7 @@ import com.google.gerrit.server.project.CreateProjectArgs;
import com.google.gerrit.server.validators.ProjectCreationValidationListener;
import com.google.gerrit.server.validators.ValidationException;
import com.google.inject.Inject;
+import java.time.Instant;
import org.junit.Test;
@UseSsh
@@ -123,8 +124,8 @@ public class SshTraceIT extends AbstractDaemonTest {
private ImmutableList.Builder<PerformanceLogEntry> logEntries = ImmutableList.builder();
@Override
- public void log(String operation, long durationMs, Metadata metadata) {
- logEntries.add(PerformanceLogEntry.create(operation, metadata));
+ public void log(String operation, long durationMs, Instant endTime, Metadata metadata) {
+ logEntries.add(PerformanceLogEntry.create(operation, endTime, metadata));
}
ImmutableList<PerformanceLogEntry> logEntries() {
@@ -134,12 +135,14 @@ public class SshTraceIT extends AbstractDaemonTest {
@AutoValue
abstract static class PerformanceLogEntry {
- static PerformanceLogEntry create(String operation, Metadata metadata) {
- return new AutoValue_SshTraceIT_PerformanceLogEntry(operation, metadata);
+ static PerformanceLogEntry create(String operation, Instant endTime, Metadata metadata) {
+ return new AutoValue_SshTraceIT_PerformanceLogEntry(operation, endTime, metadata);
}
abstract String operation();
+ abstract Instant endTime();
+
abstract Metadata metadata();
}
}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/StreamEventsIT.java b/javatests/com/google/gerrit/acceptance/ssh/StreamEventsIT.java
index 2bfc07233d..8bc457b5a2 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/StreamEventsIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/StreamEventsIT.java
@@ -194,7 +194,7 @@ public class StreamEventsIT extends AbstractDaemonTest {
draftInput.message = reviewMessage;
draftInput.path = path;
ChangeApi changeApi = gApi.changes().id(change.getId().get());
- changeApi.current().createDraft(draftInput).get();
+ changeApi.current().createDraft(draftInput);
}
private void publishDraftReviews() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImplTest.java
index 5bdf91f53c..6204115525 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImplTest.java
@@ -49,6 +49,7 @@ import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.truth.NullAwareCorrespondence;
+import com.google.gerrit.truth.OptionalSubject;
import com.google.inject.Inject;
import java.util.Map;
import org.eclipse.jgit.lib.ObjectId;
@@ -1629,7 +1630,7 @@ public class ChangeOperationsImplTest extends AbstractDaemonTest {
TestHumanComment comment = changeOperations.change(changeId).comment(childCommentUuid).get();
- assertThat(comment.parentUuid()).value().isEqualTo(parentCommentUuid);
+ OptionalSubject.assertThat(comment.parentUuid()).value().isEqualTo(parentCommentUuid);
}
@Test
@@ -1640,7 +1641,7 @@ public class ChangeOperationsImplTest extends AbstractDaemonTest {
TestHumanComment comment = changeOperations.change(changeId).comment(childCommentUuid).get();
- assertThat(comment.tag()).value().isEqualTo("tag");
+ OptionalSubject.assertThat(comment.tag()).value().isEqualTo("tag");
}
@Test
@@ -1700,7 +1701,7 @@ public class ChangeOperationsImplTest extends AbstractDaemonTest {
TestHumanComment comment =
changeOperations.change(changeId).draftComment(childCommentUuid).get();
- assertThat(comment.parentUuid()).value().isEqualTo(parentCommentUuid);
+ OptionalSubject.assertThat(comment.parentUuid()).value().isEqualTo(parentCommentUuid);
}
@Test
@@ -1730,7 +1731,7 @@ public class ChangeOperationsImplTest extends AbstractDaemonTest {
TestRobotComment comment =
changeOperations.change(changeId).robotComment(childCommentUuid).get();
- assertThat(comment.parentUuid()).value().isEqualTo(parentCommentUuid);
+ OptionalSubject.assertThat(comment.parentUuid()).value().isEqualTo(parentCommentUuid);
}
private ChangeInfo getChangeFromServer(Change.Id changeId) throws RestApiException {
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
index 473b1284d2..3c5879781d 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
@@ -16,7 +16,6 @@ package com.google.gerrit.acceptance.testsuite.group;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.collect.ImmutableSet;
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
index 661802e131..8e2dfddf12 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
@@ -67,8 +67,11 @@ public class ProjectOperationsImplTest extends AbstractDaemonTest {
@Test
public void defaultName() throws Exception {
Project.NameKey name = projectOperations.newProject().create();
+
// check that the project was created (throws exception if not found.)
- gApi.projects().name(name.get());
+ @SuppressWarnings("unused")
+ var unused = gApi.projects().name(name.get());
+
Project.NameKey name2 = projectOperations.newProject().create();
assertThat(name2).isNotEqualTo(name);
}
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java
index 424151121a..988da60661 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java
@@ -20,7 +20,6 @@ import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
import com.google.gerrit.acceptance.UseSsh;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.account.TestAccount;
@@ -29,6 +28,7 @@ import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.Sequences;
+import com.google.gerrit.server.util.RequestContext;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.concurrent.atomic.AtomicInteger;
@@ -48,8 +48,8 @@ public class RequestScopeOperationsImplTest extends AbstractDaemonTest {
@Test
public void setApiUserToExistingUserById() throws Exception {
fastCheckCurrentUser(admin.id());
- AcceptanceTestRequestScope.Context oldCtx = requestScopeOperations.setApiUser(user.id());
- assertThat(oldCtx.getUser().getAccountId()).isEqualTo(admin.id());
+ assertThat(localCtx.getContext().getUser().getAccountId()).isEqualTo(admin.id());
+ requestScopeOperations.setApiUser(user.id());
checkCurrentUser(user.id());
}
@@ -58,8 +58,8 @@ public class RequestScopeOperationsImplTest extends AbstractDaemonTest {
fastCheckCurrentUser(admin.id());
TestAccount testAccount =
accountOperations.account(accountOperations.newAccount().username("tester").create()).get();
- AcceptanceTestRequestScope.Context oldCtx = requestScopeOperations.setApiUser(testAccount);
- assertThat(oldCtx.getUser().getAccountId()).isEqualTo(admin.id());
+ assertThat(localCtx.getContext().getUser().getAccountId()).isEqualTo(admin.id());
+ requestScopeOperations.setApiUser(testAccount);
checkCurrentUser(testAccount.accountId());
}
@@ -95,7 +95,7 @@ public class RequestScopeOperationsImplTest extends AbstractDaemonTest {
assertWithMessage("user from GerritApi")
.that(gApi.accounts().self().get()._accountId)
.isEqualTo(expected.get());
- AcceptanceTestRequestScope.Context ctx = atrScope.get();
+ RequestContext ctx = localCtx.getContext();
assertWithMessage("user from AcceptanceTestRequestScope.Context is an IdentifiedUser")
.that(ctx.getUser().isIdentifiedUser())
.isTrue();
@@ -115,7 +115,9 @@ public class RequestScopeOperationsImplTest extends AbstractDaemonTest {
String changeId = gApi.changes().create(cin).get().changeId;
assertThat(gApi.changes().id(changeId).get().owner._accountId).isEqualTo(expected.get());
String queryResults =
- atrScope.get().getSession().exec("gerrit query owner:self change:" + changeId);
+ server
+ .getOrCreateSshSessionForContext(localCtx.getContext())
+ .exec("gerrit query owner:self change:" + changeId);
assertWithMessage("Change-Ids in query results:\n%s", queryResults)
.that(findDistinct(queryResults, "I[0-9a-f]{40}"))
.containsExactly(changeId);
diff --git a/javatests/com/google/gerrit/common/data/ParameterizedStringTest.java b/javatests/com/google/gerrit/common/data/ParameterizedStringTest.java
index b646d2bce0..54a8b26797 100644
--- a/javatests/com/google/gerrit/common/data/ParameterizedStringTest.java
+++ b/javatests/com/google/gerrit/common/data/ParameterizedStringTest.java
@@ -374,7 +374,7 @@ public class ParameterizedStringTest {
assertThat(p.getParameterNames()).hasSize(2);
assertThat(p.getParameterNames()).containsExactly("patchSet", "branch");
- Map<String, String> params =
+ ImmutableMap<String, String> params =
ImmutableMap.of(
"patchSet", "42",
"branch", "foo");
@@ -388,7 +388,7 @@ public class ParameterizedStringTest {
@Test
public void replaceSubmitTooltipWithoutVariables() {
ParameterizedString p = new ParameterizedString("Submit patch set 40 into master");
- Map<String, String> params =
+ ImmutableMap<String, String> params =
ImmutableMap.of(
"patchSet", "42",
"branch", "foo");
diff --git a/javatests/com/google/gerrit/entities/LabelFunctionTest.java b/javatests/com/google/gerrit/entities/LabelFunctionTest.java
index 80d97db8b2..ffbdaf1168 100644
--- a/javatests/com/google/gerrit/entities/LabelFunctionTest.java
+++ b/javatests/com/google/gerrit/entities/LabelFunctionTest.java
@@ -70,7 +70,8 @@ public class LabelFunctionTest {
@Test
public void checkMaxNoBlockIgnoresMin() {
- List<PatchSetApproval> approvals = ImmutableList.of(APPROVAL_M2, APPROVAL_2, APPROVAL_M2);
+ ImmutableList<PatchSetApproval> approvals =
+ ImmutableList.of(APPROVAL_M2, APPROVAL_2, APPROVAL_M2);
SubmitRecord.Label myLabel = LabelFunction.MAX_NO_BLOCK.check(VERIFIED_LABEL, approvals);
@@ -98,7 +99,8 @@ public class LabelFunctionTest {
}
private static void checkBlockWorks(LabelFunction function) {
- List<PatchSetApproval> approvals = ImmutableList.of(APPROVAL_1, APPROVAL_M2, APPROVAL_2);
+ ImmutableList<PatchSetApproval> approvals =
+ ImmutableList.of(APPROVAL_1, APPROVAL_M2, APPROVAL_2);
SubmitRecord.Label myLabel = function.check(VERIFIED_LABEL, approvals);
@@ -121,7 +123,7 @@ public class LabelFunctionTest {
}
private static void checkMaxIsEnforced(LabelFunction function) {
- List<PatchSetApproval> approvals = ImmutableList.of(APPROVAL_1, APPROVAL_0);
+ ImmutableList<PatchSetApproval> approvals = ImmutableList.of(APPROVAL_1, APPROVAL_0);
SubmitRecord.Label myLabel = function.check(VERIFIED_LABEL, approvals);
@@ -129,7 +131,8 @@ public class LabelFunctionTest {
}
private static void checkMaxValidatesTheLabel(LabelFunction function) {
- List<PatchSetApproval> approvals = ImmutableList.of(APPROVAL_1, APPROVAL_2, APPROVAL_M1);
+ ImmutableList<PatchSetApproval> approvals =
+ ImmutableList.of(APPROVAL_1, APPROVAL_2, APPROVAL_M1);
SubmitRecord.Label myLabel = function.check(VERIFIED_LABEL, approvals);
diff --git a/javatests/com/google/gerrit/entities/PatchSetTest.java b/javatests/com/google/gerrit/entities/PatchSetTest.java
index 7e04fe8fbf..e2718c58ac 100644
--- a/javatests/com/google/gerrit/entities/PatchSetTest.java
+++ b/javatests/com/google/gerrit/entities/PatchSetTest.java
@@ -65,7 +65,11 @@ public class PatchSetTest {
@Test
public void testSplitGroups() {
- assertRuntimeException(() -> splitGroups(null));
+ assertRuntimeException(
+ () -> {
+ @SuppressWarnings("unused")
+ var unused = splitGroups(null);
+ });
assertThat(splitGroups("")).containsExactly("");
assertThat(splitGroups("abcd")).containsExactly("abcd");
assertThat(splitGroups("ab,cd")).containsExactly("ab", "cd").inOrder();
@@ -76,8 +80,16 @@ public class PatchSetTest {
@Test
public void testJoinGroups() {
- assertRuntimeException(() -> joinGroups(null));
- assertRuntimeException(() -> joinGroups(ImmutableList.of("a,", "b")));
+ assertRuntimeException(
+ () -> {
+ @SuppressWarnings("unused")
+ var unused = joinGroups(null);
+ });
+ assertRuntimeException(
+ () -> {
+ @SuppressWarnings("unused")
+ var unused = joinGroups(ImmutableList.of("a,", "b"));
+ });
assertThat(joinGroups(ImmutableList.of(""))).isEqualTo("");
assertThat(joinGroups(ImmutableList.of("abcd"))).isEqualTo("abcd");
assertThat(joinGroups(ImmutableList.of("ab", "cd"))).isEqualTo("ab,cd");
@@ -126,7 +138,11 @@ public class PatchSetTest {
}
private static void assertInvalidId(String str) {
- assertRuntimeException(() -> PatchSet.Id.parse(str));
+ assertRuntimeException(
+ () -> {
+ @SuppressWarnings("unused")
+ var unused = PatchSet.Id.parse(str);
+ });
}
private static void assertRuntimeException(Runnable runnable) {
diff --git a/javatests/com/google/gerrit/entities/SubmitRecordTest.java b/javatests/com/google/gerrit/entities/SubmitRecordTest.java
index 578bc18201..7f0f42c78a 100644
--- a/javatests/com/google/gerrit/entities/SubmitRecordTest.java
+++ b/javatests/com/google/gerrit/entities/SubmitRecordTest.java
@@ -18,7 +18,7 @@ import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
-import java.util.Collection;
+import java.util.List;
import org.junit.Test;
public class SubmitRecordTest {
@@ -39,7 +39,7 @@ public class SubmitRecordTest {
@Test
public void okIfAllOkay() {
- Collection<SubmitRecord> submitRecords = new ArrayList<>();
+ List<SubmitRecord> submitRecords = new ArrayList<>();
submitRecords.add(OK_RECORD);
assertThat(SubmitRecord.allRecordsOK(submitRecords)).isTrue();
@@ -47,14 +47,14 @@ public class SubmitRecordTest {
@Test
public void okWhenEmpty() {
- Collection<SubmitRecord> submitRecords = new ArrayList<>();
+ List<SubmitRecord> submitRecords = new ArrayList<>();
assertThat(SubmitRecord.allRecordsOK(submitRecords)).isTrue();
}
@Test
public void okWhenForced() {
- Collection<SubmitRecord> submitRecords = new ArrayList<>();
+ List<SubmitRecord> submitRecords = new ArrayList<>();
submitRecords.add(FORCED_RECORD);
assertThat(SubmitRecord.allRecordsOK(submitRecords)).isTrue();
@@ -62,7 +62,7 @@ public class SubmitRecordTest {
@Test
public void emptyResultIfInvalid() {
- Collection<SubmitRecord> submitRecords = new ArrayList<>();
+ List<SubmitRecord> submitRecords = new ArrayList<>();
submitRecords.add(NOT_READY_RECORD);
submitRecords.add(OK_RECORD);
diff --git a/javatests/com/google/gerrit/entities/converter/AccountInputProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/AccountInputProtoConverterTest.java
new file mode 100644
index 0000000000..c14e926199
--- /dev/null
+++ b/javatests/com/google/gerrit/entities/converter/AccountInputProtoConverterTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.google.gerrit.entities.converter;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.extensions.api.accounts.AccountInput;
+import com.google.gerrit.proto.Entities;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
+import com.google.inject.TypeLiteral;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Objects;
+import org.junit.Test;
+
+public class AccountInputProtoConverterTest {
+ private final AccountInputProtoConverter accountInputProtoConverter =
+ AccountInputProtoConverter.INSTANCE;
+
+ private AccountInput createAccountInputInstance() {
+ AccountInput accountInput = new AccountInput();
+ accountInput.username = "test-username";
+ accountInput.name = "test-name";
+ accountInput.displayName = "test-display-name";
+ accountInput.email = "test-email@gmail.com";
+ accountInput.sshKey = "test-ssh-key";
+ accountInput.httpPassword = "test-http-password";
+ accountInput.groups = List.of("group1", "group2");
+ return accountInput;
+ }
+
+ private void assertAccountInputEquals(AccountInput expected, AccountInput actual) {
+ assertThat(
+ Objects.equals(expected.username, actual.username)
+ && Objects.equals(expected.name, actual.name)
+ && Objects.equals(expected.displayName, actual.displayName)
+ && Objects.equals(expected.email, actual.email)
+ && Objects.equals(expected.sshKey, actual.sshKey)
+ && Objects.equals(expected.httpPassword, actual.httpPassword)
+ && Objects.equals(expected.groups, actual.groups))
+ .isTrue();
+ }
+
+ @Test
+ public void allValuesConvertedToProto() {
+
+ Entities.AccountInput proto = accountInputProtoConverter.toProto(createAccountInputInstance());
+
+ Entities.AccountInput expectedProto =
+ Entities.AccountInput.newBuilder()
+ .setUsername("test-username")
+ .setName("test-name")
+ .setDisplayName("test-display-name")
+ .setEmail("test-email@gmail.com")
+ .setSshKey("test-ssh-key")
+ .setHttpPassword("test-http-password")
+ .addAllGroups(ImmutableList.of("group1", "group2"))
+ .build();
+ assertThat(proto).isEqualTo(expectedProto);
+ }
+
+ @Test
+ public void allValuesConvertedToProtoAndBackAgain() {
+ AccountInput accountInput = createAccountInputInstance();
+
+ AccountInput convertedaccountInput =
+ accountInputProtoConverter.fromProto(accountInputProtoConverter.toProto(accountInput));
+
+ assertAccountInputEquals(accountInput, convertedaccountInput);
+ }
+
+ /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
+ @Test
+ public void methodsExistAsExpected() {
+ assertThatSerializedClass(AccountInput.class)
+ .hasFields(
+ ImmutableMap.<String, Type>builder()
+ .put("username", String.class)
+ .put("name", String.class)
+ .put("displayName", String.class)
+ .put("email", String.class)
+ .put("sshKey", String.class)
+ .put("httpPassword", String.class)
+ .put("groups", new TypeLiteral<List<String>>() {}.getType())
+ .build());
+ }
+}
diff --git a/javatests/com/google/gerrit/entities/converter/ApplyPatchInputProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ApplyPatchInputProtoConverterTest.java
new file mode 100644
index 0000000000..6c8c85a242
--- /dev/null
+++ b/javatests/com/google/gerrit/entities/converter/ApplyPatchInputProtoConverterTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.google.gerrit.entities.converter;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
+import com.google.gerrit.proto.Entities;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
+import java.lang.reflect.Type;
+import java.util.Objects;
+import org.junit.Test;
+
+public class ApplyPatchInputProtoConverterTest {
+ private final ApplyPatchInputProtoConverter applyPatchInputProtoConverter =
+ ApplyPatchInputProtoConverter.INSTANCE;
+
+ @Test
+ public void allValuesConvertedToProto() {
+ ApplyPatchInput applyPatchInput = new ApplyPatchInput();
+ applyPatchInput.patch = "test-patch";
+ Entities.ApplyPatchInput proto = applyPatchInputProtoConverter.toProto(applyPatchInput);
+
+ Entities.ApplyPatchInput expectedProto =
+ Entities.ApplyPatchInput.newBuilder().setPatch("test-patch").build();
+ assertThat(proto).isEqualTo(expectedProto);
+ }
+
+ @Test
+ public void allValuesConvertedToProtoAndBackAgain() {
+ ApplyPatchInput applyPatchInput = new ApplyPatchInput();
+ applyPatchInput.patch = "test-patch";
+
+ ApplyPatchInput convertedApplyPatchInput =
+ applyPatchInputProtoConverter.fromProto(
+ applyPatchInputProtoConverter.toProto(applyPatchInput));
+
+ assertThat(Objects.equals(applyPatchInput.patch, convertedApplyPatchInput.patch)).isTrue();
+ }
+
+ /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
+ @Test
+ public void methodsExistAsExpected() {
+ assertThatSerializedClass(ApplyPatchInput.class)
+ .hasFields(ImmutableMap.<String, Type>builder().put("patch", String.class).build());
+ }
+}
diff --git a/javatests/com/google/gerrit/entities/converter/BUILD b/javatests/com/google/gerrit/entities/converter/BUILD
index 6c4d1e47ba..0ca947871a 100644
--- a/javatests/com/google/gerrit/entities/converter/BUILD
+++ b/javatests/com/google/gerrit/entities/converter/BUILD
@@ -5,6 +5,7 @@ junit_tests(
srcs = glob(["*.java"]),
deps = [
"//java/com/google/gerrit/entities",
+ "//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/proto/testing",
"//java/com/google/gerrit/server",
"//lib:guava",
diff --git a/javatests/com/google/gerrit/entities/converter/ChangeInputProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ChangeInputProtoConverterTest.java
new file mode 100644
index 0000000000..7123d423ac
--- /dev/null
+++ b/javatests/com/google/gerrit/entities/converter/ChangeInputProtoConverterTest.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.google.gerrit.entities.converter;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.extensions.api.accounts.AccountInput;
+import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyInfo;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.common.MergeInput;
+import com.google.gerrit.proto.Entities;
+import com.google.inject.TypeLiteral;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import org.junit.Test;
+
+public class ChangeInputProtoConverterTest {
+ private final ChangeInputProtoConverter changeInputProtoConverter =
+ ChangeInputProtoConverter.INSTANCE;
+ private final MergeInputProtoConverter mergeInputProtoConverter =
+ MergeInputProtoConverter.INSTANCE;
+ private final AccountInputProtoConverter accountInputProtoConverter =
+ AccountInputProtoConverter.INSTANCE;
+
+ // Helper method that creates a MergeInput with all possible value.
+ private MergeInput createMergeInput() {
+ MergeInput mergeInput = new MergeInput();
+ mergeInput.source = "test-source";
+ mergeInput.sourceBranch = "test-source-branch";
+ mergeInput.strategy = "test-strategy";
+ mergeInput.allowConflicts = true;
+ return mergeInput;
+ }
+
+ // Helper method that creates a AccountInput with all possible value.
+ private AccountInput createAccountInput() {
+ AccountInput accountInput = new AccountInput();
+ accountInput.username = "test-username";
+ accountInput.displayName = "test-displayName";
+ accountInput.name = "test-name";
+ accountInput.email = "test-email";
+ accountInput.sshKey = "test-ssh-key";
+ accountInput.httpPassword = "test-http-password";
+ accountInput.groups = ImmutableList.of("test-group");
+ return accountInput;
+ }
+
+ // Helper method that creates a ChangeInput with all possible value.
+ private ChangeInput createChangeInput() {
+ ChangeInput changeInput = new ChangeInput("test-project", "test-branch", "test-subject");
+ changeInput.topic = "test-topic";
+ changeInput.status = ChangeStatus.NEW;
+ changeInput.isPrivate = true;
+ changeInput.workInProgress = true;
+ changeInput.baseChange = "test-base-change";
+ changeInput.baseCommit = "test-base-commit";
+ changeInput.newBranch = true;
+
+ Map<String, String> validationOptions = new HashMap<>();
+ validationOptions.put("test-key", "test-value");
+ changeInput.validationOptions = validationOptions;
+
+ Map<String, String> customKeyedValues = new HashMap<>();
+ customKeyedValues.put("test-key", "test-value");
+ changeInput.customKeyedValues = customKeyedValues;
+
+ changeInput.merge = createMergeInput();
+
+ ApplyPatchInput applyPatchInput = new ApplyPatchInput();
+ applyPatchInput.patch = "test-patch";
+ changeInput.patch = applyPatchInput;
+
+ changeInput.author = createAccountInput();
+
+ changeInput.responseFormatOptions = new ArrayList<ListChangesOption>();
+ changeInput.responseFormatOptions.addAll(
+ ImmutableList.of(ListChangesOption.LABELS, ListChangesOption.DETAILED_LABELS));
+
+ changeInput.notify = NotifyHandling.OWNER;
+
+ Map<RecipientType, NotifyInfo> notifyDetails = new HashMap<>();
+ NotifyInfo notifyInfo = new NotifyInfo(ImmutableList.of("account1", "account2"));
+ notifyDetails.put(RecipientType.TO, notifyInfo);
+ changeInput.notifyDetails = notifyDetails;
+ return changeInput;
+ }
+
+ private void assertAccountInputEquals(AccountInput expected, AccountInput actual) {
+ assertThat(
+ (expected == null && actual == null)
+ || (Objects.equals(expected.username, actual.username)
+ && Objects.equals(expected.name, actual.name)
+ && Objects.equals(expected.displayName, actual.displayName)
+ && Objects.equals(expected.email, actual.email)
+ && Objects.equals(expected.sshKey, actual.sshKey)
+ && Objects.equals(expected.httpPassword, actual.httpPassword)
+ && Objects.equals(expected.groups, actual.groups)))
+ .isTrue();
+ }
+
+ private void assertMergeInputEquals(MergeInput expected, MergeInput actual) {
+ assertThat(
+ (expected == null && actual == null)
+ || (Objects.equals(expected.source, actual.source)
+ && Objects.equals(expected.sourceBranch, actual.sourceBranch)
+ && Objects.equals(expected.strategy, actual.strategy)
+ && expected.allowConflicts == actual.allowConflicts))
+ .isTrue();
+ }
+
+ private void assertChangeInputEquals(ChangeInput expected, ChangeInput actual) {
+ assertThat(
+ Objects.equals(expected.project, actual.project)
+ && Objects.equals(expected.branch, actual.branch)
+ && Objects.equals(expected.subject, actual.subject)
+ && Objects.equals(expected.topic, actual.topic)
+ && Objects.equals(expected.status, actual.status)
+ && Objects.equals(expected.isPrivate, actual.isPrivate)
+ && Objects.equals(expected.workInProgress, actual.workInProgress)
+ && Objects.equals(expected.baseChange, actual.baseChange)
+ && Objects.equals(expected.baseCommit, actual.baseCommit)
+ && Objects.equals(expected.newBranch, actual.newBranch)
+ && Objects.equals(expected.validationOptions, actual.validationOptions)
+ && Objects.equals(expected.customKeyedValues, actual.customKeyedValues)
+ && Objects.equals(expected.responseFormatOptions, actual.responseFormatOptions)
+ && Objects.equals(expected.notify, actual.notify)
+ && Objects.equals(expected.notifyDetails, actual.notifyDetails))
+ .isTrue();
+ assertThat(
+ (expected.patch == null && actual.patch == null)
+ || Objects.equals(expected.patch.patch, actual.patch.patch))
+ .isTrue();
+ assertAccountInputEquals(expected.author, actual.author);
+ assertMergeInputEquals(expected.merge, actual.merge);
+ }
+
+ @Test
+ public void mandatoryValuesConvertedToProto() {
+ ChangeInput changeInput = new ChangeInput("test-project", "test-branch", "test-subject");
+
+ Entities.ChangeInput proto = changeInputProtoConverter.toProto(changeInput);
+
+ Entities.ChangeInput expectedProto =
+ Entities.ChangeInput.newBuilder()
+ .setProject("test-project")
+ .setBranch("test-branch")
+ .setSubject("test-subject")
+ .setNotify(Entities.NotifyHandling.ALL)
+ .build();
+ assertThat(proto).isEqualTo(expectedProto);
+ }
+
+ @Test
+ public void allValuesConvertedToProto() {
+
+ Entities.ChangeInput proto = changeInputProtoConverter.toProto(createChangeInput());
+
+ Entities.ChangeInput.Builder expectedProto =
+ Entities.ChangeInput.newBuilder()
+ .setProject("test-project")
+ .setBranch("test-branch")
+ .setSubject("test-subject")
+ .setTopic("test-topic")
+ .setStatus(Entities.ChangeStatus.NEW)
+ .setBaseChange("test-base-change")
+ .setBaseCommit("test-base-commit")
+ .setNewBranch(true)
+ .setIsPrivate(true)
+ .setWorkInProgress(true)
+ .setPatch(Entities.ApplyPatchInput.newBuilder().setPatch("test-patch").build());
+
+ Map<String, String> validationOptions = new HashMap<>();
+ validationOptions.put("test-key", "test-value");
+ expectedProto.putAllValidationOptions(validationOptions);
+
+ Map<String, String> customKeyedValues = new HashMap<>();
+ customKeyedValues.put("test-key", "test-value");
+ expectedProto.putAllCustomKeyedValues(customKeyedValues);
+
+ expectedProto.setMerge(mergeInputProtoConverter.toProto(createMergeInput()));
+ expectedProto.setAuthor(accountInputProtoConverter.toProto(createAccountInput()));
+
+ expectedProto.addAllResponseFormatOptions(
+ ImmutableList.of(
+ Entities.ListChangesOption.LABELS, Entities.ListChangesOption.DETAILED_LABELS));
+ expectedProto.setNotify(Entities.NotifyHandling.OWNER);
+ Map<String, Entities.NotifyInfo> notifyDetailsProto = new HashMap<>();
+ Entities.NotifyInfo.Builder notifyInfoBuilder =
+ Entities.NotifyInfo.newBuilder().addAllAccounts(ImmutableList.of("account1", "account2"));
+ notifyDetailsProto.put(RecipientType.TO.name(), notifyInfoBuilder.build());
+ expectedProto.putAllNotifyDetails(notifyDetailsProto);
+
+ assertThat(proto).isEqualTo(expectedProto.build());
+ }
+
+ @Test
+ public void mandatoryValuesConvertedToProtoAndBackAgain() {
+ ChangeInput changeInput = new ChangeInput("test-project", "test-branch", "test-subject");
+
+ ChangeInput convertedChangeInput =
+ changeInputProtoConverter.fromProto(changeInputProtoConverter.toProto(changeInput));
+
+ assertChangeInputEquals(changeInput, convertedChangeInput);
+ }
+
+ @Test
+ public void allValuesConvertedToProtoAndBackAgain() {
+ ChangeInput changeInput = createChangeInput();
+
+ ChangeInput convertedChangeInput =
+ changeInputProtoConverter.fromProto(changeInputProtoConverter.toProto(changeInput));
+
+ assertChangeInputEquals(changeInput, convertedChangeInput);
+ }
+
+ /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
+ @Test
+ public void methodsExistAsExpected() {
+ assertThatSerializedClass(ChangeInput.class)
+ .hasFields(
+ ImmutableMap.<String, Type>builder()
+ .put("project", String.class)
+ .put("branch", String.class)
+ .put("subject", String.class)
+ .put("topic", String.class)
+ .put("status", ChangeStatus.class)
+ .put("isPrivate", Boolean.class)
+ .put("workInProgress", Boolean.class)
+ .put("baseChange", String.class)
+ .put("baseCommit", String.class)
+ .put("newBranch", Boolean.class)
+ .put("validationOptions", new TypeLiteral<Map<String, String>>() {}.getType())
+ .put("customKeyedValues", new TypeLiteral<Map<String, String>>() {}.getType())
+ .put("merge", MergeInput.class)
+ .put("patch", ApplyPatchInput.class)
+ .put("author", AccountInput.class)
+ .put(
+ "responseFormatOptions",
+ new TypeLiteral<List<ListChangesOption>>() {}.getType())
+ .put("notify", NotifyHandling.class)
+ .put(
+ "notifyDetails", new TypeLiteral<Map<RecipientType, NotifyInfo>>() {}.getType())
+ .build());
+ }
+}
diff --git a/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java
index 812a0dfa37..512aac9893 100644
--- a/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java
@@ -78,6 +78,7 @@ public class ChangeProtoConverterTest {
.setWorkInProgress(true)
.setReviewStarted(true)
.setRevertOf(Entities.Change_Id.newBuilder().setId(180))
+ .setServerId(TEST_SERVER_ID)
.build();
assertThat(proto).isEqualTo(expectedProto);
}
@@ -91,7 +92,6 @@ public class ChangeProtoConverterTest {
Account.id(35),
BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
Instant.ofEpochMilli(987654L));
- change.setServerId(TEST_SERVER_ID);
Entities.Change proto = changeProtoConverter.toProto(change);
@@ -151,6 +151,7 @@ public class ChangeProtoConverterTest {
.setIsPrivate(false)
.setWorkInProgress(false)
.setReviewStarted(false)
+ .setServerId(TEST_SERVER_ID)
.build();
assertThat(proto).isEqualTo(expectedProto);
}
@@ -189,12 +190,13 @@ public class ChangeProtoConverterTest {
.setIsPrivate(false)
.setWorkInProgress(false)
.setReviewStarted(false)
+ .setServerId(TEST_SERVER_ID)
.build();
assertThat(proto).isEqualTo(expectedProto);
}
@Test
- public void allValuesConvertedToProtoAndBackAgainExceptServerId() {
+ public void allValuesConvertedToProtoAndBackAgainExceptNullServerId() {
Change change =
new Change(
Change.key("change 1"),
@@ -202,7 +204,7 @@ public class ChangeProtoConverterTest {
Account.id(35),
BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
Instant.ofEpochMilli(987654L));
- change.setServerId(TEST_SERVER_ID);
+ change.setServerId(null);
change.setLastUpdatedOn(Instant.ofEpochMilli(1234567L));
change.setStatus(Change.Status.MERGED);
change.setCurrentPatchSet(
@@ -215,11 +217,6 @@ public class ChangeProtoConverterTest {
change.setRevertOf(Change.id(180));
Change convertedChange = changeProtoConverter.fromProto(changeProtoConverter.toProto(change));
-
- // Change serverId is not one of the protobuf definitions, hence is not supposed to be converted
- // from proto
- assertThat(convertedChange.getServerId()).isNull();
- change.setServerId(null);
assertEqualChange(convertedChange, change);
}
diff --git a/javatests/com/google/gerrit/entities/converter/HumanCommentProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/HumanCommentProtoConverterTest.java
new file mode 100644
index 0000000000..a6aaf36fef
--- /dev/null
+++ b/javatests/com/google/gerrit/entities/converter/HumanCommentProtoConverterTest.java
@@ -0,0 +1,130 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.entities.converter;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.CommentRange;
+import com.google.gerrit.entities.HumanComment;
+import java.time.Instant;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public class HumanCommentProtoConverterTest {
+ private static final ObjectId VALID_OBJECT_ID =
+ ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+
+ private final HumanCommentProtoConverter converter = HumanCommentProtoConverter.INSTANCE;
+
+ @Test
+ public void fileLevelCommentWithAllOptionalFields() {
+ HumanComment orig =
+ new HumanComment(
+ new Comment.Key("uuid", "a.txt", 42),
+ Account.id(314),
+ Instant.ofEpochMilli(12345),
+ (short) 1,
+ "message",
+ "server",
+ /* unresolved= */ true);
+ orig.tag = "tag";
+ orig.setCommitId(VALID_OBJECT_ID);
+ orig.setRealAuthor(Account.id(271));
+
+ HumanComment res = converter.fromProto(converter.toProto(orig));
+
+ assertThat(res).isEqualTo(orig);
+ }
+
+ @Test
+ public void patchsetLevelComment() {
+ HumanComment orig =
+ new HumanComment(
+ new Comment.Key("uuid", PATCHSET_LEVEL, 42),
+ Account.id(314),
+ Instant.ofEpochMilli(12345),
+ (short) 1,
+ "message",
+ "server",
+ /* unresolved= */ false);
+
+ HumanComment res = converter.fromProto(converter.toProto(orig));
+
+ assertThat(res).isEqualTo(orig);
+ }
+
+ @Test
+ public void lineComment() {
+ HumanComment orig =
+ new HumanComment(
+ new Comment.Key("uuid", "a.txt", 42),
+ Account.id(314),
+ Instant.ofEpochMilli(12345),
+ (short) 1,
+ "message",
+ "server",
+ /* unresolved= */ true);
+ orig.setLineNbrAndRange(7, null);
+
+ HumanComment res = converter.fromProto(converter.toProto(orig));
+
+ assertThat(res).isEqualTo(orig);
+ }
+
+ @Test
+ public void rangeComment() {
+ HumanComment orig =
+ new HumanComment(
+ new Comment.Key("uuid", "a.txt", 42),
+ Account.id(314),
+ Instant.ofEpochMilli(12345),
+ (short) 1,
+ "message",
+ "server",
+ /* unresolved= */ true);
+ orig.setRange(new CommentRange(2, 3, 5, 7));
+
+ HumanComment res = converter.fromProto(converter.toProto(orig));
+
+ assertThat(res).isEqualTo(orig);
+ }
+
+ @Test
+ public void extensionRangeComment() {
+ HumanComment orig =
+ new HumanComment(
+ new Comment.Key("uuid", "a.txt", 42),
+ Account.id(314),
+ Instant.ofEpochMilli(12345),
+ (short) 1,
+ "message",
+ "server",
+ /* unresolved= */ false);
+ com.google.gerrit.extensions.client.Comment.Range range =
+ new com.google.gerrit.extensions.client.Comment.Range();
+ range.startLine = 2;
+ range.startCharacter = 3;
+ range.endLine = 5;
+ range.endCharacter = 7;
+ orig.setLineNbrAndRange(null, range);
+
+ HumanComment res = converter.fromProto(converter.toProto(orig));
+
+ assertThat(res).isEqualTo(orig);
+ }
+}
diff --git a/javatests/com/google/gerrit/entities/converter/MergeInputProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/MergeInputProtoConverterTest.java
new file mode 100644
index 0000000000..d625545774
--- /dev/null
+++ b/javatests/com/google/gerrit/entities/converter/MergeInputProtoConverterTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.google.gerrit.entities.converter;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.extensions.common.MergeInput;
+import com.google.gerrit.proto.Entities;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
+import java.lang.reflect.Type;
+import java.util.Objects;
+import org.junit.Test;
+
+public class MergeInputProtoConverterTest {
+ private final MergeInputProtoConverter mergeInputProtoConverter =
+ MergeInputProtoConverter.INSTANCE;
+
+ // Helper method that creates a MergeInput with all possible value.
+ private MergeInput createMergeInput() {
+ MergeInput mergeInput = new MergeInput();
+ mergeInput.source = "test-source";
+ mergeInput.sourceBranch = "test-source-branch";
+ mergeInput.strategy = "test-strategy";
+ mergeInput.allowConflicts = true;
+ return mergeInput;
+ }
+
+ private void assertMergeInputEquals(MergeInput expected, MergeInput actual) {
+ assertThat(
+ Objects.equals(expected.source, actual.source)
+ && Objects.equals(expected.sourceBranch, actual.sourceBranch)
+ && Objects.equals(expected.strategy, actual.strategy)
+ && expected.allowConflicts == actual.allowConflicts)
+ .isTrue();
+ }
+
+ @Test
+ public void allValuesConvertedToProto() {
+
+ Entities.MergeInput proto = mergeInputProtoConverter.toProto(createMergeInput());
+
+ Entities.MergeInput expectedProto =
+ Entities.MergeInput.newBuilder()
+ .setSource("test-source")
+ .setSourceBranch("test-source-branch")
+ .setStrategy("test-strategy")
+ .setAllowConflicts(true)
+ .build();
+ assertThat(proto).isEqualTo(expectedProto);
+ }
+
+ @Test
+ public void allValuesConvertedToProtoAndBackAgain() {
+ MergeInput mergeInput = createMergeInput();
+
+ MergeInput convertedMergeInput =
+ mergeInputProtoConverter.fromProto(mergeInputProtoConverter.toProto(mergeInput));
+
+ assertMergeInputEquals(mergeInput, convertedMergeInput);
+ }
+
+ /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
+ @Test
+ public void methodsExistAsExpected() {
+ assertThatSerializedClass(MergeInput.class)
+ .hasFields(
+ ImmutableMap.<String, Type>builder()
+ .put("source", String.class)
+ .put("sourceBranch", String.class)
+ .put("strategy", String.class)
+ .put("allowConflicts", boolean.class)
+ .build());
+ }
+}
diff --git a/javatests/com/google/gerrit/entities/converter/NotifyInfoProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/NotifyInfoProtoConverterTest.java
new file mode 100644
index 0000000000..db723502e0
--- /dev/null
+++ b/javatests/com/google/gerrit/entities/converter/NotifyInfoProtoConverterTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package com.google.gerrit.entities.converter;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.extensions.api.changes.NotifyInfo;
+import com.google.gerrit.proto.Entities;
+import com.google.gerrit.proto.testing.SerializedClassSubject;
+import com.google.inject.TypeLiteral;
+import java.lang.reflect.Type;
+import java.util.List;
+import org.junit.Test;
+
+public class NotifyInfoProtoConverterTest {
+ private final NotifyInfoProtoConverter notifyInfoProtoConverter =
+ NotifyInfoProtoConverter.INSTANCE;
+
+ @Test
+ public void allValuesConvertedToProto() {
+ NotifyInfo notifyInfo = new NotifyInfo(ImmutableList.of("account1", "account2"));
+ Entities.NotifyInfo proto = notifyInfoProtoConverter.toProto(notifyInfo);
+
+ Entities.NotifyInfo expectedProto =
+ Entities.NotifyInfo.newBuilder()
+ .addAllAccounts(ImmutableList.of("account1", "account2"))
+ .build();
+ assertThat(proto).isEqualTo(expectedProto);
+ }
+
+ @Test
+ public void allValuesConvertedToProtoAndBackAgain() {
+ NotifyInfo notifyInfo = new NotifyInfo(ImmutableList.of("account1", "account2"));
+
+ NotifyInfo convertedNotifyInfo =
+ notifyInfoProtoConverter.fromProto(notifyInfoProtoConverter.toProto(notifyInfo));
+
+ assertThat(convertedNotifyInfo).isEqualTo(notifyInfo);
+ }
+
+ /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
+ @Test
+ public void methodsExistAsExpected() {
+ assertThatSerializedClass(NotifyInfo.class)
+ .hasFields(
+ ImmutableMap.<String, Type>builder()
+ .put("accounts", new TypeLiteral<List<String>>() {}.getType())
+ .build());
+ }
+}
diff --git a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
index 3704969817..f8e7d80741 100644
--- a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
+++ b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
@@ -584,7 +584,8 @@ public final class ChangeInfoDifferTest {
@Test
public void getDiff_assertCanConstructAllChangeInfoReferences() throws Exception {
- buildObjectWithFullFields(ChangeInfo.class);
+ @SuppressWarnings("unused")
+ var unused = buildObjectWithFullFields(ChangeInfo.class);
}
@Test
diff --git a/javatests/com/google/gerrit/git/ObjectIdsTest.java b/javatests/com/google/gerrit/git/ObjectIdsTest.java
index b254d6f78a..a011bbaa2b 100644
--- a/javatests/com/google/gerrit/git/ObjectIdsTest.java
+++ b/javatests/com/google/gerrit/git/ObjectIdsTest.java
@@ -41,7 +41,11 @@ public class ObjectIdsTest {
@Test
public void abbreviateNameDefaultLength() throws Exception {
- assertRuntimeException(() -> abbreviateName(null));
+ assertRuntimeException(
+ () -> {
+ @SuppressWarnings("unused")
+ var unused = abbreviateName(null);
+ });
assertThat(abbreviateName(ID)).isEqualTo("0000000");
assertThat(abbreviateName(AMBIGUOUS_BLOB_ID)).isEqualTo(abbreviateName(ID));
assertThat(abbreviateName(AMBIGUOUS_TREE_ID)).isEqualTo(abbreviateName(ID));
@@ -49,17 +53,37 @@ public class ObjectIdsTest {
@Test
public void abbreviateNameCustomLength() throws Exception {
- assertRuntimeException(() -> abbreviateName(null, 1));
- assertRuntimeException(() -> abbreviateName(ID, -1));
- assertRuntimeException(() -> abbreviateName(ID, 0));
- assertRuntimeException(() -> abbreviateName(ID, 41));
+ assertRuntimeException(
+ () -> {
+ @SuppressWarnings("unused")
+ var unused = abbreviateName(null, 1);
+ });
+ assertRuntimeException(
+ () -> {
+ @SuppressWarnings("unused")
+ var unused = abbreviateName(ID, -1);
+ });
+ assertRuntimeException(
+ () -> {
+ @SuppressWarnings("unused")
+ var unused = abbreviateName(ID, 0);
+ });
+ assertRuntimeException(
+ () -> {
+ @SuppressWarnings("unused")
+ var unused = abbreviateName(ID, 41);
+ });
assertThat(abbreviateName(ID, 5)).isEqualTo("00000");
assertThat(abbreviateName(ID, 40)).isEqualTo(ID.name());
}
@Test
public void abbreviateNameDefaultLengthWithReader() throws Exception {
- assertRuntimeException(() -> abbreviateName(ID, null));
+ assertRuntimeException(
+ () -> {
+ @SuppressWarnings("unused")
+ var unused = abbreviateName(ID, null);
+ });
ObjectReader reader = newReaderWithAmbiguousIds();
assertThat(abbreviateName(ID, reader)).isEqualTo("00000000001");
@@ -68,10 +92,26 @@ public class ObjectIdsTest {
@Test
public void abbreviateNameCustomLengthWithReader() throws Exception {
ObjectReader reader = newReaderWithAmbiguousIds();
- assertRuntimeException(() -> abbreviateName(ID, -1, reader));
- assertRuntimeException(() -> abbreviateName(ID, 0, reader));
- assertRuntimeException(() -> abbreviateName(ID, 41, reader));
- assertRuntimeException(() -> abbreviateName(ID, 5, null));
+ assertRuntimeException(
+ () -> {
+ @SuppressWarnings("unused")
+ var unused = abbreviateName(ID, -1, reader);
+ });
+ assertRuntimeException(
+ () -> {
+ @SuppressWarnings("unused")
+ var unused = abbreviateName(ID, 0, reader);
+ });
+ assertRuntimeException(
+ () -> {
+ @SuppressWarnings("unused")
+ var unused = abbreviateName(ID, 41, reader);
+ });
+ assertRuntimeException(
+ () -> {
+ @SuppressWarnings("unused")
+ var unused = abbreviateName(ID, 5, null);
+ });
String shortest = "00000000001";
assertThat(abbreviateName(ID, 1, reader)).isEqualTo(shortest);
diff --git a/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java b/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
index 05e9808dbb..1f6259f4d7 100644
--- a/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
+++ b/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
@@ -28,6 +28,7 @@ import static org.eclipse.jgit.lib.RefUpdate.Result.NEW;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
import com.google.gerrit.gpg.testing.TestKey;
@@ -112,7 +113,8 @@ public class GerritPublicKeyCheckerTest {
.update("Set Preferred Email", userId, u -> u.setPreferredEmail("user@example.com"));
user = reloadUser();
- requestContext.setContext(() -> user);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(() -> user);
storeRepo = new InMemoryRepository(new DfsRepositoryDescription("repo"));
store = new PublicKeyStore(storeRepo);
@@ -130,6 +132,7 @@ public class GerritPublicKeyCheckerTest {
return userFactory.create(id);
}
+ @CanIgnoreReturnValue
private IdentifiedUser reloadUser() {
user = userFactory.create(userId);
return user;
@@ -389,6 +392,7 @@ public class GerritPublicKeyCheckerTest {
accountsUpdateProvider.get().update("Add External IDs", id, u -> u.addExternalIds(newExtIds));
}
+ @CanIgnoreReturnValue
private TestKey add(TestKey k, IdentifiedUser user) throws Exception {
add(k.getPublicKeyRing(), user);
return k;
diff --git a/javatests/com/google/gerrit/gpg/PublicKeyCheckerTest.java b/javatests/com/google/gerrit/gpg/PublicKeyCheckerTest.java
index c360b2f46b..6faebab522 100644
--- a/javatests/com/google/gerrit/gpg/PublicKeyCheckerTest.java
+++ b/javatests/com/google/gerrit/gpg/PublicKeyCheckerTest.java
@@ -37,6 +37,7 @@ import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_KEY;
import static org.bouncycastle.openpgp.PGPSignature.DIRECT_KEY;
import static org.junit.Assert.assertEquals;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.gpg.testing.TestKey;
import java.time.Instant;
import java.time.ZoneId;
@@ -298,6 +299,7 @@ public class PublicKeyCheckerTest {
return new PublicKeyChecker().enableTrust(maxTrustDepth, fps).setStore(store);
}
+ @CanIgnoreReturnValue
private TestKey add(TestKey k) {
store.add(k.getPublicKeyRing());
return k;
diff --git a/javatests/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java b/javatests/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
index 49322485c4..41b2b9fc78 100644
--- a/javatests/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
+++ b/javatests/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
@@ -21,6 +21,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
import com.google.gerrit.server.plugins.Plugin;
@@ -76,6 +77,7 @@ public class AllRequestFilterFilterProxyTest {
* <p>This method adds the given filter to all {@link AllRequestFilter.FilterProxy} instances
* created by {@link #getFilterProxy()}.
*/
+ @CanIgnoreReturnValue
private ReloadableRegistrationHandle<AllRequestFilter> addFilter(AllRequestFilter filter) {
Key<AllRequestFilter> key = Key.get(AllRequestFilter.class);
return filters.add("gerrit", key, Providers.of(filter));
diff --git a/javatests/com/google/gerrit/httpd/gitweb/GitwebServletTest.java b/javatests/com/google/gerrit/httpd/gitweb/GitwebServletTest.java
index d1598b9b90..257a5a13ac 100644
--- a/javatests/com/google/gerrit/httpd/gitweb/GitwebServletTest.java
+++ b/javatests/com/google/gerrit/httpd/gitweb/GitwebServletTest.java
@@ -71,7 +71,7 @@ public class GitwebServletTest {
gitWebConfig = mock(GitwebConfig.class);
allProjectsName = new AllProjectsName(AllProjectsNameProvider.DEFAULT);
// All-Projects must exist prior to calling GitwebServlet ctor
- repoManager.createRepository(allProjectsName);
+ repoManager.createRepository(allProjectsName).close();
servlet =
new GitwebServlet(
repoManager,
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
index c06d231804..6f8e73a583 100644
--- a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
@@ -116,7 +116,7 @@ public class IndexHtmlUtilTest {
assertThat(dynamicTemplateData(gerritApi, "/c/project/+/123"))
.containsAtLeast(
- "defaultChangeDetailHex", "9916394",
+ "defaultChangeDetailHex", "9996394",
"changeRequestsPath", "changes/project~123");
}
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexPreloadingUtilTest.java b/javatests/com/google/gerrit/httpd/raw/IndexPreloadingUtilTest.java
index 9f8f4941b1..291a064c50 100644
--- a/javatests/com/google/gerrit/httpd/raw/IndexPreloadingUtilTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/IndexPreloadingUtilTest.java
@@ -15,7 +15,6 @@
package com.google.gerrit.httpd.raw;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.httpd.raw.IndexPreloadingUtil.computeChangeRequestsPath;
import static com.google.gerrit.httpd.raw.IndexPreloadingUtil.parseRequestedPage;
diff --git a/javatests/com/google/gerrit/httpd/raw/StaticModuleTest.java b/javatests/com/google/gerrit/httpd/raw/StaticModuleTest.java
new file mode 100644
index 0000000000..28ec30ddad
--- /dev/null
+++ b/javatests/com/google/gerrit/httpd/raw/StaticModuleTest.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.httpd.raw;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+
+public class StaticModuleTest {
+
+ @Test
+ public void doNotMatchPolyGerritIndex() {
+ ImmutableList.of(
+ "/c/123456/anyString",
+ "/123456/anyString",
+ "/c/123456/comment/9ab75172_67d798e1",
+ "/123456/comment/9ab75172_67d798e1")
+ .forEach(url -> assertThat(StaticModule.PolyGerritFilter.isPolyGerritIndex(url)).isFalse());
+ }
+}
diff --git a/javatests/com/google/gerrit/index/IndexUpgradeTest.java b/javatests/com/google/gerrit/index/IndexUpgradeTest.java
index 724964b741..27abdad753 100644
--- a/javatests/com/google/gerrit/index/IndexUpgradeTest.java
+++ b/javatests/com/google/gerrit/index/IndexUpgradeTest.java
@@ -20,7 +20,6 @@ import com.google.gerrit.index.project.ProjectSchemaDefinitions;
import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
-import java.util.Collection;
import java.util.Map.Entry;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -43,7 +42,7 @@ public class IndexUpgradeTest {
@Parameter public SchemaDefinitions<?> schemaDefinitions;
@Parameters(name = "schema: {0}")
- public static Collection<SchemaDefinitions<?>> indexes() {
+ public static ImmutableList<SchemaDefinitions<?>> indexes() {
return ImmutableList.of(
AccountSchemaDefinitions.INSTANCE,
ChangeSchemaDefinitions.INSTANCE,
diff --git a/javatests/com/google/gerrit/index/SchemaUtilTest.java b/javatests/com/google/gerrit/index/SchemaUtilTest.java
index 4f67f8e361..a92c13e30a 100644
--- a/javatests/com/google/gerrit/index/SchemaUtilTest.java
+++ b/javatests/com/google/gerrit/index/SchemaUtilTest.java
@@ -21,7 +21,7 @@ import static com.google.gerrit.index.SchemaUtil.schema;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.collect.ImmutableList;
-import java.util.Map;
+import com.google.common.collect.ImmutableMap;
import org.eclipse.jgit.lib.PersonIdent;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -53,7 +53,8 @@ public class SchemaUtilTest {
@Test
public void schemasFromClassBuildsMap() {
- Map<Integer, Schema<String>> all = SchemaUtil.schemasFromClass(TestSchemas.class, String.class);
+ ImmutableMap<Integer, Schema<String>> all =
+ SchemaUtil.schemasFromClass(TestSchemas.class, String.class);
assertThat(all.keySet()).containsExactly(1, 2, 4);
assertThat(all.get(1)).isEqualTo(TestSchemas.V1);
assertThat(all.get(2)).isEqualTo(TestSchemas.V2);
diff --git a/javatests/com/google/gerrit/index/query/AndPredicateTest.java b/javatests/com/google/gerrit/index/query/AndPredicateTest.java
index 860a9fd2d4..fc5303127d 100644
--- a/javatests/com/google/gerrit/index/query/AndPredicateTest.java
+++ b/javatests/com/google/gerrit/index/query/AndPredicateTest.java
@@ -98,8 +98,8 @@ public class AndPredicateTest extends PredicateTest {
final TestPredicate<String> a = f("author", "alice");
final TestPredicate<String> b = f("author", "bob");
final TestPredicate<String> c = f("author", "charlie");
- final List<TestPredicate<String>> s2 = ImmutableList.of(a, b);
- final List<TestPredicate<String>> s3 = ImmutableList.of(a, b, c);
+ final ImmutableList<TestPredicate<String>> s2 = ImmutableList.of(a, b);
+ final ImmutableList<TestPredicate<String>> s3 = ImmutableList.of(a, b, c);
final Predicate<String> n2 = and(a, b);
assertNotSame(n2, n2.copy(s2));
diff --git a/javatests/com/google/gerrit/index/query/OrPredicateTest.java b/javatests/com/google/gerrit/index/query/OrPredicateTest.java
index e5c9672b7e..4b00a52063 100644
--- a/javatests/com/google/gerrit/index/query/OrPredicateTest.java
+++ b/javatests/com/google/gerrit/index/query/OrPredicateTest.java
@@ -98,8 +98,8 @@ public class OrPredicateTest extends PredicateTest {
final TestPredicate<String> a = f("author", "alice");
final TestPredicate<String> b = f("author", "bob");
final TestPredicate<String> c = f("author", "charlie");
- final List<TestPredicate<String>> s2 = ImmutableList.of(a, b);
- final List<TestPredicate<String>> s3 = ImmutableList.of(a, b, c);
+ final ImmutableList<TestPredicate<String>> s2 = ImmutableList.of(a, b);
+ final ImmutableList<TestPredicate<String>> s3 = ImmutableList.of(a, b, c);
final Predicate<String> n2 = or(a, b);
assertNotSame(n2, n2.copy(s2));
diff --git a/javatests/com/google/gerrit/index/query/QueryBuilderTest.java b/javatests/com/google/gerrit/index/query/QueryBuilderTest.java
index eb3358d753..42148dab37 100644
--- a/javatests/com/google/gerrit/index/query/QueryBuilderTest.java
+++ b/javatests/com/google/gerrit/index/query/QueryBuilderTest.java
@@ -114,7 +114,8 @@ public class QueryBuilderTest {
private static ThrowableSubject assertThatParseException(String query) {
try {
- new TestQueryBuilder().parse(query);
+ @SuppressWarnings("unused")
+ var unused = new TestQueryBuilder().parse(query);
throw new AssertionError("expected QueryParseException for " + query);
} catch (QueryParseException e) {
return assertThat(e);
diff --git a/javatests/com/google/gerrit/index/query/QueryProcessorTest.java b/javatests/com/google/gerrit/index/query/QueryProcessorTest.java
new file mode 100644
index 0000000000..38c610a347
--- /dev/null
+++ b/javatests/com/google/gerrit/index/query/QueryProcessorTest.java
@@ -0,0 +1,151 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.index.query;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+
+import com.google.gerrit.index.IndexCollection;
+import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.IndexRewriter;
+import com.google.gerrit.index.SchemaDefinitions;
+import java.util.function.IntSupplier;
+import org.junit.Test;
+import org.mockito.Mock;
+
+public class QueryProcessorTest {
+
+ private boolean noLimit = false;
+
+ private int userQueryLimit = 1000;
+
+ private int userProvidedLimit = 0;
+
+ private int maxLimit = 5000;
+
+ private int defaultLimit = Integer.MAX_VALUE;
+
+ private String limitField = null;
+
+ @Mock private SchemaDefinitions<String> schemaDef;
+
+ @Mock private IndexCollection<?, String, ?> indexes;
+
+ @Mock private IndexRewriter<String> rewriter;
+
+ public QueryProcessor<String> createProcessor() {
+ QueryProcessor.Metrics metrics = mock(QueryProcessor.Metrics.class);
+ IndexConfig indexConfig =
+ IndexConfig.builder().maxLimit(maxLimit).defaultLimit(defaultLimit).build();
+ IntSupplier userQueryLimit =
+ new IntSupplier() {
+ @Override
+ public int getAsInt() {
+ return QueryProcessorTest.this.userQueryLimit;
+ }
+ };
+
+ QueryProcessor<String> processor =
+ new QueryProcessor<>(
+ metrics, schemaDef, indexConfig, indexes, rewriter, limitField, userQueryLimit) {
+ @Override
+ protected Predicate<String> enforceVisibility(Predicate<String> pred) {
+ return Predicate.any();
+ }
+
+ @Override
+ protected String formatForLogging(String o) {
+ return "";
+ }
+ };
+ processor.setNoLimit(noLimit);
+ processor.setUserProvidedLimit(userProvidedLimit, /* applyDefaultLimit */ true);
+ return processor;
+ }
+
+ @Test
+ public void getEffectiveLimit() {
+ assertThat(createProcessor().getEffectiveLimit(Predicate.any())).isEqualTo(1000);
+ }
+
+ @Test
+ public void getEffectiveLimit_UserQueryLimit() {
+ userQueryLimit = 314;
+ assertThat(createProcessor().getEffectiveLimit(Predicate.any())).isEqualTo(314);
+ }
+
+ @Test
+ public void getEffectiveLimit_UserProvidedLimit() {
+ userProvidedLimit = 314;
+ assertThat(createProcessor().getEffectiveLimit(Predicate.any())).isEqualTo(314);
+ }
+
+ @Test
+ public void getEffectiveLimit_MaxLimit() {
+ maxLimit = 314;
+ assertThat(createProcessor().getEffectiveLimit(Predicate.any())).isEqualTo(314);
+ }
+
+ @Test
+ public void getEffectiveLimit_DefaultLimit() {
+ defaultLimit = 314;
+ assertThat(createProcessor().getEffectiveLimit(Predicate.any())).isEqualTo(314);
+
+ // Prefer user provided limit over default limit.
+ userProvidedLimit = 333;
+ assertThat(createProcessor().getEffectiveLimit(Predicate.any())).isEqualTo(333);
+ }
+
+ @Test
+ public void getEffectiveLimit_LimitField() throws QueryParseException {
+ limitField = "limit";
+ assertThat(createProcessor().getEffectiveLimit(new LimitPredicate<>(limitField, 314)))
+ .isEqualTo(314);
+ }
+
+ @Test
+ public void getEffectiveLimit_SmallestWins() throws QueryParseException {
+ limitField = "limit";
+ int[] limits = {271, 314, 499, 666};
+
+ LimitPredicate<String> p = null;
+ for (int i = 0; i < 4; i++) {
+ userProvidedLimit = limits[0];
+ userQueryLimit = limits[1];
+ maxLimit = limits[2];
+ p = new LimitPredicate<>(limitField, limits[3]);
+
+ // "rotate" the array of limits
+ int l = limits[0];
+ for (int j = 0; j < 3; j++) limits[j] = limits[j + 1];
+ limits[3] = l;
+
+ assertThat(createProcessor().getEffectiveLimit(p)).isEqualTo(271);
+ }
+ }
+
+ @Test
+ public void getEffectiveLimit_NoLimit() {
+ noLimit = true;
+ assertThat(createProcessor().getEffectiveLimit(Predicate.any())).isEqualTo(Integer.MAX_VALUE);
+
+ // noLimit has precedence over all other limits
+ userProvidedLimit = 1;
+ userQueryLimit = 1;
+ maxLimit = 1;
+ defaultLimit = 1;
+ assertThat(createProcessor().getEffectiveLimit(Predicate.any())).isEqualTo(Integer.MAX_VALUE);
+ }
+}
diff --git a/javatests/com/google/gerrit/integration/git/GitProtocolV2IT.java b/javatests/com/google/gerrit/integration/git/GitProtocolV2IT.java
index d40f2a149d..f33dc8d6fb 100644
--- a/javatests/com/google/gerrit/integration/git/GitProtocolV2IT.java
+++ b/javatests/com/google/gerrit/integration/git/GitProtocolV2IT.java
@@ -127,11 +127,11 @@ public class GitProtocolV2IT extends StandaloneSiteTest {
"ssh://%s@" + sshAddress.getHostName() + ":" + sshAddress.getPort() + "/" + project.get();
// Admin user was already created by the base class
- setUpUserAuthentication(admin.username());
+ setUpUserAuthentication(admin.username(), admin.id().get());
// Create non-admin user
TestAccount user = accountCreator.user1();
- setUpUserAuthentication(user.username());
+ setUpUserAuthentication(user.username(), user.id().get());
// Prepare data for new change on master branch
ChangeInput in = new ChangeInput(project.get(), "master", "Test public change");
@@ -271,7 +271,7 @@ public class GitProtocolV2IT extends StandaloneSiteTest {
ctx.getInjector().injectMembers(this);
// Setup admin password
- gApi.accounts().id(admin.username()).setHttpPassword(ADMIN_PASSWORD);
+ gApi.accounts().id(admin.id().get()).setHttpPassword(ADMIN_PASSWORD);
// Get authenticated Git/HTTP URL
String urlWithCredentials =
@@ -336,7 +336,7 @@ public class GitProtocolV2IT extends StandaloneSiteTest {
ctx.getInjector().injectMembers(this);
// Setup admin password
- gApi.accounts().id(admin.username()).setHttpPassword(ADMIN_PASSWORD);
+ gApi.accounts().id(admin.id().get()).setHttpPassword(ADMIN_PASSWORD);
// Get authenticated Git/HTTP URL
String urlWithCredentials =
@@ -405,7 +405,7 @@ public class GitProtocolV2IT extends StandaloneSiteTest {
ctx.getInjector().injectMembers(this);
// Setup admin password
- gApi.accounts().id(admin.username()).setHttpPassword(ADMIN_PASSWORD);
+ gApi.accounts().id(admin.id().get()).setHttpPassword(ADMIN_PASSWORD);
// Get authenticated Git/HTTP URL
String urlWithCredentials =
@@ -488,7 +488,7 @@ public class GitProtocolV2IT extends StandaloneSiteTest {
ctx.getInjector().injectMembers(this);
// Setup admin password
- gApi.accounts().id(admin.username()).setHttpPassword(ADMIN_PASSWORD);
+ gApi.accounts().id(admin.id().get()).setHttpPassword(ADMIN_PASSWORD);
String url = config.getString("gerrit", null, "canonicalweburl");
@@ -560,9 +560,9 @@ public class GitProtocolV2IT extends StandaloneSiteTest {
}
}
- private void setUpUserAuthentication(String username) throws Exception {
+ private void setUpUserAuthentication(String username, int accountId) throws Exception {
// Assign HTTP password to user
- gApi.accounts().id(username).setHttpPassword(ADMIN_PASSWORD);
+ gApi.accounts().id(accountId).setHttpPassword(ADMIN_PASSWORD);
// Generate private/public key for user
execute(
@@ -573,7 +573,7 @@ public class GitProtocolV2IT extends StandaloneSiteTest {
// Read the content of generated public key and add it for the user in Gerrit
gApi.accounts()
- .id(username)
+ .id(accountId)
.addSshKey(
new String(
java.nio.file.Files.readAllBytes(
diff --git a/javatests/com/google/gerrit/integration/git/NoAccessSameAsNotFoundIT.java b/javatests/com/google/gerrit/integration/git/NoAccessSameAsNotFoundIT.java
index ad8a4869eb..78fd35f553 100644
--- a/javatests/com/google/gerrit/integration/git/NoAccessSameAsNotFoundIT.java
+++ b/javatests/com/google/gerrit/integration/git/NoAccessSameAsNotFoundIT.java
@@ -65,7 +65,7 @@ public class NoAccessSameAsNotFoundIT extends StandaloneSiteTest {
ctx.getInjector().injectMembers(this);
TestAccount user = accountCreator.user1();
- gApi.accounts().id(user.username()).setHttpPassword(PASSWORD);
+ gApi.accounts().id(user.id().get()).setHttpPassword(PASSWORD);
String canonical = config.getString("gerrit", null, "canonicalweburl");
repoUrl =
diff --git a/javatests/com/google/gerrit/integration/git/PushToRefsUsersIT.java b/javatests/com/google/gerrit/integration/git/PushToRefsUsersIT.java
index c42f00d738..6ccc7cd0a9 100644
--- a/javatests/com/google/gerrit/integration/git/PushToRefsUsersIT.java
+++ b/javatests/com/google/gerrit/integration/git/PushToRefsUsersIT.java
@@ -55,7 +55,7 @@ public class PushToRefsUsersIT extends StandaloneSiteTest {
ctx.getInjector().injectMembers(this);
// Setup admin password
- gApi.accounts().id(admin.username()).setHttpPassword(ADMIN_PASSWORD);
+ gApi.accounts().id(admin.id().get()).setHttpPassword(ADMIN_PASSWORD);
// Get authenticated Git/HTTP URL
String urlWithCredentials =
diff --git a/javatests/com/google/gerrit/integration/git/UploadArchiveIT.java b/javatests/com/google/gerrit/integration/git/UploadArchiveIT.java
index c720905b10..573c050d76 100644
--- a/javatests/com/google/gerrit/integration/git/UploadArchiveIT.java
+++ b/javatests/com/google/gerrit/integration/git/UploadArchiveIT.java
@@ -110,12 +110,12 @@ public class UploadArchiveIT extends StandaloneSiteTest {
outputPath);
try (InputStream fi = Files.newInputStream(outputPath);
InputStream bi = new BufferedInputStream(fi);
- ArchiveInputStream archive = archiveStreamForFormat(bi, format)) {
+ ArchiveInputStream<?> archive = archiveStreamForFormat(bi, format)) {
assertEntries(archive);
}
}
- private ArchiveInputStream archiveStreamForFormat(InputStream bi, String format)
+ private ArchiveInputStream<?> archiveStreamForFormat(InputStream bi, String format)
throws IOException {
switch (format) {
case "zip":
@@ -154,7 +154,7 @@ public class UploadArchiveIT extends StandaloneSiteTest {
.add(String.format("id_rsa_%s", admin.username()))
.build());
gApi.accounts()
- .id(admin.username())
+ .id(admin.id().get())
.addSshKey(
new String(
java.nio.file.Files.readAllBytes(
diff --git a/javatests/com/google/gerrit/integration/ssh/NoShellIT.java b/javatests/com/google/gerrit/integration/ssh/NoShellIT.java
index 2bbbf1a30e..f25db6e32c 100644
--- a/javatests/com/google/gerrit/integration/ssh/NoShellIT.java
+++ b/javatests/com/google/gerrit/integration/ssh/NoShellIT.java
@@ -66,7 +66,7 @@ public class NoShellIT extends StandaloneSiteTest {
.add(String.format("id_rsa_%s", "admin"))
.build());
gApi.accounts()
- .id("admin")
+ .id(admin.id().get())
.addSshKey(
new String(
java.nio.file.Files.readAllBytes(
diff --git a/javatests/com/google/gerrit/mail/MailHeaderParserTest.java b/javatests/com/google/gerrit/mail/MailHeaderParserTest.java
index 7e3edab76b..3e1c046c31 100644
--- a/javatests/com/google/gerrit/mail/MailHeaderParserTest.java
+++ b/javatests/com/google/gerrit/mail/MailHeaderParserTest.java
@@ -39,7 +39,7 @@ public class MailHeaderParserTest {
b.addAdditionalHeader(
MailHeader.COMMENT_DATE.fieldWithDelimiter() + "Tue, 25 Oct 2016 02:11:35 -0700");
- Address author = Address.create("Diffy", "test@gerritcodereview.com");
+ Address author = Address.create("Diffy", "test@example.com");
b.from(author);
MailMetadata meta = MailHeaderParser.parse(b.build());
@@ -72,7 +72,7 @@ public class MailHeaderParserTest {
.append("Tue, 25 Oct 2016 02:11:35 -0700\r\n");
b.textContent(stringBuilder.toString());
- Address author = Address.create("Diffy", "test@gerritcodereview.com");
+ Address author = Address.create("Diffy", "test@example.com");
b.from(author);
MailMetadata meta = MailHeaderParser.parse(b.build());
@@ -113,7 +113,7 @@ public class MailHeaderParserTest {
.append("</div>");
b.htmlContent(stringBuilder.toString());
- Address author = Address.create("Diffy", "test@gerritcodereview.com");
+ Address author = Address.create("Diffy", "test@example.com");
b.from(author);
MailMetadata meta = MailHeaderParser.parse(b.build());
diff --git a/javatests/com/google/gerrit/metrics/FieldSanitizeProjectNameTest.java b/javatests/com/google/gerrit/metrics/FieldSanitizeProjectNameTest.java
index f12b1b3c97..196a1b4972 100644
--- a/javatests/com/google/gerrit/metrics/FieldSanitizeProjectNameTest.java
+++ b/javatests/com/google/gerrit/metrics/FieldSanitizeProjectNameTest.java
@@ -17,7 +17,7 @@ package com.google.gerrit.metrics;
import static com.google.common.truth.Truth.assertThat;
import java.util.Arrays;
-import java.util.Collection;
+import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -25,7 +25,7 @@ import org.junit.runners.Parameterized;
@RunWith(Parameterized.class)
public class FieldSanitizeProjectNameTest {
@Parameterized.Parameters
- public static Collection<Object[]> testData() {
+ public static List<Object[]> testData() {
return Arrays.asList(
new Object[][] {
{"repoName", "repoName"},
diff --git a/javatests/com/google/gerrit/metrics/dropwizard/DropWizardMetricMakerTest.java b/javatests/com/google/gerrit/metrics/dropwizard/DropWizardMetricMakerTest.java
index a50520aca9..359f91539f 100644
--- a/javatests/com/google/gerrit/metrics/dropwizard/DropWizardMetricMakerTest.java
+++ b/javatests/com/google/gerrit/metrics/dropwizard/DropWizardMetricMakerTest.java
@@ -85,9 +85,13 @@ public class DropWizardMetricMakerTest {
public void shouldRequestForReservoirForNewTimer() throws Exception {
when(reservoirConfigMock.reservoirType()).thenReturn(ReservoirType.ExponentiallyDecaying);
- metrics.newTimer(
- "foo",
- new Description("foo description").setCumulative().setUnit(Description.Units.MILLISECONDS));
+ @SuppressWarnings("unused")
+ var unused =
+ metrics.newTimer(
+ "foo",
+ new Description("foo description")
+ .setCumulative()
+ .setUnit(Description.Units.MILLISECONDS));
verify(reservoirConfigMock).reservoirType();
}
diff --git a/javatests/com/google/gerrit/pgm/BUILD b/javatests/com/google/gerrit/pgm/BUILD
index 0fe4fad1c0..43b86cbcd5 100644
--- a/javatests/com/google/gerrit/pgm/BUILD
+++ b/javatests/com/google/gerrit/pgm/BUILD
@@ -7,6 +7,7 @@ junit_tests(
deps = [
"//java/com/google/gerrit/pgm/http/jetty",
"//java/com/google/gerrit/pgm/init/api",
+ "//java/com/google/gerrit/pgm/util",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server/securestore/testing",
"//lib:guava",
diff --git a/javatests/com/google/gerrit/pgm/util/LogFileManagerTest.java b/javatests/com/google/gerrit/pgm/util/LogFileManagerTest.java
new file mode 100644
index 0000000000..b3f59cc2bc
--- /dev/null
+++ b/javatests/com/google/gerrit/pgm/util/LogFileManagerTest.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.pgm.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.server.config.SitePaths;
+import java.nio.file.Path;
+import java.time.Instant;
+import java.util.List;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class LogFileManagerTest {
+
+ @Test
+ public void testLogFilePattern() throws Exception {
+ List<String> filenamesWithDate =
+ List.of(
+ "error_log.2024-01-01",
+ "error_log.2024-01-01.gz",
+ "error_log.json.2024-01-01",
+ "error_log.json.2024-01-01.gz",
+ "sshd_log.2024-01-01",
+ "httpd_log.2024-01-01");
+
+ List<String> filenamesWithoutDate =
+ List.of(
+ "error_log",
+ "error_log.gz",
+ "error_log.json",
+ "error_log.json.gz",
+ "sshd_log",
+ "httpd_log");
+
+ LogFileManager manager = new LogFileManager(new SitePaths(Path.of("/gerrit")), new Config());
+ Instant expected = Instant.parse("2024-01-01T00:00:00.00Z");
+ for (String filename : filenamesWithDate) {
+ assertThat(manager.getDateFromFilename(Path.of(filename)).get()).isEqualTo(expected);
+ }
+
+ for (String filename : filenamesWithoutDate) {
+ assertThat(manager.getDateFromFilename(Path.of(filename)).isEmpty()).isTrue();
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index 0c84093f55..fd8ddd2d34 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -59,6 +59,7 @@ junit_tests(
"//java/com/google/gerrit/server/cache/testing",
"//java/com/google/gerrit/server/cancellation",
"//java/com/google/gerrit/server/fixes/testing",
+ "//java/com/google/gerrit/server/git/receive",
"//java/com/google/gerrit/server/git/receive:ref_cache",
"//java/com/google/gerrit/server/ioutil",
"//java/com/google/gerrit/server/logging",
@@ -66,6 +67,7 @@ junit_tests(
"//java/com/google/gerrit/server/restapi",
"//java/com/google/gerrit/server/schema",
"//java/com/google/gerrit/server/schema/testing",
+ "//java/com/google/gerrit/server/util/git",
"//java/com/google/gerrit/server/util/time",
"//java/com/google/gerrit/sshd",
"//java/com/google/gerrit/testing:assertable-executor",
diff --git a/javatests/com/google/gerrit/server/IdentifiedUserTest.java b/javatests/com/google/gerrit/server/IdentifiedUserTest.java
index ce045f7506..f726be322c 100644
--- a/javatests/com/google/gerrit/server/IdentifiedUserTest.java
+++ b/javatests/com/google/gerrit/server/IdentifiedUserTest.java
@@ -103,13 +103,15 @@ public class IdentifiedUserTest {
Account account =
Account.builder(Account.id(1), TimeUtil.now())
.setMetaId("1234567812345678123456781234567812345678")
+ .setUniqueTag("1234567812345678123456781234567812345678")
.build();
Account.Id ownerId = account.id();
identifiedUser = identifiedUserFactory.create(ownerId);
/* Trigger identifiedUser to load the email addresses from mockRealm */
- identifiedUser.getEmailAddresses();
+ @SuppressWarnings("unused")
+ var unused = identifiedUser.getEmailAddresses();
}
@Test
diff --git a/javatests/com/google/gerrit/server/account/AccountCacheTest.java b/javatests/com/google/gerrit/server/account/AccountCacheTest.java
index 662836295f..45eff9a37b 100644
--- a/javatests/com/google/gerrit/server/account/AccountCacheTest.java
+++ b/javatests/com/google/gerrit/server/account/AccountCacheTest.java
@@ -41,12 +41,15 @@ public class AccountCacheTest {
@Test
public void account_roundTrip() throws Exception {
+ // The uniqueTag and metaId can be different (in google internal implementation).
+ // This tests ensures that they are serialized/deserialized separately.
Account account =
Account.builder(Account.id(1), Instant.EPOCH)
.setFullName("foo bar")
.setDisplayName("foo")
.setActive(false)
.setMetaId("dead..beef")
+ .setUniqueTag("dead..beef..tag")
.setStatus("OOO")
.setPreferredEmail("foo@bar.tld")
.build();
@@ -63,6 +66,7 @@ public class AccountCacheTest {
.setDisplayName("foo")
.setInactive(true)
.setMetaId("dead..beef")
+ .setUniqueTag("dead..beef..tag")
.setStatus("OOO")
.setPreferredEmail("foo@bar.tld"))
.build();
@@ -71,6 +75,39 @@ public class AccountCacheTest {
}
@Test
+ public void account_deserializeOldRecordWithoutUniqueTag() throws Exception {
+ Account.Builder builder =
+ Account.builder(Account.id(1), Instant.EPOCH)
+ .setFullName("foo bar")
+ .setDisplayName("foo")
+ .setActive(false)
+ .setMetaId("dead..beef")
+ .setStatus("OOO")
+ .setPreferredEmail("foo@bar.tld");
+ CachedAccountDetails original =
+ CachedAccountDetails.create(builder.build(), ImmutableMap.of(), CachedPreferences.EMPTY);
+ CachedAccountDetails expected =
+ CachedAccountDetails.create(
+ builder.setUniqueTag("dead..beef").build(), ImmutableMap.of(), CachedPreferences.EMPTY);
+ byte[] serialized = SERIALIZER.serialize(original);
+ Cache.AccountDetailsProto expectedProto =
+ Cache.AccountDetailsProto.newBuilder()
+ .setAccount(
+ Cache.AccountProto.newBuilder()
+ .setId(1)
+ .setRegisteredOn(0)
+ .setFullName("foo bar")
+ .setDisplayName("foo")
+ .setInactive(true)
+ .setMetaId("dead..beef")
+ .setStatus("OOO")
+ .setPreferredEmail("foo@bar.tld"))
+ .build();
+ ProtoTruth.assertThat(Cache.AccountDetailsProto.parseFrom(serialized)).isEqualTo(expectedProto);
+ Truth.assertThat(SERIALIZER.deserialize(serialized)).isEqualTo(expected);
+ }
+
+ @Test
public void account_roundTripNullFields() throws Exception {
CachedAccountDetails original =
CachedAccountDetails.create(ACCOUNT, ImmutableMap.of(), CachedPreferences.EMPTY);
diff --git a/javatests/com/google/gerrit/server/account/AccountResolverTest.java b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
index 34f746ad87..a53abfa4cd 100644
--- a/javatests/com/google/gerrit/server/account/AccountResolverTest.java
+++ b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
@@ -351,6 +351,7 @@ public class AccountResolverTest {
return AccountState.forAccount(
Account.builder(Account.id(id), TimeUtil.now())
.setMetaId("1234567812345678123456781234567812345678")
+ .setUniqueTag("1234567812345678123456781234567812345678")
.build());
}
diff --git a/javatests/com/google/gerrit/server/account/WatchConfigTest.java b/javatests/com/google/gerrit/server/account/WatchConfigTest.java
index 7d36b94b08..a277c3b19d 100644
--- a/javatests/com/google/gerrit/server/account/WatchConfigTest.java
+++ b/javatests/com/google/gerrit/server/account/WatchConfigTest.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.account;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.NotifyConfig.NotifyType;
@@ -54,7 +55,7 @@ public class WatchConfigTest implements ValidationError.Sink {
+ "[project \"otherProject\"]\n"
+ " notify = [NEW_PATCHSETS]\n"
+ " notify = * [NEW_PATCHSETS, ALL_COMMENTS]\n");
- Map<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches =
+ ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches =
ProjectWatches.parse(Account.id(1000000), cfg, this);
assertThat(validationErrors).isEmpty();
@@ -88,7 +89,9 @@ public class WatchConfigTest implements ValidationError.Sink {
+ "[project \"otherProject\"]\n"
+ " notify = [NEW_PATCHSETS]\n");
- ProjectWatches.parse(Account.id(1000000), cfg, this);
+ @SuppressWarnings("unused")
+ var unused = ProjectWatches.parse(Account.id(1000000), cfg, this);
+
assertThat(validationErrors).hasSize(1);
assertThat(validationErrors.get(0).getMessage())
.isEqualTo(
@@ -170,7 +173,10 @@ public class WatchConfigTest implements ValidationError.Sink {
private void assertParseNotifyValueFails(String notifyValue) {
assertThat(validationErrors).isEmpty();
- parseNotifyValue(notifyValue);
+
+ @SuppressWarnings("unused")
+ var unused = parseNotifyValue(notifyValue);
+
assertWithMessage("expected validation error for notifyValue: " + notifyValue)
.that(validationErrors)
.isNotEmpty();
diff --git a/javatests/com/google/gerrit/server/cancellation/RequestStateContextTest.java b/javatests/com/google/gerrit/server/cancellation/RequestStateContextTest.java
index 2d3e94a454..d69553339f 100644
--- a/javatests/com/google/gerrit/server/cancellation/RequestStateContextTest.java
+++ b/javatests/com/google/gerrit/server/cancellation/RequestStateContextTest.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.cancellation;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.collect.ImmutableSet;
diff --git a/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java b/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
index 6cbbd2657e..4c5264f283 100644
--- a/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
+++ b/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
@@ -90,7 +90,8 @@ public class LabelNormalizerTest {
userId = accountManager.authenticate(authRequestFactory.createForUser("user")).getAccountId();
user = userFactory.create(userId);
- requestContext.setContext(() -> user);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(() -> user);
configureProject();
setUpChange();
@@ -131,7 +132,8 @@ public class LabelNormalizerTest {
if (lifecycle != null) {
lifecycle.stop();
}
- requestContext.setContext(null);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(null);
}
@Test
diff --git a/javatests/com/google/gerrit/server/change/WalkSorterTest.java b/javatests/com/google/gerrit/server/change/WalkSorterTest.java
index 2c4c98fb5e..ca2163ad9e 100644
--- a/javatests/com/google/gerrit/server/change/WalkSorterTest.java
+++ b/javatests/com/google/gerrit/server/change/WalkSorterTest.java
@@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
@@ -58,7 +59,7 @@ public class WalkSorterTest {
ChangeData cd2 = newChange(p, c2_1);
ChangeData cd3 = newChange(p, c3_1);
- List<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3);
+ ImmutableList<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3);
WalkSorter sorter = new WalkSorter(repoManager);
assertSorted(
@@ -85,6 +86,114 @@ public class WalkSorterTest {
}
@Test
+ public void seriesOfMergeChangesInSpecialOrder() throws Exception {
+ TestRepository<Repo> p = newRepo("p");
+ // For this test, the RevWalk in the sortProject must return commits in the following order:
+ // c3, c1, c2, c4.
+ // To achieve this order, the commit timestamps must be set in a specific order - RevWalk
+ // returns them sorted by timestamp, starting from the newest one.
+
+ // timestamp: base + 3
+ RevCommit c1 = p.commit().tick(3).create();
+ // timestamp: base + 2
+ RevCommit c2 = p.commit().tick(-1).parent(c1).create();
+ // timestamp: base + 4
+ RevCommit c3 = p.commit().tick(2).parent(c1).parent(c2).create();
+ // timestamp: base + 1
+ RevCommit c4 = p.commit().tick(-3).parent(c3).create();
+
+ ChangeData cd1 = newChange(p, c1);
+ ChangeData cd2 = newChange(p, c2);
+ ChangeData cd3 = newChange(p, c3);
+ ChangeData cd4 = newChange(p, c4);
+
+ ImmutableList<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3, cd4);
+ WalkSorter sorter = new WalkSorter(repoManager);
+
+ assertSorted(
+ sorter,
+ changes,
+ ImmutableList.of(
+ patchSetData(cd4, c4),
+ patchSetData(cd3, c3),
+ patchSetData(cd2, c2),
+ patchSetData(cd1, c1)));
+ }
+
+ @Test
+ public void seriesOfMergeChangesWorstCaseForTopoSorting() throws Exception {
+ TestRepository<Repo> p = newRepo("p");
+ // For this test, the RevWalk in the sortProject must return commits in the following order:
+ // c1, c2, c3, c4, c5, c6, c7.
+ // To achieve this order, the commit timestamps must be set in a specific order - RevWalk
+ // returns them sorted by timestamp, starting from the newest one.
+
+ // timestamp: base + 8
+ RevCommit c1 = p.commit().tick(8).create();
+ // timestamp: base + 7
+ RevCommit c2 = p.commit().tick(-1).parent(c1).create();
+ // timestamp: base + 6
+ RevCommit c3 = p.commit().tick(-1).parent(c1).parent(c2).create();
+ // timestamp: base + 5
+ RevCommit c4 = p.commit().tick(-1).parent(c1).parent(c2).parent(c3).create();
+ // timestamp: base + 4
+ RevCommit c5 = p.commit().tick(-1).parent(c1).parent(c2).parent(c3).parent(c4).create();
+ // timestamp: base + 3
+ RevCommit c6 =
+ p.commit().tick(-1).parent(c1).parent(c2).parent(c3).parent(c4).parent(c5).create();
+ // timestamp: base + 2
+ RevCommit c7 =
+ p.commit()
+ .tick(-1)
+ .parent(c1)
+ .parent(c2)
+ .parent(c3)
+ .parent(c4)
+ .parent(c5)
+ .parent(c6)
+ .create();
+ // timestamp: base + 1
+ RevCommit c8 =
+ p.commit()
+ .tick(-1)
+ .parent(c1)
+ .parent(c2)
+ .parent(c3)
+ .parent(c4)
+ .parent(c5)
+ .parent(c6)
+ .parent(c7)
+ .create();
+
+ ChangeData cd1 = newChange(p, c1);
+ ChangeData cd2 = newChange(p, c2);
+ ChangeData cd3 = newChange(p, c3);
+ ChangeData cd4 = newChange(p, c4);
+ ChangeData cd5 = newChange(p, c5);
+ ChangeData cd6 = newChange(p, c6);
+ ChangeData cd7 = newChange(p, c7);
+ ChangeData cd8 = newChange(p, c8);
+
+ ImmutableList<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3, cd4, cd5, cd6, cd7, cd8);
+ WalkSorter sorter = new WalkSorter(repoManager);
+
+ // Do not use assertSorted because it tests all permutations. We don't need it for this test
+ // and total number of permutations is quite big.
+ assertThat(sorter.sort(changes))
+ .containsExactlyElementsIn(
+ ImmutableList.of(
+ patchSetData(cd8, c8),
+ patchSetData(cd7, c7),
+ patchSetData(cd6, c6),
+ patchSetData(cd5, c5),
+ patchSetData(cd4, c4),
+ patchSetData(cd3, c3),
+ patchSetData(cd2, c2),
+ patchSetData(cd1, c1)))
+ .inOrder();
+ }
+
+ @Test
public void subsetOfSeriesOfChanges() throws Exception {
TestRepository<Repo> p = newRepo("p");
RevCommit c1_1 = p.commit().create();
@@ -94,7 +203,7 @@ public class WalkSorterTest {
ChangeData cd1 = newChange(p, c1_1);
ChangeData cd3 = newChange(p, c3_1);
- List<ChangeData> changes = ImmutableList.of(cd1, cd3);
+ ImmutableList<ChangeData> changes = ImmutableList.of(cd1, cd3);
WalkSorter sorter = new WalkSorter(repoManager);
assertSorted(
@@ -121,7 +230,7 @@ public class WalkSorterTest {
ChangeData cd3 = newChange(p, c3);
ChangeData cd4 = newChange(p, c4);
- List<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3, cd4);
+ ImmutableList<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3, cd4);
WalkSorter sorter = new WalkSorter(repoManager);
assertSorted(
@@ -154,7 +263,7 @@ public class WalkSorterTest {
ChangeData cd3 = newChange(p, c3);
ChangeData cd4 = newChange(p, c4);
- List<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3, cd4);
+ ImmutableList<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3, cd4);
WalkSorter sorter = new WalkSorter(repoManager);
assertSorted(
@@ -186,9 +295,9 @@ public class WalkSorterTest {
ChangeData cd2 = newChange(p, c2);
ChangeData cd4 = newChange(p, c4);
- List<ChangeData> changes = ImmutableList.of(cd1, cd2, cd4);
+ ImmutableList<ChangeData> changes = ImmutableList.of(cd1, cd2, cd4);
WalkSorter sorter = new WalkSorter(repoManager);
- List<PatchSetData> expected =
+ ImmutableList<PatchSetData> expected =
ImmutableList.of(patchSetData(cd4, c4), patchSetData(cd2, c2), patchSetData(cd1, c1));
for (List<ChangeData> list : permutations(changes)) {
@@ -217,7 +326,7 @@ public class WalkSorterTest {
ChangeData cd3 = newChange(p, c3);
ChangeData cd4 = newChange(p, c4);
- List<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3, cd4);
+ ImmutableList<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3, cd4);
WalkSorter sorter = new WalkSorter(repoManager);
assertSorted(
@@ -270,7 +379,7 @@ public class WalkSorterTest {
addPatchSet(cd1, c1_2);
addPatchSet(cd2, c2_2);
- List<ChangeData> changes = ImmutableList.of(cd1, cd2);
+ ImmutableList<ChangeData> changes = ImmutableList.of(cd1, cd2);
WalkSorter sorter = new WalkSorter(repoManager);
assertSorted(
@@ -293,7 +402,7 @@ public class WalkSorterTest {
ChangeData cd1 = newChange(pa, c1);
ChangeData cd2 = newChange(pb, c2);
- List<ChangeData> changes = ImmutableList.of(cd1, cd2);
+ ImmutableList<ChangeData> changes = ImmutableList.of(cd1, cd2);
WalkSorter sorter =
new WalkSorter(repoManager).includePatchSets(ImmutableSet.of(cd1.currentPatchSet().id()));
@@ -306,7 +415,7 @@ public class WalkSorterTest {
RevCommit c = p.commit().message("message").create();
ChangeData cd = newChange(p, c);
- List<ChangeData> changes = ImmutableList.of(cd);
+ ImmutableList<ChangeData> changes = ImmutableList.of(cd);
RevCommit actual =
new WalkSorter(repoManager).setRetainBody(true).sort(changes).iterator().next().commit();
assertThat(actual.getRawBuffer()).isNotNull();
@@ -323,7 +432,7 @@ public class WalkSorterTest {
RevCommit c = p.commit().create();
ChangeData cd = newChange(p, c);
- List<ChangeData> changes = ImmutableList.of(cd);
+ ImmutableList<ChangeData> changes = ImmutableList.of(cd);
WalkSorter sorter = new WalkSorter(repoManager);
assertSorted(sorter, changes, ImmutableList.of(patchSetData(cd, c)));
@@ -338,6 +447,7 @@ public class WalkSorterTest {
return cd;
}
+ @CanIgnoreReturnValue
private PatchSet addPatchSet(ChangeData cd, ObjectId id) throws Exception {
TestChanges.incrementPatchSet(cd.change());
PatchSet ps = TestChanges.newPatchSet(cd.change().currentPatchSetId(), id.name(), userId);
diff --git a/javatests/com/google/gerrit/server/config/CachedPreferencesTest.java b/javatests/com/google/gerrit/server/config/CachedPreferencesTest.java
index 772f4b88b2..b149d09a04 100644
--- a/javatests/com/google/gerrit/server/config/CachedPreferencesTest.java
+++ b/javatests/com/google/gerrit/server/config/CachedPreferencesTest.java
@@ -130,6 +130,27 @@ public class CachedPreferencesTest {
}
@Test
+ public void userPreferencesProto_falseValueReturnsAsNull() throws Exception {
+ UserPreferences originalProto =
+ UserPreferences.newBuilder()
+ .setEditPreferencesInfo(
+ UserPreferences.EditPreferencesInfo.newBuilder()
+ .setTabSize(17)
+ .setHideTopMenu(false)
+ .setHideLineNumbers(false)
+ .setAutoCloseBrackets(true))
+ .build();
+
+ CachedPreferences pref = CachedPreferences.fromUserPreferencesProto(originalProto);
+ EditPreferencesInfo edit = CachedPreferences.edit(Optional.empty(), pref);
+
+ assertThat(edit.tabSize).isEqualTo(17);
+ assertThat(edit.hideTopMenu).isNull();
+ assertThat(edit.hideLineNumbers).isNull();
+ assertThat(edit.autoCloseBrackets).isTrue();
+ }
+
+ @Test
public void defaultPreferences_acceptingGitConfig() throws Exception {
Config cfg = new Config();
cfg.fromText("[general]\n\tchangesPerPage = 19");
diff --git a/javatests/com/google/gerrit/server/config/GerritInstanceNameProviderTest.java b/javatests/com/google/gerrit/server/config/GerritInstanceNameProviderTest.java
new file mode 100644
index 0000000000..edf9b8b6c9
--- /dev/null
+++ b/javatests/com/google/gerrit/server/config/GerritInstanceNameProviderTest.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.server.util.git.DelegateSystemReader;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.util.SystemReader;
+import org.junit.Test;
+
+public class GerritInstanceNameProviderTest {
+
+ @Test
+ public void instanceNameSet_canonicalWebUrlUnset_useInstanceName() {
+ Config cfg = new Config();
+ cfg.setString("gerrit", null, "instanceName", "myName");
+
+ GerritInstanceNameProvider provider = new GerritInstanceNameProvider(cfg, null);
+ assertThat(provider.get()).isEqualTo("myName");
+ }
+
+ @Test
+ public void instanceNameSet_canonicalWebUrlSet_useInstanceName() {
+ Config cfg = new Config();
+ cfg.setString("gerrit", null, "instanceName", "myName");
+
+ GerritInstanceNameProvider provider = new GerritInstanceNameProvider(cfg, "http://myhost");
+ assertThat(provider.get()).isEqualTo("myName");
+ }
+
+ @Test
+ public void instanceNameNotSet_canonicalWebUrlSet_useHostFromCanonicalWebUrl() {
+ Config cfg = new Config();
+ GerritInstanceNameProvider provider = new GerritInstanceNameProvider(cfg, "http://myhost");
+ assertThat(provider.get()).isEqualTo("myhost");
+ }
+
+ @Test
+ public void instanceNameNotSet_canonicalWebUrlNotSet_useSystemHostName() {
+ SystemReader oldSystemReader = SystemReader.getInstance();
+ SystemReader.setInstance(
+ new DelegateSystemReader(oldSystemReader) {
+ @Override
+ public String getHostname() {
+ return "systemHostName";
+ }
+ });
+ Config cfg = new Config();
+ GerritInstanceNameProvider provider = new GerritInstanceNameProvider(cfg, null);
+ assertThat(provider.get()).isEqualTo("systemHostName");
+ }
+}
diff --git a/javatests/com/google/gerrit/server/config/RepositoryConfigTest.java b/javatests/com/google/gerrit/server/config/RepositoryConfigTest.java
index d7aae6a0fe..8d93f663f2 100644
--- a/javatests/com/google/gerrit/server/config/RepositoryConfigTest.java
+++ b/javatests/com/google/gerrit/server/config/RepositoryConfigTest.java
@@ -15,13 +15,11 @@
package com.google.gerrit.server.config;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.client.SubmitType;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.List;
import org.eclipse.jgit.lib.Config;
import org.junit.Before;
@@ -191,7 +189,7 @@ public class RepositoryConfigTest {
public void allBasePath() {
ImmutableList<Path> allBasePaths =
ImmutableList.of(
- Paths.get("/someBasePath1"), Paths.get("/someBasePath2"), Paths.get("/someBasePath2"));
+ Path.of("/someBasePath1"), Path.of("/someBasePath2"), Path.of("/someBasePath2"));
configureBasePath("*", allBasePaths.get(0).toString());
configureBasePath("project/*", allBasePaths.get(1).toString());
diff --git a/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java b/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java
index 55f0374b56..341872dbe1 100644
--- a/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java
+++ b/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.config;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
diff --git a/javatests/com/google/gerrit/server/config/SitePathsTest.java b/javatests/com/google/gerrit/server/config/SitePathsTest.java
index 1e5f41d228..1de6c306e3 100644
--- a/javatests/com/google/gerrit/server/config/SitePathsTest.java
+++ b/javatests/com/google/gerrit/server/config/SitePathsTest.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.config;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.gerrit.server.ioutil.HostPlatform;
@@ -23,7 +22,6 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NotDirectoryException;
import java.nio.file.Path;
-import java.nio.file.Paths;
import org.junit.Test;
public class SitePathsTest {
@@ -92,7 +90,7 @@ public class SitePathsTest {
final String pfx = HostPlatform.isWin32() ? "C:/" : "/";
assertThat(site.resolve(pfx + "a")).isNotNull();
- assertThat(site.resolve(pfx + "a")).isEqualTo(Paths.get(pfx + "a"));
+ assertThat(site.resolve(pfx + "a")).isEqualTo(Path.of(pfx + "a"));
}
private static Path random() throws IOException {
diff --git a/javatests/com/google/gerrit/server/config/UserPreferencesConverterTest.java b/javatests/com/google/gerrit/server/config/UserPreferencesConverterTest.java
index c6ca3e4513..8cfcdbd2d1 100644
--- a/javatests/com/google/gerrit/server/config/UserPreferencesConverterTest.java
+++ b/javatests/com/google/gerrit/server/config/UserPreferencesConverterTest.java
@@ -118,6 +118,51 @@ public class UserPreferencesConverterTest {
}
@Test
+ public void generalPreferencesInfo_toProtoTrimsMyMenuSpaces() {
+ GeneralPreferencesInfo info = new GeneralPreferencesInfo();
+ info.my =
+ ImmutableList.of(
+ new com.google.gerrit.extensions.client.MenuItem(
+ " name1 ", " url1 ", " target1 ", " id1 "),
+ new com.google.gerrit.extensions.client.MenuItem(null, " url2 ", null, null));
+ UserPreferences.GeneralPreferencesInfo resProto = GeneralPreferencesInfoConverter.toProto(info);
+ assertThat(resProto)
+ .isEqualTo(
+ UserPreferences.GeneralPreferencesInfo.newBuilder()
+ .addAllMyMenuItems(
+ ImmutableList.of(
+ MenuItem.newBuilder()
+ .setUrl("url1")
+ .setName("name1")
+ .setTarget("target1")
+ .setId("id1")
+ .build(),
+ MenuItem.newBuilder().setUrl("url2").build()))
+ .build());
+ }
+
+ @Test
+ public void generalPreferencesInfo_fromProtoTrimsMyMenuSpaces() {
+ UserPreferences.GeneralPreferencesInfo originalProto =
+ UserPreferences.GeneralPreferencesInfo.newBuilder()
+ .addAllMyMenuItems(
+ ImmutableList.of(
+ MenuItem.newBuilder()
+ .setName(" name1 ")
+ .setUrl(" url1 ")
+ .setTarget(" target1 ")
+ .setId(" id1 ")
+ .build(),
+ MenuItem.newBuilder().setUrl(" url2 ").build()))
+ .build();
+ GeneralPreferencesInfo info = GeneralPreferencesInfoConverter.fromProto(originalProto);
+ assertThat(info.my)
+ .containsExactly(
+ new com.google.gerrit.extensions.client.MenuItem("name1", "url1", "target1", "id1"),
+ new com.google.gerrit.extensions.client.MenuItem(null, "url2", null, null));
+ }
+
+ @Test
public void generalPreferencesInfo_emptyJavaToProto() {
GeneralPreferencesInfo info = new GeneralPreferencesInfo();
UserPreferences.GeneralPreferencesInfo res = GeneralPreferencesInfoConverter.toProto(info);
diff --git a/javatests/com/google/gerrit/server/fixes/fixCalculator/EmptyContentTest.java b/javatests/com/google/gerrit/server/fixes/fixCalculator/EmptyContentTest.java
index 51fbc673a2..1c0ac5e7b0 100644
--- a/javatests/com/google/gerrit/server/fixes/fixCalculator/EmptyContentTest.java
+++ b/javatests/com/google/gerrit/server/fixes/fixCalculator/EmptyContentTest.java
@@ -27,7 +27,7 @@ public class EmptyContentTest {
FixResult fixResult =
FixCalculatorVariousTest.calculateFixSingleReplacement("", 1, 0, 1, 0, "Abc");
assertThat(fixResult).text().isEqualTo("Abc");
- assertThat(fixResult).edits().onlyElement();
+ assertThat(fixResult).edits().hasSize(1);
Edit edit = fixResult.edits.get(0);
assertThat(edit).isInsert(0, 0, 1);
assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 3);
@@ -38,7 +38,7 @@ public class EmptyContentTest {
FixResult fixResult =
FixCalculatorVariousTest.calculateFixSingleReplacement("", 1, 0, 1, 0, "Abc\n");
assertThat(fixResult).text().isEqualTo("Abc\n");
- assertThat(fixResult).edits().onlyElement();
+ assertThat(fixResult).edits().hasSize(1);
Edit edit = fixResult.edits.get(0);
assertThat(edit).isInsert(0, 0, 1);
assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 4);
@@ -49,7 +49,7 @@ public class EmptyContentTest {
FixResult fixResult =
FixCalculatorVariousTest.calculateFixSingleReplacement("", 1, 0, 1, 0, "Abc\nDEFGH");
assertThat(fixResult).text().isEqualTo("Abc\nDEFGH");
- assertThat(fixResult).edits().onlyElement();
+ assertThat(fixResult).edits().hasSize(1);
Edit edit = fixResult.edits.get(0);
assertThat(edit).isInsert(0, 0, 2);
assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 9);
@@ -60,7 +60,7 @@ public class EmptyContentTest {
FixResult fixResult =
FixCalculatorVariousTest.calculateFixSingleReplacement("", 1, 0, 1, 0, "Abc\nDEFGH\n");
assertThat(fixResult).text().isEqualTo("Abc\nDEFGH\n");
- assertThat(fixResult).edits().onlyElement();
+ assertThat(fixResult).edits().hasSize(1);
Edit edit = fixResult.edits.get(0);
assertThat(edit).isInsert(0, 0, 2);
assertThat(edit).internalEdits().onlyElement().isInsert(0, 0, 10);
diff --git a/javatests/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java b/javatests/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
index 12130ea766..71eda9ecba 100644
--- a/javatests/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
+++ b/javatests/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
@@ -199,14 +199,14 @@ public class LocalDiskRepositoryManagerTest {
@Test
public void testProjectRecreation() throws Exception {
- repoManager.createRepository(Project.nameKey("a"));
+ repoManager.createRepository(Project.nameKey("a")).close();
assertThrows(
RepositoryExistsException.class, () -> repoManager.createRepository(Project.nameKey("a")));
}
@Test
public void testProjectRecreationAfterRestart() throws Exception {
- repoManager.createRepository(Project.nameKey("a"));
+ repoManager.createRepository(Project.nameKey("a")).close();
LocalDiskRepositoryManager newRepoManager = new LocalDiskRepositoryManager(site, cfg);
assertThrows(
RepositoryExistsException.class,
@@ -226,7 +226,7 @@ public class LocalDiskRepositoryManagerTest {
@Test
public void testGetRepositoryStatusNameCaseMismatch() throws Exception {
assume().that(HostPlatform.isWin32() || HostPlatform.isMac()).isTrue();
- repoManager.createRepository(Project.nameKey("a"));
+ repoManager.createRepository(Project.nameKey("a")).close();
assertThat(repoManager.getRepositoryStatus(Project.nameKey("A"))).isEqualTo(Status.ACTIVE);
}
@@ -239,7 +239,7 @@ public class LocalDiskRepositoryManagerTest {
@Test
public void testNameCaseMismatch() throws Exception {
assume().that(HostPlatform.isWin32() || HostPlatform.isMac()).isTrue();
- repoManager.createRepository(Project.nameKey("a"));
+ repoManager.createRepository(Project.nameKey("a")).close();
assertThrows(
RepositoryCaseMismatchException.class,
() -> repoManager.createRepository(Project.nameKey("A")));
@@ -249,7 +249,7 @@ public class LocalDiskRepositoryManagerTest {
public void testNameCaseMismatchWithSymlink() throws Exception {
assume().that(HostPlatform.isWin32() || HostPlatform.isMac()).isTrue();
Project.NameKey name = Project.nameKey("a");
- repoManager.createRepository(name);
+ repoManager.createRepository(name).close();
createSymLink(name, "b.git");
assertThrows(
RepositoryCaseMismatchException.class,
@@ -260,7 +260,7 @@ public class LocalDiskRepositoryManagerTest {
public void testNameCaseMismatchAfterRestart() throws Exception {
assume().that(HostPlatform.isWin32() || HostPlatform.isMac()).isTrue();
Project.NameKey name = Project.nameKey("a");
- repoManager.createRepository(name);
+ repoManager.createRepository(name).close();
LocalDiskRepositoryManager newRepoManager = new LocalDiskRepositoryManager(site, cfg);
assertThrows(
diff --git a/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java b/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
index 6c771d74f6..57941490b8 100644
--- a/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
+++ b/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
@@ -25,7 +25,6 @@ import com.google.gerrit.server.config.RepositoryConfig;
import com.google.gerrit.server.config.SitePaths;
import java.io.IOException;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.NavigableSet;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
@@ -62,17 +61,20 @@ public class MultiBaseLocalDiskRepositoryManagerTest {
public void defaultRepositoryLocation()
throws RepositoryCaseMismatchException, RepositoryNotFoundException, IOException {
Project.NameKey someProjectKey = Project.nameKey("someProject");
- Repository repo = repoManager.createRepository(someProjectKey);
- assertThat(repo.getDirectory()).isNotNull();
- assertThat(repo.getDirectory().exists()).isTrue();
- assertThat(repo.getDirectory().getParent())
- .isEqualTo(repoManager.getBasePath(someProjectKey).toAbsolutePath().toString());
- repo = repoManager.openRepository(someProjectKey);
- assertThat(repo.getDirectory()).isNotNull();
- assertThat(repo.getDirectory().exists()).isTrue();
- assertThat(repo.getDirectory().getParent())
- .isEqualTo(repoManager.getBasePath(someProjectKey).toAbsolutePath().toString());
+ try (Repository repo = repoManager.createRepository(someProjectKey)) {
+ assertThat(repo.getDirectory()).isNotNull();
+ assertThat(repo.getDirectory().exists()).isTrue();
+ assertThat(repo.getDirectory().getParent())
+ .isEqualTo(repoManager.getBasePath(someProjectKey).toAbsolutePath().toString());
+ }
+
+ try (Repository repo = repoManager.openRepository(someProjectKey)) {
+ assertThat(repo.getDirectory()).isNotNull();
+ assertThat(repo.getDirectory().exists()).isTrue();
+ assertThat(repo.getDirectory().getParent())
+ .isEqualTo(repoManager.getBasePath(someProjectKey).toAbsolutePath().toString());
+ }
assertThat(repoManager.getBasePath(someProjectKey).toAbsolutePath().toString())
.isEqualTo(repoManager.getBasePath(someProjectKey).toAbsolutePath().toString());
@@ -90,17 +92,19 @@ public class MultiBaseLocalDiskRepositoryManagerTest {
when(configMock.getBasePath(someProjectKey)).thenReturn(alternateBasePath);
when(configMock.getAllBasePaths()).thenReturn(ImmutableList.of(alternateBasePath));
- Repository repo = repoManager.createRepository(someProjectKey);
- assertThat(repo.getDirectory()).isNotNull();
- assertThat(repo.getDirectory().exists()).isTrue();
- assertThat(repo.getDirectory().getParent())
- .isEqualTo(alternateBasePath.toRealPath().toString());
+ try (Repository repo = repoManager.createRepository(someProjectKey)) {
+ assertThat(repo.getDirectory()).isNotNull();
+ assertThat(repo.getDirectory().exists()).isTrue();
+ assertThat(repo.getDirectory().getParent())
+ .isEqualTo(alternateBasePath.toRealPath().toString());
+ }
- repo = repoManager.openRepository(someProjectKey);
- assertThat(repo.getDirectory()).isNotNull();
- assertThat(repo.getDirectory().exists()).isTrue();
- assertThat(repo.getDirectory().getParent())
- .isEqualTo(alternateBasePath.toRealPath().toString());
+ try (Repository repo = repoManager.openRepository(someProjectKey)) {
+ assertThat(repo.getDirectory()).isNotNull();
+ assertThat(repo.getDirectory().exists()).isTrue();
+ assertThat(repo.getDirectory().getParent())
+ .isEqualTo(alternateBasePath.toRealPath().toString());
+ }
assertThat(repoManager.getBasePath(someProjectKey).toAbsolutePath().toString())
.isEqualTo(alternateBasePath.toString());
@@ -124,8 +128,8 @@ public class MultiBaseLocalDiskRepositoryManagerTest {
when(configMock.getBasePath(misplacedProject2)).thenReturn(alternateBasePath);
when(configMock.getAllBasePaths()).thenReturn(ImmutableList.of(alternateBasePath));
- repoManager.createRepository(basePathProject);
- repoManager.createRepository(altPathProject);
+ repoManager.createRepository(basePathProject).close();
+ repoManager.createRepository(altPathProject).close();
// create the misplaced ones without the repomanager otherwise they would
// end up at the proper place.
createRepository(repoManager.getBasePath(basePathProject), misplacedProject2);
@@ -151,7 +155,7 @@ public class MultiBaseLocalDiskRepositoryManagerTest {
IllegalStateException.class,
() -> {
configMock = mock(RepositoryConfig.class);
- when(configMock.getAllBasePaths()).thenReturn(ImmutableList.of(Paths.get("repos")));
+ when(configMock.getAllBasePaths()).thenReturn(ImmutableList.of(Path.of("repos")));
repoManager = new MultiBaseLocalDiskRepositoryManager(site, cfg, configMock);
});
}
diff --git a/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java b/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
index 91d5596730..f2e4f82083 100644
--- a/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
+++ b/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
@@ -17,7 +17,6 @@ package com.google.gerrit.server.git.meta;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Streams;
diff --git a/javatests/com/google/gerrit/server/git/receive/ReceivePackRefCacheTest.java b/javatests/com/google/gerrit/server/git/receive/ReceivePackRefCacheTest.java
index 7eb6bc7965..2d7f69846c 100644
--- a/javatests/com/google/gerrit/server/git/receive/ReceivePackRefCacheTest.java
+++ b/javatests/com/google/gerrit/server/git/receive/ReceivePackRefCacheTest.java
@@ -26,7 +26,6 @@ import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.RefNames;
-import java.util.Map;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectIdRef;
import org.eclipse.jgit.lib.Ref;
@@ -93,7 +92,7 @@ public class ReceivePackRefCacheTest {
@Test
public void advertisedRefs_prefixScansChangeId() throws Exception {
- Map<String, Ref> refs = setupTwoChanges();
+ ImmutableMap<String, Ref> refs = setupTwoChanges();
ReceivePackRefCache cache = ReceivePackRefCache.withAdvertisedRefs(() -> refs);
assertThat(cache.byPrefix(RefNames.changeRefPrefix(Change.id(1))))
@@ -102,7 +101,7 @@ public class ReceivePackRefCacheTest {
@Test
public void advertisedRefs_exactRef() throws Exception {
- Map<String, Ref> refs = setupTwoChanges();
+ ImmutableMap<String, Ref> refs = setupTwoChanges();
ReceivePackRefCache cache = ReceivePackRefCache.withAdvertisedRefs(() -> refs);
assertThat(cache.exactRef("refs/changes/01/1/1")).isEqualTo(refs.get("refs/changes/01/1/1"));
@@ -110,7 +109,7 @@ public class ReceivePackRefCacheTest {
@Test
public void advertisedRefs_patchSetIdsFromObjectId() throws Exception {
- Map<String, Ref> refs = setupTwoChanges();
+ ImmutableMap<String, Ref> refs = setupTwoChanges();
ReceivePackRefCache cache = ReceivePackRefCache.withAdvertisedRefs(() -> refs);
assertThat(
@@ -123,7 +122,7 @@ public class ReceivePackRefCacheTest {
return new ObjectIdRef.Unpeeled(Ref.Storage.NEW, name, ObjectId.fromString(sha1), 1);
}
- private Map<String, Ref> setupTwoChanges() {
+ private ImmutableMap<String, Ref> setupTwoChanges() {
Ref ref1 = newRef("refs/changes/01/1/1", "badc0feebadc0feebadc0feebadc0feebadc0fee");
Ref ref2 = newRef("refs/changes/02/2/1", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
return ImmutableMap.of(ref1.getName(), ref1, ref2.getName(), ref2);
diff --git a/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
index e0f4b63150..c96f3a11b0 100644
--- a/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
+++ b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
@@ -46,7 +46,7 @@ import org.junit.Ignore;
public class AbstractGroupTest {
protected static final ZoneId ZONE_ID = ZoneId.of("America/Los_Angeles");
protected static final String SERVER_ID = "server-id";
- protected static final String SERVER_EMAIL = "noreply@gerritcodereview.com";
+ protected static final String SERVER_EMAIL = "noreply@example.com";
protected static final int SERVER_ACCOUNT_NUMBER = 100000;
protected static final int USER_ACCOUNT_NUMBER = 100001;
diff --git a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
index 8c19732e5d..a5fc114b3a 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
@@ -18,10 +18,10 @@ import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.gerrit.server.group.testing.InternalGroupSubject.internalGroups;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-import static com.google.gerrit.truth.OptionalSubject.assertThat;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
@@ -1063,7 +1063,7 @@ public class GroupConfigTest {
groupConfig.setGroupDelta(groupDelta, auditLogFormatter);
PersonIdent committerIdent =
- new PersonIdent("Jane", "Jane@gerritcodereview.com", committerTimestamp, zoneId);
+ new PersonIdent("Jane", "Jane@example.com", committerTimestamp, zoneId);
try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
metaDataUpdate.getCommitBuilder().setCommitter(committerIdent);
groupConfig.commit(metaDataUpdate);
@@ -1094,8 +1094,7 @@ public class GroupConfigTest {
GroupConfig groupConfig = GroupConfig.createForNewGroup(projectName, repository, groupCreation);
groupConfig.setGroupDelta(groupDelta, auditLogFormatter);
- PersonIdent authorIdent =
- new PersonIdent("Jane", "Jane@gerritcodereview.com", authorTimestamp, zoneId);
+ PersonIdent authorIdent = new PersonIdent("Jane", "Jane@example.com", authorTimestamp, zoneId);
try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
metaDataUpdate.getCommitBuilder().setAuthor(authorIdent);
groupConfig.commit(metaDataUpdate);
@@ -1154,7 +1153,7 @@ public class GroupConfigTest {
groupConfig.setGroupDelta(groupDelta, auditLogFormatter);
PersonIdent committerIdent =
- new PersonIdent("Jane", "Jane@gerritcodereview.com", committerTimestamp, zoneId);
+ new PersonIdent("Jane", "Jane@example.com", committerTimestamp, zoneId);
try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
metaDataUpdate.getCommitBuilder().setCommitter(committerIdent);
groupConfig.commit(metaDataUpdate);
@@ -1180,8 +1179,7 @@ public class GroupConfigTest {
GroupConfig groupConfig = GroupConfig.loadForGroup(projectName, repository, groupUuid);
groupConfig.setGroupDelta(groupDelta, auditLogFormatter);
- PersonIdent authorIdent =
- new PersonIdent("Jane", "Jane@gerritcodereview.com", authorTimestamp, zoneId);
+ PersonIdent authorIdent = new PersonIdent("Jane", "Jane@example.com", authorTimestamp, zoneId);
try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
metaDataUpdate.getCommitBuilder().setAuthor(authorIdent);
groupConfig.commit(metaDataUpdate);
@@ -1499,6 +1497,7 @@ public class GroupConfigTest {
.setId(groupId);
}
+ @CanIgnoreReturnValue
private Optional<InternalGroup> createGroup(InternalGroupCreation groupCreation)
throws Exception {
GroupConfig groupConfig = GroupConfig.createForNewGroup(projectName, repository, groupCreation);
@@ -1506,6 +1505,7 @@ public class GroupConfigTest {
return groupConfig.getLoadedGroup();
}
+ @CanIgnoreReturnValue
private Optional<InternalGroup> createGroup(
InternalGroupCreation groupCreation, GroupDelta groupDelta) throws Exception {
GroupConfig groupConfig = GroupConfig.createForNewGroup(projectName, repository, groupCreation);
@@ -1514,11 +1514,13 @@ public class GroupConfigTest {
return groupConfig.getLoadedGroup();
}
+ @CanIgnoreReturnValue
private Optional<InternalGroup> updateGroup(AccountGroup.UUID uuid, GroupDelta groupDelta)
throws Exception {
return updateGroup(uuid, groupDelta, auditLogFormatter);
}
+ @CanIgnoreReturnValue
private Optional<InternalGroup> updateGroup(
AccountGroup.UUID uuid, GroupDelta groupDelta, AuditLogFormatter auditLogFormatter)
throws Exception {
@@ -1541,7 +1543,7 @@ public class GroupConfigTest {
private MetaDataUpdate createMetaDataUpdate() {
PersonIdent serverIdent =
- new PersonIdent("Gerrit Server", "noreply@gerritcodereview.com", TimeUtil.now(), zoneId);
+ new PersonIdent("Gerrit Server", "noreply@example.com", TimeUtil.now(), zoneId);
MetaDataUpdate metaDataUpdate =
new MetaDataUpdate(
@@ -1596,6 +1598,6 @@ public class GroupConfigTest {
private static OptionalSubject<InternalGroupSubject, InternalGroup> assertThatGroup(
Optional<InternalGroup> loadedGroup) {
- return assertThat(loadedGroup, internalGroups());
+ return OptionalSubject.assertThat(loadedGroup, internalGroups());
}
}
diff --git a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
index 9d8f260517..3658e4b03c 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
@@ -70,7 +70,7 @@ import org.junit.Test;
public class GroupNameNotesTest {
private static final String SERVER_NAME = "Gerrit Server";
- private static final String SERVER_EMAIL = "noreply@gerritcodereview.com";
+ private static final String SERVER_EMAIL = "noreply@example.com";
private static final ZoneId ZONE_ID = ZoneId.of("America/Los_Angeles");
private final AccountGroup.UUID groupUuid = AccountGroup.uuid("users-XYZ");
diff --git a/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java b/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java
index 6745b1d564..1017ed0bfd 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java
@@ -22,14 +22,13 @@ import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
import com.google.gerrit.server.group.db.testing.GroupTestUtil;
-import java.util.List;
import org.junit.Test;
public class GroupsNoteDbConsistencyCheckerTest extends AbstractGroupTest {
@Test
public void groupNamesRefIsMissing() throws Exception {
- List<ConsistencyProblemInfo> problems =
+ ImmutableList<ConsistencyProblemInfo> problems =
GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
allUsersRepo, AccountGroup.nameKey("g-1"), AccountGroup.uuid("uuid-1"));
assertThat(problems)
@@ -39,7 +38,7 @@ public class GroupsNoteDbConsistencyCheckerTest extends AbstractGroupTest {
@Test
public void groupNameNoteIsMissing() throws Exception {
updateGroupNamesRef("g-2", "[group]\n\tuuid = uuid-2\n\tname = g-2\n");
- List<ConsistencyProblemInfo> problems =
+ ImmutableList<ConsistencyProblemInfo> problems =
GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
allUsersRepo, AccountGroup.nameKey("g-1"), AccountGroup.uuid("uuid-1"));
assertThat(problems)
@@ -49,7 +48,7 @@ public class GroupsNoteDbConsistencyCheckerTest extends AbstractGroupTest {
@Test
public void groupNameNoteIsConsistent() throws Exception {
updateGroupNamesRef("g-1", "[group]\n\tuuid = uuid-1\n\tname = g-1\n");
- List<ConsistencyProblemInfo> problems =
+ ImmutableList<ConsistencyProblemInfo> problems =
GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
allUsersRepo, AccountGroup.nameKey("g-1"), AccountGroup.uuid("uuid-1"));
assertThat(problems).isEmpty();
@@ -58,7 +57,7 @@ public class GroupsNoteDbConsistencyCheckerTest extends AbstractGroupTest {
@Test
public void groupNameNoteHasDifferentUUID() throws Exception {
updateGroupNamesRef("g-1", "[group]\n\tuuid = uuid-2\n\tname = g-1\n");
- List<ConsistencyProblemInfo> problems =
+ ImmutableList<ConsistencyProblemInfo> problems =
GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
allUsersRepo, AccountGroup.nameKey("g-1"), AccountGroup.uuid("uuid-1"));
assertThat(problems)
@@ -71,7 +70,7 @@ public class GroupsNoteDbConsistencyCheckerTest extends AbstractGroupTest {
@Test
public void groupNameNoteHasDifferentName() throws Exception {
updateGroupNamesRef("g-1", "[group]\n\tuuid = uuid-1\n\tname = g-2\n");
- List<ConsistencyProblemInfo> problems =
+ ImmutableList<ConsistencyProblemInfo> problems =
GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
allUsersRepo, AccountGroup.nameKey("g-1"), AccountGroup.uuid("uuid-1"));
assertThat(problems)
@@ -81,7 +80,7 @@ public class GroupsNoteDbConsistencyCheckerTest extends AbstractGroupTest {
@Test
public void groupNameNoteHasDifferentNameAndUUID() throws Exception {
updateGroupNamesRef("g-1", "[group]\n\tuuid = uuid-2\n\tname = g-2\n");
- List<ConsistencyProblemInfo> problems =
+ ImmutableList<ConsistencyProblemInfo> problems =
GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
allUsersRepo, AccountGroup.nameKey("g-1"), AccountGroup.uuid("uuid-1"));
assertThat(problems)
diff --git a/javatests/com/google/gerrit/server/index/IndexedFieldTest.java b/javatests/com/google/gerrit/server/index/IndexedFieldTest.java
index 5029334c7c..2c3584b0b7 100644
--- a/javatests/com/google/gerrit/server/index/IndexedFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/IndexedFieldTest.java
@@ -113,7 +113,10 @@ public class IndexedFieldTest {
IndexedField<TestIndexedData, StoredValue>.SearchSpec searchSpec = fieldToStoredValue.getKey();
StoredValue storedValue = new FakeStoredValue(fieldToStoredValue.getValue());
TestIndexedData testIndexedData = new TestIndexedData();
- searchSpec.setIfPossible(testIndexedData, storedValue);
+
+ @SuppressWarnings("unused")
+ var unused = searchSpec.setIfPossible(testIndexedData, storedValue);
+
assertThat(testIndexedData.getTestField()).isEqualTo(docValue);
}
@@ -122,7 +125,10 @@ public class IndexedFieldTest {
Entities.Change changeProto = TestIndexedFields.createChangeProto(12345);
StoredValue storedValue = new FakeStoredValue(Protos.toByteArray(changeProto));
TestIndexedData testIndexedData = new TestIndexedData();
- STORED_PROTO_FIELD_SPEC.setIfPossible(testIndexedData, storedValue);
+
+ @SuppressWarnings("unused")
+ var unused = STORED_PROTO_FIELD_SPEC.setIfPossible(testIndexedData, storedValue);
+
assertThat(testIndexedData.getTestField()).isEqualTo(changeProto);
}
@@ -137,7 +143,10 @@ public class IndexedFieldTest {
.map(proto -> Protos.toByteArray(proto))
.collect(toImmutableList()));
TestIndexedData testIndexedData = new TestIndexedData();
- ITERABLE_STORED_PROTO_FIELD.setIfPossible(testIndexedData, storedValue);
+
+ @SuppressWarnings("unused")
+ var unused = ITERABLE_STORED_PROTO_FIELD.setIfPossible(testIndexedData, storedValue);
+
assertThat(testIndexedData.getTestField()).isEqualTo(changeProtos);
}
@@ -150,7 +159,10 @@ public class IndexedFieldTest {
IndexedField<TestIndexedData, StoredValue>.SearchSpec searchSpec = fieldToStoredValue.getKey();
StoredValue storedValue = new FakeStoredValue(fieldToStoredValue.getValue(), /*isProto=*/ true);
TestIndexedData testIndexedData = new TestIndexedData();
- searchSpec.setIfPossible(testIndexedData, storedValue);
+
+ @SuppressWarnings("unused")
+ var unused = searchSpec.setIfPossible(testIndexedData, storedValue);
+
assertThat(testIndexedData.getTestField()).isEqualTo(docValue);
}
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
index 4ce4262475..53431d1e69 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
@@ -260,7 +260,7 @@ public class ChangeFieldTest {
}
private static void assertStoredRecordRoundTrip(SubmitRecord... records) {
- List<SubmitRecord> recordList = ImmutableList.copyOf(records);
+ ImmutableList<SubmitRecord> recordList = ImmutableList.copyOf(records);
List<String> stored =
ChangeField.storedSubmitRecords(recordList).stream()
.map(s -> new String(s, UTF_8))
diff --git a/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java b/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
index 6f406808f1..b7e733f69a 100644
--- a/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
+++ b/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
@@ -29,6 +29,7 @@ import com.google.gerrit.index.RefState;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
import com.google.gerrit.testing.InMemoryRepositoryManager;
+import java.util.List;
import java.util.stream.Stream;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
@@ -311,7 +312,7 @@ public class StalenessCheckerTest {
.isFalse();
}
- private static Iterable<byte[]> byteArrays(String... strs) {
+ private static List<byte[]> byteArrays(String... strs) {
return Stream.of(strs).map(s -> s != null ? s.getBytes(UTF_8) : null).collect(toList());
}
}
diff --git a/javatests/com/google/gerrit/server/ioutil/RegexListSearcherTest.java b/javatests/com/google/gerrit/server/ioutil/RegexListSearcherTest.java
index 048d59d00a..6f7832d014 100644
--- a/javatests/com/google/gerrit/server/ioutil/RegexListSearcherTest.java
+++ b/javatests/com/google/gerrit/server/ioutil/RegexListSearcherTest.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.ioutil;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import com.google.common.collect.ImmutableList;
import java.util.List;
@@ -31,7 +30,7 @@ public class RegexListSearcherTest {
@Test
public void anchors() {
- List<String> list = ImmutableList.of("foo");
+ ImmutableList<String> list = ImmutableList.of("foo");
assertSearchReturns(list, "^f.*", list);
assertSearchReturns(list, "^f.*o$", list);
assertSearchReturns(list, "f.*o$", list);
@@ -41,7 +40,7 @@ public class RegexListSearcherTest {
@Test
public void noCommonPrefix() {
- List<String> list = ImmutableList.of("bar", "foo", "quux");
+ ImmutableList<String> list = ImmutableList.of("bar", "foo", "quux");
assertSearchReturns(ImmutableList.of("foo"), "f.*", list);
assertSearchReturns(ImmutableList.of("foo"), ".*o.*", list);
assertSearchReturns(ImmutableList.of("bar", "foo", "quux"), ".*[aou].*", list);
@@ -49,7 +48,7 @@ public class RegexListSearcherTest {
@Test
public void commonPrefix() {
- List<String> list = ImmutableList.of("bar", "baz", "foo1", "foo2", "foo3", "quux");
+ ImmutableList<String> list = ImmutableList.of("bar", "baz", "foo1", "foo2", "foo3", "quux");
assertSearchReturns(ImmutableList.of("bar", "baz"), "b.*", list);
assertSearchReturns(ImmutableList.of("foo1", "foo2"), "foo[12]", list);
assertSearchReturns(ImmutableList.of("foo1", "foo2", "foo3"), "foo.*", list);
diff --git a/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java b/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java
index 1f0da16fba..c1b9f13839 100644
--- a/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java
+++ b/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java
@@ -24,6 +24,7 @@ import com.google.gerrit.testing.InMemoryModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
+import java.time.Instant;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
@@ -51,7 +52,7 @@ public class LoggingContextAwareExecutorServiceTest {
testPerformanceLogger =
new PerformanceLogger() {
@Override
- public void log(String operation, long durationMs, Metadata metadata) {
+ public void log(String operation, long durationMs, Instant endTime, Metadata metadata) {
// do nothing
}
};
diff --git a/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java b/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
index 512a1b199d..c93061d3f6 100644
--- a/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
+++ b/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
@@ -32,6 +32,7 @@ import com.google.gerrit.testing.InMemoryModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
+import java.time.Instant;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.jgit.lib.Config;
import org.junit.After;
@@ -364,7 +365,7 @@ public class PerformanceLogContextTest {
private ImmutableList.Builder<PerformanceLogEntry> logEntries = ImmutableList.builder();
@Override
- public void log(String operation, long durationMs, Metadata metadata) {
+ public void log(String operation, long durationMs, Instant endTime, Metadata metadata) {
logEntries.add(PerformanceLogEntry.create(operation, metadata));
}
diff --git a/javatests/com/google/gerrit/server/mail/send/HumanCommentFormatterTest.java b/javatests/com/google/gerrit/server/mail/send/HumanCommentFormatterTest.java
index 46ea8b218e..68cb231c76 100644
--- a/javatests/com/google/gerrit/server/mail/send/HumanCommentFormatterTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/HumanCommentFormatterTest.java
@@ -20,6 +20,7 @@ import static com.google.gerrit.server.mail.send.CommentFormatter.BlockType.PARA
import static com.google.gerrit.server.mail.send.CommentFormatter.BlockType.PRE_FORMATTED;
import static com.google.gerrit.server.mail.send.CommentFormatter.BlockType.QUOTE;
+import com.google.common.collect.ImmutableList;
import java.util.List;
import org.junit.Test;
@@ -63,7 +64,7 @@ public class HumanCommentFormatterTest {
@Test
public void parseSimple() {
String comment = "Para1";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(1);
assertBlock(result, 0, PARAGRAPH, comment);
@@ -72,7 +73,7 @@ public class HumanCommentFormatterTest {
@Test
public void parseMultilinePara() {
String comment = "Para 1\nStill para 1";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(1);
assertBlock(result, 0, PARAGRAPH, comment);
@@ -81,7 +82,7 @@ public class HumanCommentFormatterTest {
@Test
public void parseParaBreak() {
String comment = "Para 1\n\nPara 2\n\nPara 3";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(3);
assertBlock(result, 0, PARAGRAPH, "Para 1");
@@ -92,7 +93,7 @@ public class HumanCommentFormatterTest {
@Test
public void parseQuote() {
String comment = "> Quote text";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(1);
assertQuoteBlock(result, 0, 1);
@@ -102,7 +103,7 @@ public class HumanCommentFormatterTest {
@Test
public void parseExcludesEmpty() {
String comment = "Para 1\n\n\n\nPara 2";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(2);
assertBlock(result, 0, PARAGRAPH, "Para 1");
@@ -112,7 +113,7 @@ public class HumanCommentFormatterTest {
@Test
public void parseQuoteLeadSpace() {
String comment = " > Quote text";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(1);
assertQuoteBlock(result, 0, 1);
@@ -122,7 +123,7 @@ public class HumanCommentFormatterTest {
@Test
public void parseMultiLineQuote() {
String comment = "> Quote line 1\n> Quote line 2\n > Quote line 3\n";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(1);
assertQuoteBlock(result, 0, 1);
@@ -133,7 +134,7 @@ public class HumanCommentFormatterTest {
@Test
public void parsePre() {
String comment = " Four space indent.";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(1);
assertBlock(result, 0, PRE_FORMATTED, comment);
@@ -142,7 +143,7 @@ public class HumanCommentFormatterTest {
@Test
public void parseOneSpacePre() {
String comment = " One space indent.\n Another line.";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(1);
assertBlock(result, 0, PRE_FORMATTED, comment);
@@ -151,7 +152,7 @@ public class HumanCommentFormatterTest {
@Test
public void parseTabPre() {
String comment = "\tOne tab indent.\n\tAnother line.\n Yet another!";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(1);
assertBlock(result, 0, PRE_FORMATTED, comment);
@@ -160,7 +161,7 @@ public class HumanCommentFormatterTest {
@Test
public void parseIntermediateLeadingWhitespacePre() {
String comment = "No indent.\n\tNonzero indent.\nNo indent again.";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(1);
assertBlock(result, 0, PRE_FORMATTED, comment);
@@ -169,7 +170,7 @@ public class HumanCommentFormatterTest {
@Test
public void parseStarList() {
String comment = "* Item 1\n* Item 2\n* Item 3";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(1);
assertListBlock(result, 0, 0, "Item 1");
@@ -180,7 +181,7 @@ public class HumanCommentFormatterTest {
@Test
public void parseDashList() {
String comment = "- Item 1\n- Item 2\n- Item 3";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(1);
assertListBlock(result, 0, 0, "Item 1");
@@ -191,7 +192,7 @@ public class HumanCommentFormatterTest {
@Test
public void parseMixedList() {
String comment = "- Item 1\n* Item 2\n- Item 3\n* Item 4";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(1);
assertListBlock(result, 0, 0, "Item 1");
@@ -216,7 +217,7 @@ public class HumanCommentFormatterTest {
+ "\tPreformatted text."
+ "\n\n"
+ "Parting words.";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(7);
assertBlock(result, 0, PARAGRAPH, "Paragraph\nacross\na\nfew\nlines.");
@@ -235,7 +236,7 @@ public class HumanCommentFormatterTest {
@Test
public void bulletList1() {
String comment = "A\n\n* line 1\n* 2nd line";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(2);
assertBlock(result, 0, PARAGRAPH, "A");
@@ -246,7 +247,7 @@ public class HumanCommentFormatterTest {
@Test
public void bulletList2() {
String comment = "A\n\n* line 1\n* 2nd line\n\nB";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(3);
assertBlock(result, 0, PARAGRAPH, "A");
@@ -258,7 +259,7 @@ public class HumanCommentFormatterTest {
@Test
public void bulletList3() {
String comment = "* line 1\n* 2nd line\n\nB";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(2);
assertListBlock(result, 0, 0, "line 1");
@@ -272,7 +273,7 @@ public class HumanCommentFormatterTest {
"To see this bug, you have to:\n" //
+ "* Be on IMAP or EAS (not on POP)\n" //
+ "* Be very unlucky\n";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(2);
assertBlock(result, 0, PARAGRAPH, "To see this bug, you have to:");
@@ -287,7 +288,7 @@ public class HumanCommentFormatterTest {
+ "you have to:\n" //
+ "* Be on IMAP or EAS (not on POP)\n" //
+ "* Be very unlucky\n";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(2);
assertBlock(result, 0, PARAGRAPH, "To see this bug, you have to:");
@@ -298,7 +299,7 @@ public class HumanCommentFormatterTest {
@Test
public void dashList1() {
String comment = "A\n\n- line 1\n- 2nd line";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(2);
assertBlock(result, 0, PARAGRAPH, "A");
@@ -309,7 +310,7 @@ public class HumanCommentFormatterTest {
@Test
public void dashList2() {
String comment = "A\n\n- line 1\n- 2nd line\n\nB";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(3);
assertBlock(result, 0, PARAGRAPH, "A");
@@ -321,7 +322,7 @@ public class HumanCommentFormatterTest {
@Test
public void dashList3() {
String comment = "- line 1\n- 2nd line\n\nB";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(2);
assertListBlock(result, 0, 0, "line 1");
@@ -332,7 +333,7 @@ public class HumanCommentFormatterTest {
@Test
public void preformat1() {
String comment = "A\n\n This is pre\n formatted";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(2);
assertBlock(result, 0, PARAGRAPH, "A");
@@ -342,7 +343,7 @@ public class HumanCommentFormatterTest {
@Test
public void preformat2() {
String comment = "A\n\n This is pre\n formatted\n\nbut this is not";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(3);
assertBlock(result, 0, PARAGRAPH, "A");
@@ -353,7 +354,7 @@ public class HumanCommentFormatterTest {
@Test
public void preformat3() {
String comment = "A\n\n Q\n <R>\n S\n\nB";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(3);
assertBlock(result, 0, PARAGRAPH, "A");
@@ -364,7 +365,7 @@ public class HumanCommentFormatterTest {
@Test
public void preformat4() {
String comment = " Q\n <R>\n S\n\nB";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(2);
assertBlock(result, 0, PRE_FORMATTED, " Q\n <R>\n S");
@@ -374,7 +375,7 @@ public class HumanCommentFormatterTest {
@Test
public void quote1() {
String comment = "> I'm happy\n > with quotes!\n\nSee above.";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(2);
assertQuoteBlock(result, 0, 1);
@@ -385,7 +386,7 @@ public class HumanCommentFormatterTest {
@Test
public void quote2() {
String comment = "See this said:\n\n > a quoted\n > string block\n\nOK?";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(3);
assertBlock(result, 0, PARAGRAPH, "See this said:");
@@ -397,7 +398,7 @@ public class HumanCommentFormatterTest {
@Test
public void nestedQuotes1() {
String comment = " > > prior\n > \n > next\n";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(1);
assertQuoteBlock(result, 0, 2);
@@ -428,7 +429,7 @@ public class HumanCommentFormatterTest {
+ "> Paragraph 6.\n"
+ "\n"
+ "Paragraph 7.\n";
- List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+ ImmutableList<CommentFormatter.Block> result = CommentFormatter.parse(comment);
assertThat(result).hasSize(2);
assertQuoteBlock(result, 0, 2);
diff --git a/javatests/com/google/gerrit/server/mail/send/MailSoySauceLoaderTest.java b/javatests/com/google/gerrit/server/mail/send/MailSoySauceLoaderTest.java
index 7f893f1559..412bd0fa26 100644
--- a/javatests/com/google/gerrit/server/mail/send/MailSoySauceLoaderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/MailSoySauceLoaderTest.java
@@ -20,7 +20,7 @@ import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.plugincontext.PluginContext.PluginMetrics;
import com.google.gerrit.server.plugincontext.PluginSetContext;
-import java.nio.file.Paths;
+import java.nio.file.Path;
import org.junit.Before;
import org.junit.Test;
@@ -31,7 +31,7 @@ public class MailSoySauceLoaderTest {
@Before
public void setUp() throws Exception {
- sitePaths = new SitePaths(Paths.get("."));
+ sitePaths = new SitePaths(Path.of("."));
set = new DynamicSet<>();
}
diff --git a/javatests/com/google/gerrit/server/mail/send/MailSoySauceModuleTest.java b/javatests/com/google/gerrit/server/mail/send/MailSoySauceModuleTest.java
index 5a6db42d76..ed179a73fe 100644
--- a/javatests/com/google/gerrit/server/mail/send/MailSoySauceModuleTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/MailSoySauceModuleTest.java
@@ -33,7 +33,7 @@ import com.google.inject.Key;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Names;
import com.google.template.soy.jbcsrc.api.SoySauce;
-import java.nio.file.Paths;
+import java.nio.file.Path;
import javax.inject.Provider;
import org.eclipse.jgit.lib.Config;
import org.junit.Test;
@@ -41,7 +41,7 @@ import org.junit.Test;
public class MailSoySauceModuleTest {
@Test
public void soySauceProviderReturnsCachedValue() throws Exception {
- SitePaths sitePaths = new SitePaths(Paths.get("."));
+ SitePaths sitePaths = new SitePaths(Path.of("."));
Injector injector =
Guice.createInjector(
new MailSoySauceModule(),
diff --git a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index 96a485a458..6118d2e79b 100644
--- a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -57,6 +57,8 @@ import com.google.gerrit.server.config.EnablePeerIPInReflogRecord;
import com.google.gerrit.server.config.GerritImportedServerIds;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.GerritServerId;
+import com.google.gerrit.server.experiments.ConfigExperimentFeatures;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitModule;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -186,6 +188,8 @@ public abstract class AbstractChangeNotesTest {
install(new GitModule());
install(new DefaultUrlFormatterModule());
+ install(new NoteDbDraftCommentsModule());
+ install(new NoteDbStarredChangesModule());
install(NoteDbModule.forTest());
install(new DefaultRefLogIdentityProvider.Module());
bind(AllUsersName.class).toProvider(AllUsersNameProvider.class);
@@ -223,6 +227,7 @@ public abstract class AbstractChangeNotesTest {
bind(PatchSetApprovalUuidGenerator.class).to(TestPatchSetApprovalUuidGenerator.class);
bind(ChangeDraftUpdate.ChangeDraftUpdateFactory.class)
.to(ChangeDraftNotesUpdate.Factory.class);
+ bind(ExperimentFeatures.class).to(ConfigExperimentFeatures.class);
}
});
}
@@ -231,7 +236,7 @@ public abstract class AbstractChangeNotesTest {
throws RepositoryCaseMismatchException, RepositoryNotFoundException {
AllUsersName allUsersName = injector.getInstance(AllUsersName.class);
- repoManager.createRepository(allUsersName);
+ repoManager.createRepository(allUsersName).close();
IdentifiedUser.GenericFactory identifiedUserFactory =
injector.getInstance(IdentifiedUser.GenericFactory.class);
@@ -247,7 +252,11 @@ public abstract class AbstractChangeNotesTest {
@After
public void resetTime() {
TestTimeUtil.useSystemTime();
- System.setProperty("user.timezone", systemTimeZone);
+ if (systemTimeZone != null) {
+ System.setProperty("user.timezone", systemTimeZone);
+ } else {
+ System.clearProperty("user.timezone");
+ }
}
protected Change newChange(boolean workInProgress) throws Exception {
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNoteJsonTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNoteJsonTest.java
index 24e28f376f..591ed96d97 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNoteJsonTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNoteJsonTest.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.notedb;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import com.google.gson.Gson;
import com.google.inject.TypeLiteral;
@@ -33,6 +32,18 @@ public class ChangeNoteJsonTest {
Optional<Child> optionalChild;
}
+ static class ParentWithoutField {}
+
+ static class ChildWithField extends ParentWithoutField {
+ String field;
+ }
+
+ static class ParentWithField {
+ String field;
+ }
+
+ static class ChildWithoutField extends ParentWithField {}
+
@Test
public void shouldSerializeAndDeserializeEmptyOptional() {
// given
@@ -124,4 +135,39 @@ public class ChangeNoteJsonTest {
assertThat(result.optionalChild).isPresent();
assertThat(result.optionalChild.get().optionalValue).isEmpty();
}
+
+ @Test
+ public void ignoresUnknownField() {
+ Child fooChild = new Child();
+ fooChild.optionalValue = Optional.empty();
+ Parent parent = new Parent();
+ parent.optionalChild = Optional.of(fooChild);
+
+ String jsonWithUnknownField =
+ "{\n"
+ + " \"unknown-field\": \"unknown-value\","
+ + " \"optionalChild\": {\n"
+ + " \"value\": {\n"
+ + " \"optionalValue\": {}\n"
+ + " }\n"
+ + " }\n"
+ + "}";
+
+ Parent result = gson.fromJson(jsonWithUnknownField, new TypeLiteral<Parent>() {}.getType());
+
+ assertThat(result.optionalChild).isPresent();
+ assertThat(result.optionalChild.get().optionalValue).isEmpty();
+ }
+
+ @Test
+ public void fieldCanBeMovedFromChildToParentWithoutChangingSerializedRepresentation() {
+ ChildWithField c = new ChildWithField();
+ c.field = "test";
+ ChildWithoutField c2 = gson.fromJson(gson.toJson(c), ChildWithoutField.class);
+ assertThat(c2.field).isEqualTo("test");
+
+ String serialized = "" + "{\n" + " \"field\": \"test\"\n" + "}";
+ assertThat(gson.toJson(c)).isEqualTo(serialized);
+ assertThat(gson.toJson(c2)).isEqualTo(serialized);
+ }
}
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java
index 84d2d5d8f1..caf2023d34 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java
@@ -56,7 +56,9 @@ public class ChangeNotesCommitTest extends AbstractChangeNotesTest {
+ FQ_USER_IDENT
+ "\\u003e\",\"operation\":\"ADD\",\"reason\":\"Added by Administrator using the hovercard menu\"}");
- newParser(commit).parseAll();
+ @SuppressWarnings("unused")
+ var unused = newParser(commit).parseAll();
+
assertThat(((ChangeNotesCommit) commit).isAttentionSetCommitOnly(false)).isEqualTo(true);
}
@@ -73,7 +75,9 @@ public class ChangeNotesCommitTest extends AbstractChangeNotesTest {
+ FQ_USER_IDENT
+ "\\u003e\",\"operation\":\"ADD\",\"reason\":\"Added by Administrator using the hovercard menu\"}");
- newParser(commit).parseAll();
+ @SuppressWarnings("unused")
+ var unused = newParser(commit).parseAll();
+
assertThat(((ChangeNotesCommit) commit).isAttentionSetCommitOnly(false)).isEqualTo(false);
}
@@ -82,7 +86,9 @@ public class ChangeNotesCommitTest extends AbstractChangeNotesTest {
throws Exception {
RevCommit commit = writeCommit("Update patch set 1\n" + "\n" + "Patch-set: 1\n");
- newParser(commit).parseAll();
+ @SuppressWarnings("unused")
+ var unused = newParser(commit).parseAll();
+
assertThat(((ChangeNotesCommit) commit).isAttentionSetCommitOnly(false)).isEqualTo(false);
}
@@ -97,7 +103,9 @@ public class ChangeNotesCommitTest extends AbstractChangeNotesTest {
+ FQ_USER_IDENT
+ "\\u003e\",\"operation\":\"ADD\",\"reason\":\"Added by Administrator using the hovercard menu\"}");
- newParser(commit).parseAll();
+ @SuppressWarnings("unused")
+ var unused = newParser(commit).parseAll();
+
assertThat(((ChangeNotesCommit) commit).isAttentionSetCommitOnly(true)).isEqualTo(false);
}
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index 23c57048bf..8717d999c8 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.notedb;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Change;
import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
import com.google.gerrit.server.util.time.TimeUtil;
@@ -662,7 +663,10 @@ public class ChangeNotesParserTest extends AbstractChangeNotesTest {
+ " by Administrator using the hovercard menu\"}",
false);
ChangeNotesParser changeNotesParser = newParser(commit);
- changeNotesParser.parseAll();
+
+ @SuppressWarnings("unused")
+ var unused = changeNotesParser.parseAll();
+
final boolean hasChangeMessage = false;
assertThat(
changeNotesParser.countTowardsMaxUpdatesLimit(
@@ -684,7 +688,10 @@ public class ChangeNotesParserTest extends AbstractChangeNotesTest {
+ " by Administrator using the hovercard menu\"}",
false);
ChangeNotesParser changeNotesParser = newParser(commit);
- changeNotesParser.parseAll();
+
+ @SuppressWarnings("unused")
+ var unused = changeNotesParser.parseAll();
+
final boolean hasChangeMessage = false;
assertThat(
changeNotesParser.countTowardsMaxUpdatesLimit(
@@ -696,7 +703,10 @@ public class ChangeNotesParserTest extends AbstractChangeNotesTest {
public void changeWithoutAttentionSetShouldCountTowardsMaxUpdatesLimit() throws Exception {
RevCommit commit = writeCommit("Update WIP change\n" + "\n" + "Patch-set: 1\n", true);
ChangeNotesParser changeNotesParser = newParser(commit);
- changeNotesParser.parseAll();
+
+ @SuppressWarnings("unused")
+ var unused = changeNotesParser.parseAll();
+
final boolean hasChangeMessage = false;
assertThat(
changeNotesParser.countTowardsMaxUpdatesLimit(
@@ -717,7 +727,10 @@ public class ChangeNotesParserTest extends AbstractChangeNotesTest {
+ " by Administrator using the hovercard menu\"}",
false);
ChangeNotesParser changeNotesParser = newParser(commit);
- changeNotesParser.parseAll();
+
+ @SuppressWarnings("unused")
+ var unused = changeNotesParser.parseAll();
+
final boolean hasChangeMessage = true;
assertThat(
changeNotesParser.countTowardsMaxUpdatesLimit(
@@ -783,10 +796,12 @@ public class ChangeNotesParserTest extends AbstractChangeNotesTest {
}
}
+ @CanIgnoreReturnValue
private ChangeNotesState assertParseSucceeds(String body) throws Exception {
return assertParseSucceeds(writeCommit(body));
}
+ @CanIgnoreReturnValue
private ChangeNotesState assertParseSucceeds(RevCommit commit) throws Exception {
return newParser(commit).parseAll();
}
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index 8efc5bdad0..008a74bb93 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -24,12 +24,14 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Iterables;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Address;
import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.FixSuggestion;
import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LegacySubmitRequirement;
@@ -658,12 +660,12 @@ public class ChangeNotesStateTest {
newBuilder()
.reviewerUpdates(
ImmutableList.of(
- ReviewerStatusUpdate.create(
+ ReviewerStatusUpdate.createForReviewer(
Instant.ofEpochMilli(1212L),
Account.id(1000),
Account.id(2002),
ReviewerStateInternal.CC),
- ReviewerStatusUpdate.create(
+ ReviewerStatusUpdate.createForReviewer(
Instant.ofEpochMilli(3434L),
Account.id(1000),
Account.id(2001),
@@ -677,18 +679,58 @@ public class ChangeNotesStateTest {
ReviewerStatusUpdateProto.newBuilder()
.setTimestampMillis(1212L)
.setUpdatedBy(1000)
+ .setHasReviewer(true)
.setReviewer(2002)
.setState("CC"))
.addReviewerUpdate(
ReviewerStatusUpdateProto.newBuilder()
.setTimestampMillis(3434L)
.setUpdatedBy(1000)
+ .setHasReviewer(true)
.setReviewer(2001)
.setState("REVIEWER"))
.build());
}
@Test
+ public void serializeReviewerByEmailUpdates() throws Exception {
+ assertRoundTrip(
+ newBuilder()
+ .reviewerUpdates(
+ ImmutableList.of(
+ ReviewerStatusUpdate.createForReviewerByEmail(
+ Instant.ofEpochMilli(1212L),
+ Account.id(1000),
+ Address.parse("email1@example.com"),
+ ReviewerStateInternal.CC),
+ ReviewerStatusUpdate.createForReviewerByEmail(
+ Instant.ofEpochMilli(3434L),
+ Account.id(1000),
+ Address.parse("email2@example.com"),
+ ReviewerStateInternal.REVIEWER)))
+ .build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto)
+ .addReviewerUpdate(
+ ReviewerStatusUpdateProto.newBuilder()
+ .setTimestampMillis(1212L)
+ .setUpdatedBy(1000)
+ .setHasReviewerByEmail(true)
+ .setReviewerByEmail("email1@example.com")
+ .setState("CC"))
+ .addReviewerUpdate(
+ ReviewerStatusUpdateProto.newBuilder()
+ .setTimestampMillis(3434L)
+ .setUpdatedBy(1000)
+ .setHasReviewerByEmail(true)
+ .setReviewerByEmail("email2@example.com")
+ .setState("REVIEWER"))
+ .build());
+ }
+
+ @Test
public void serializeAttentionSetUpdates() throws Exception {
assertRoundTrip(
newBuilder()
@@ -1069,7 +1111,8 @@ public class ChangeNotesStateTest {
ImmutableMap.of(
"date", Instant.class,
"updatedBy", Account.Id.class,
- "reviewer", Account.Id.class,
+ "reviewer", new TypeLiteral<Optional<Account.Id>>() {}.getType(),
+ "reviewerByEmail", new TypeLiteral<Optional<Address>>() {}.getType(),
"state", ReviewerStateInternal.class));
}
@@ -1161,6 +1204,7 @@ public class ChangeNotesStateTest {
.put("revId", String.class)
.put("serverId", String.class)
.put("unresolved", boolean.class)
+ .put("fixSuggestions", new TypeLiteral<List<FixSuggestion>>() {}.getType())
.build());
}
@@ -1181,6 +1225,7 @@ public class ChangeNotesStateTest {
return ChangeNotesStateProto.parseFrom(Serializer.INSTANCE.serialize(state));
}
+ @CanIgnoreReturnValue
private static ChangeNotesState assertRoundTrip(
ChangeNotesState state, ChangeNotesStateProto expectedProto) throws Exception {
ChangeNotesStateProto actualProto = toProto(state);
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index e025ee2252..3c1abbd70f 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -18,7 +18,6 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.entities.RefNames.changeMetaRef;
import static com.google.gerrit.entities.RefNames.refsDraftComments;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
@@ -26,20 +25,19 @@ import static com.google.gerrit.server.notedb.ReviewerStateInternal.REMOVED;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
-import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Comparator.comparing;
-import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import static org.mockito.Mockito.mock;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Iterables;
-import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Address;
@@ -56,6 +54,7 @@ import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.SubmissionId;
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.ChangeDraftUpdate;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.IdentifiedUser;
@@ -70,7 +69,6 @@ import com.google.inject.Inject;
import java.time.Instant;
import java.util.LinkedHashSet;
import java.util.List;
-import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -292,7 +290,7 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
ChangeNotes notes = newNotes(c);
assertThat(notes.getApprovals().all().keySet()).containsExactly(c.currentPatchSetId());
- List<PatchSetApproval> psas = notes.getApprovals().all().get(c.currentPatchSetId());
+ ImmutableList<PatchSetApproval> psas = notes.getApprovals().all().get(c.currentPatchSetId());
assertThat(psas).hasSize(2);
assertThat(psas.get(0).patchSetId()).isEqualTo(c.currentPatchSetId());
@@ -325,7 +323,7 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
PatchSet.Id ps2 = c.currentPatchSetId();
ChangeNotes notes = newNotes(c);
- ListMultimap<PatchSet.Id, PatchSetApproval> psas = notes.getApprovals().all();
+ ImmutableListMultimap<PatchSet.Id, PatchSetApproval> psas = notes.getApprovals().all();
assertThat(psas).hasSize(2);
PatchSetApproval psa1 = Iterables.getOnlyElement(psas.get(ps1));
@@ -383,7 +381,7 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
ChangeNotes notes = newNotes(c);
assertThat(notes.getApprovals().all().keySet()).containsExactly(c.currentPatchSetId());
- List<PatchSetApproval> psas = notes.getApprovals().all().get(c.currentPatchSetId());
+ ImmutableList<PatchSetApproval> psas = notes.getApprovals().all().get(c.currentPatchSetId());
assertThat(psas).hasSize(2);
assertThat(psas.get(0).patchSetId()).isEqualTo(c.currentPatchSetId());
@@ -606,7 +604,7 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
ChangeNotes notes = newNotes(c);
assertThat(notes.getApprovals().all().keySet()).containsExactly(c.currentPatchSetId());
- List<PatchSetApproval> patchSetApprovals =
+ ImmutableList<PatchSetApproval> patchSetApprovals =
notes.getApprovals().all().get(c.currentPatchSetId());
assertThat(patchSetApprovals).hasSize(2);
assertThat(
@@ -631,7 +629,7 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
ChangeNotes notes = newNotes(c);
assertThat(notes.getApprovals().all().keySet()).containsExactly(c.currentPatchSetId());
- List<PatchSetApproval> patchSetApprovals =
+ ImmutableList<PatchSetApproval> patchSetApprovals =
notes.getApprovals().all().get(c.currentPatchSetId());
assertThat(patchSetApprovals).hasSize(2);
assertThat(
@@ -1220,7 +1218,7 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
update.commit();
ChangeNotes notes = newNotes(c);
- List<PatchSetApproval> psas = notes.getApprovals().all().get(c.currentPatchSetId());
+ ImmutableList<PatchSetApproval> psas = notes.getApprovals().all().get(c.currentPatchSetId());
assertThat(psas).hasSize(2);
assertThat(psas.get(0).accountId()).isEqualTo(changeOwner.getAccount().id());
assertThat(psas.get(1).accountId()).isEqualTo(otherUser.getAccount().id());
@@ -1258,7 +1256,7 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
update.commit();
ChangeNotes notes = newNotes(c);
- List<SubmitRecord> recs = notes.getSubmitRecords();
+ ImmutableList<SubmitRecord> recs = notes.getSubmitRecords();
assertThat(recs).hasSize(2);
assertThat(recs.get(0))
.isEqualTo(
@@ -2009,9 +2007,7 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
update.commit();
ChangeNotes notes = newNotes(c);
- readNote(notes, commit);
-
- Map<PatchSet.Id, PatchSet> patchSets = notes.getPatchSets();
+ ImmutableMap<PatchSet.Id, PatchSet> patchSets = notes.getPatchSets();
assertThat(patchSets.get(psId1).pushCertificate()).isEmpty();
assertThat(patchSets.get(psId2).pushCertificate()).hasValue(pushCert);
assertThat(notes.getHumanComments()).isEmpty();
@@ -2068,7 +2064,7 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
}
ChangeNotes notes = newNotes(c);
- List<PatchSetApproval> psas = notes.getApprovals().all().get(c.currentPatchSetId());
+ ImmutableList<PatchSetApproval> psas = notes.getApprovals().all().get(c.currentPatchSetId());
assertThat(psas).hasSize(2);
assertThat(psas.get(0).accountId()).isEqualTo(changeOwner.getAccount().id());
@@ -2320,7 +2316,7 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
ChangeNotes notes = newNotes(c);
- List<ChangeMessage> cm = notes.getChangeMessages();
+ ImmutableList<ChangeMessage> cm = notes.getChangeMessages();
assertThat(cm).hasSize(2);
assertThat(cm.get(0).getMessage()).isEqualTo("First change message.\n");
assertThat(cm.get(0).getAuthor()).isEqualTo(changeOwner.getAccount().id());
@@ -3472,13 +3468,11 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
// Re-add draft version of comment2 back to draft ref without updating
// change ref. Simulates the case where deleting the draft failed
// non-atomically after adding the published comment succeeded.
- Optional<ChangeDraftNotesUpdate> draftUpdate =
- ChangeDraftNotesUpdate.asChangeDraftNotesUpdate(
- newUpdate(c, otherUser).createDraftUpdateIfNull());
- if (draftUpdate.isPresent()) {
- draftUpdate.get().putDraftComment(comment2);
+ ChangeDraftUpdate draftUpdate = newUpdate(c, otherUser).createDraftUpdateIfNull();
+ if (draftUpdate != null) {
+ draftUpdate.putDraftComment(comment2);
try (NoteDbUpdateManager manager = updateManagerFactory.create(c.getProject(), otherUser)) {
- manager.add(draftUpdate.get());
+ manager.add(draftUpdate);
testRefAction(() -> manager.execute());
}
}
@@ -3548,7 +3542,7 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
}
ChangeNotes notes = newNotes(c);
- List<HumanComment> comments = notes.getHumanComments().get(commitId);
+ ImmutableList<HumanComment> comments = notes.getHumanComments().get(commitId);
assertThat(comments).hasSize(2);
assertThat(comments.get(0).message).isEqualTo("comment 1");
assertThat(comments.get(1).message).isEqualTo("comment 2");
@@ -3680,7 +3674,7 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
@Test
public void putReviewerByEmail() throws Exception {
- Address adr = Address.create("Foo Bar", "foo.bar@gerritcodereview.com");
+ Address adr = Address.create("Foo Bar", "foo.bar@example.com");
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
@@ -3693,7 +3687,7 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
@Test
public void putAndRemoveReviewerByEmail() throws Exception {
- Address adr = Address.create("Foo Bar", "foo.bar@gerritcodereview.com");
+ Address adr = Address.create("Foo Bar", "foo.bar@example.com");
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
@@ -3710,7 +3704,7 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
@Test
public void putRemoveAndAddBackReviewerByEmail() throws Exception {
- Address adr = Address.create("Foo Bar", "foo.bar@gerritcodereview.com");
+ Address adr = Address.create("Foo Bar", "foo.bar@example.com");
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
@@ -3731,8 +3725,8 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
@Test
public void putReviewerByEmailAndCcByEmail() throws Exception {
- Address adrReviewer = Address.create("Foo Bar", "foo.bar@gerritcodereview.com");
- Address adrCc = Address.create("Foo Bor", "foo.bar.2@gerritcodereview.com");
+ Address adrReviewer = Address.create("Foo Bar", "foo.bar@example.com");
+ Address adrCc = Address.create("Foo Bor", "foo.bar.2@example.com");
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
@@ -3753,7 +3747,7 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
@Test
public void putReviewerByEmailAndChangeToCc() throws Exception {
- Address adr = Address.create("Foo Bar", "foo.bar@gerritcodereview.com");
+ Address adr = Address.create("Foo Bar", "foo.bar@example.com");
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
@@ -3809,8 +3803,8 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
@Test
public void pendingReviewers() throws Exception {
- Address adr1 = Address.create("Foo Bar1", "foo.bar1@gerritcodereview.com");
- Address adr2 = Address.create("Foo Bar2", "foo.bar2@gerritcodereview.com");
+ Address adr1 = Address.create("Foo Bar1", "foo.bar1@example.com");
+ Address adr2 = Address.create("Foo Bar2", "foo.bar2@example.com");
Account.Id ownerId = changeOwner.getAccount().id();
Account.Id otherUserId = otherUser.getAccount().id();
@@ -3979,11 +3973,6 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
assertThat(newNotes(c).getChange().currentPatchSetId().get()).isEqualTo(2);
}
- private String readNote(ChangeNotes notes, ObjectId noteId) throws Exception {
- ObjectId dataId = notes.revisionNoteMap.noteMap.getNote(noteId).getData();
- return new String(rw.getObjectReader().open(dataId, OBJ_BLOB).getCachedBytes(), UTF_8);
- }
-
@Nullable
private ObjectId exactRefAllUsers(String refName) throws Exception {
try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
@@ -4014,10 +4003,12 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
TestChanges.incrementPatchSet(c);
}
+ @CanIgnoreReturnValue
private RevCommit incrementPatchSet(Change c) throws Exception {
return incrementPatchSet(c, userFactory.create(c.getOwner()));
}
+ @CanIgnoreReturnValue
private RevCommit incrementPatchSet(Change c, IdentifiedUser user) throws Exception {
incrementCurrentPatchSetFieldOnly(c);
RevCommit commit = tr.commit().message("PS" + c.currentPatchSetId().get()).create();
diff --git a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index 25f2f9867e..1f22fc1d5f 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -390,12 +390,11 @@ public class CommitMessageOutputTest extends AbstractChangeNotesTest {
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
update.putReviewerByEmail(
- Address.create("John Doe", "j.doe@gerritcodereview.com"), ReviewerStateInternal.REVIEWER);
+ Address.create("John Doe", "j.doe@example.com"), ReviewerStateInternal.REVIEWER);
update.commit();
assertBodyEquals(
- "Update patch set 1\n\nPatch-set: 1\n"
- + "Reviewer-email: John Doe <j.doe@gerritcodereview.com>\n",
+ "Update patch set 1\n\nPatch-set: 1\n" + "Reviewer-email: John Doe <j.doe@example.com>\n",
update.getResult());
}
@@ -403,13 +402,11 @@ public class CommitMessageOutputTest extends AbstractChangeNotesTest {
public void ccByEmail() throws Exception {
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
- update.putReviewerByEmail(
- Address.create("j.doe@gerritcodereview.com"), ReviewerStateInternal.CC);
+ update.putReviewerByEmail(Address.create("j.doe@example.com"), ReviewerStateInternal.CC);
update.commit();
assertBodyEquals(
- "Update patch set 1\n\nPatch-set: 1\nCC-email: j.doe@gerritcodereview.com\n",
- update.getResult());
+ "Update patch set 1\n\nPatch-set: 1\nCC-email: j.doe@example.com\n", update.getResult());
}
private RevCommit parseCommit(ObjectId id) throws Exception {
diff --git a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
index 064cd89fbe..1e45af210f 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
@@ -27,6 +27,7 @@ import static org.mockito.Mockito.mock;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.AttentionSetUpdate.Operation;
@@ -53,7 +54,6 @@ import com.google.inject.Inject;
import java.time.Instant;
import java.util.Arrays;
import java.util.HashSet;
-import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.IntStream;
@@ -179,7 +179,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
assertFixedCommits(ImmutableList.of(commitToFix), backfillResult, c.getId());
- List<String> commitHistoryDiff = commitHistoryDiff(backfillResult, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(backfillResult, c.getId());
assertThat(commitHistoryDiff).containsExactly("");
BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
@@ -384,7 +384,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
assertThat(invalidUpdateCommit.getCommitterIdent())
.isEqualTo(fixedUpdateCommit.getCommitterIdent());
assertThat(fixedUpdateCommit.getFullMessage()).doesNotContain(changeOwner.getName());
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff).hasSize(1);
assertThat(commitHistoryDiff.get(0)).contains("-author Change Owner <1@gerrit>");
assertThat(commitHistoryDiff.get(0)).contains("+author Gerrit User 1 <1@gerrit>");
@@ -447,7 +447,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
commitsBeforeRewrite, commitsAfterRewrite, ImmutableList.of(invalidCommitIndex));
assertFixedCommits(ImmutableList.of(invalidUpdateCommit), result, c.getId());
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -9 +9 @@\n"
@@ -508,10 +508,10 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
Instant updateTimestamp = serverIdent.getWhenAsInstant();
ImmutableList<ReviewerStatusUpdate> expectedReviewerUpdates =
ImmutableList.of(
- ReviewerStatusUpdate.create(
+ ReviewerStatusUpdate.createForReviewer(
updateTimestamp, changeOwner.getAccountId(), otherUserId, REVIEWER),
- ReviewerStatusUpdate.create(updateTimestamp, otherUserId, otherUserId, CC),
- ReviewerStatusUpdate.create(
+ ReviewerStatusUpdate.createForReviewer(updateTimestamp, otherUserId, otherUserId, CC),
+ ReviewerStatusUpdate.createForReviewer(
updateTimestamp, changeOwner.getAccountId(), otherUserId, REMOVED));
ChangeNotes notesAfterRewrite = newNotes(c);
@@ -525,7 +525,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
assertFixedCommits(commitsToFix, result, c.getId());
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -9 +9 @@\n"
@@ -592,13 +592,13 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
Instant updateTimestamp = serverIdent.getWhenAsInstant();
ImmutableList<ReviewerStatusUpdate> expectedReviewerUpdates =
ImmutableList.of(
- ReviewerStatusUpdate.create(
+ ReviewerStatusUpdate.createForReviewer(
addReviewerUpdate.when, changeOwner.getAccountId(), otherUserId, REVIEWER),
- ReviewerStatusUpdate.create(
+ ReviewerStatusUpdate.createForReviewer(
updateTimestamp, changeOwner.getAccountId(), otherUserId, REMOVED),
- ReviewerStatusUpdate.create(
+ ReviewerStatusUpdate.createForReviewer(
addCcUpdate.when, changeOwner.getAccountId(), otherUserId, CC),
- ReviewerStatusUpdate.create(
+ ReviewerStatusUpdate.createForReviewer(
updateTimestamp, changeOwner.getAccountId(), otherUserId, REMOVED));
ChangeNotes notesAfterRewrite = newNotes(c);
@@ -617,7 +617,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
assertFixedCommits(commitsToFix.build(), result, c.getId());
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -6 +6 @@\n" + "-Removed reviewer Other Account.\n" + "+Removed reviewer\n",
@@ -655,7 +655,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -6 +6 @@\n" + "-Removed reviewer Other Account.\n" + "+Removed reviewer\n",
@@ -782,7 +782,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
assertFixedCommits(commitsToFix, result, c.getId());
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -7,2 +7,2 @@\n"
@@ -907,7 +907,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
assertFixedCommits(commitsToFix, result, c.getId());
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -6 +6 @@\n"
@@ -946,7 +946,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
// Other Account does not applier in any change updates, replaced with default
assertThat(commitHistoryDiff)
.containsExactly(
@@ -990,7 +990,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -6 +6 @@\n"
@@ -1024,7 +1024,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -6 +6 @@\n"
@@ -1053,7 +1053,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -6 +6 @@\n"
@@ -1100,7 +1100,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -6 +6 @@\n"
@@ -1160,7 +1160,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -7 +7 @@\n"
@@ -1257,7 +1257,6 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
options.dryRun = false;
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
- notesBeforeRewrite.getAttentionSetUpdates();
Instant updateTimestamp = serverIdent.getWhenAsInstant();
ImmutableList<AttentionSetUpdate> attentionSetUpdatesBeforeRewrite =
ImmutableList.of(
@@ -1359,7 +1358,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
assertFixedCommits(commitsToFix.build(), result, c.getId());
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff).hasSize(4);
assertThat(commitHistoryDiff.get(0))
.isEqualTo(
@@ -1571,7 +1570,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
assertFixedCommits(commitsToFix.build(), result, c.getId());
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -6 +6 @@\n"
@@ -1621,7 +1620,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -1 +1 @@\n"
@@ -1702,7 +1701,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
commitsBeforeRewrite, commitsAfterRewrite, ImmutableList.of(invalidCommitIndex));
assertFixedCommits(ImmutableList.of(invalidUpdateCommit.getId()), result, c.getId());
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -10 +10 @@\n"
@@ -1777,7 +1776,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
assertFixedCommits(commitsToFix.build(), result, c.getId());
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -6 +6 @@\n"
@@ -1890,7 +1889,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
assertFixedCommits(commitsToFix.build(), result, c.getId());
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -6 +6 @@\n"
@@ -2065,7 +2064,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
assertFixedCommits(commitsToFix.build(), result, c.getId());
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -8 +8 @@\n"
@@ -2172,7 +2171,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
String expectedFixedIdent = getValidIdentAsString(changeOwner.getAccount());
assertThat(fixedUpdateCommit.getFullMessage()).contains(expectedFixedIdent);
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -9 +9 @@\n"
@@ -2255,7 +2254,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
assertFixedCommits(commitsToFix, result, c.getId());
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -6 +6 @@\n"
@@ -2319,7 +2318,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
options.dryRun = false;
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff)
.containsExactly(
"@@ -6 +6 @@\n"
@@ -2384,7 +2383,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
assertThat(fixedUpdateCommit.getFullMessage()).doesNotContain(changeOwner.getName());
assertThat(fixedUpdateCommit.getFullMessage()).doesNotContain(otherUser.getName());
- List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ ImmutableList<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
assertThat(commitHistoryDiff).hasSize(1);
assertThat(commitHistoryDiff.get(0)).contains("-author Change Owner <1@gerrit>");
assertThat(commitHistoryDiff.get(0)).contains("+author Gerrit User 1 <1@gerrit>");
@@ -2401,6 +2400,7 @@ public class CommitRewriterTest extends AbstractChangeNotesTest {
assertThat(secondRunResult.refsFailedToFix).isEmpty();
}
+ @CanIgnoreReturnValue
private RevCommit writeUpdate(String metaRef, String body, PersonIdent author) throws Exception {
return tr.branch(metaRef).commit().message(body).author(author).committer(serverIdent).create();
}
diff --git a/javatests/com/google/gerrit/server/notedb/IntBlobTest.java b/javatests/com/google/gerrit/server/notedb/IntBlobTest.java
index 333c229147..f335201327 100644
--- a/javatests/com/google/gerrit/server/notedb/IntBlobTest.java
+++ b/javatests/com/google/gerrit/server/notedb/IntBlobTest.java
@@ -16,12 +16,12 @@ package com.google.gerrit.server.notedb;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-import static com.google.gerrit.truth.OptionalSubject.assertThat;
import com.google.gerrit.entities.Project;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.git.LockFailureException;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.truth.OptionalSubject;
import java.io.IOException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
@@ -52,7 +52,7 @@ public class IntBlobTest {
@Test
public void parseNoRef() throws Exception {
- assertThat(IntBlob.parse(repo, "refs/nothing")).isEmpty();
+ OptionalSubject.assertThat(IntBlob.parse(repo, "refs/nothing")).isEmpty();
}
@Test
@@ -66,14 +66,18 @@ public class IntBlobTest {
public void parseValid() throws Exception {
String refName = "refs/foo";
ObjectId id = tr.update(refName, tr.blob("123"));
- assertThat(IntBlob.parse(repo, refName)).value().isEqualTo(IntBlob.create(id, 123));
+ OptionalSubject.assertThat(IntBlob.parse(repo, refName))
+ .value()
+ .isEqualTo(IntBlob.create(id, 123));
}
@Test
public void parseWithWhitespace() throws Exception {
String refName = "refs/foo";
ObjectId id = tr.update(refName, tr.blob(" 123 "));
- assertThat(IntBlob.parse(repo, refName)).value().isEqualTo(IntBlob.create(id, 123));
+ OptionalSubject.assertThat(IntBlob.parse(repo, refName))
+ .value()
+ .isEqualTo(IntBlob.create(id, 123));
}
@Test
@@ -92,7 +96,7 @@ public class IntBlobTest {
IntBlob.tryStore(repo, rw, projectName, refName, null, 123, GitReferenceUpdated.DISABLED);
assertThat(ru.getResult()).isEqualTo(RefUpdate.Result.NEW);
assertThat(ru.getName()).isEqualTo(refName);
- assertThat(IntBlob.parse(repo, refName))
+ OptionalSubject.assertThat(IntBlob.parse(repo, refName))
.value()
.isEqualTo(IntBlob.create(ru.getNewObjectId(), 123));
}
@@ -105,7 +109,7 @@ public class IntBlobTest {
repo, rw, projectName, refName, ObjectId.zeroId(), 123, GitReferenceUpdated.DISABLED);
assertThat(ru.getResult()).isEqualTo(RefUpdate.Result.NEW);
assertThat(ru.getName()).isEqualTo(refName);
- assertThat(IntBlob.parse(repo, refName))
+ OptionalSubject.assertThat(IntBlob.parse(repo, refName))
.value()
.isEqualTo(IntBlob.create(ru.getNewObjectId(), 123));
}
@@ -118,7 +122,7 @@ public class IntBlobTest {
IntBlob.tryStore(repo, rw, projectName, refName, id, 456, GitReferenceUpdated.DISABLED);
assertThat(ru.getResult()).isEqualTo(RefUpdate.Result.FORCED);
assertThat(ru.getName()).isEqualTo(refName);
- assertThat(IntBlob.parse(repo, refName))
+ OptionalSubject.assertThat(IntBlob.parse(repo, refName))
.value()
.isEqualTo(IntBlob.create(ru.getNewObjectId(), 456));
}
@@ -137,14 +141,14 @@ public class IntBlobTest {
GitReferenceUpdated.DISABLED);
assertThat(ru.getResult()).isEqualTo(RefUpdate.Result.LOCK_FAILURE);
assertThat(ru.getName()).isEqualTo(refName);
- assertThat(IntBlob.parse(repo, refName)).isEmpty();
+ OptionalSubject.assertThat(IntBlob.parse(repo, refName)).isEmpty();
}
@Test
public void storeNoOldId() throws Exception {
String refName = "refs/foo";
IntBlob.store(repo, rw, projectName, refName, null, 123, GitReferenceUpdated.DISABLED);
- assertThat(IntBlob.parse(repo, refName))
+ OptionalSubject.assertThat(IntBlob.parse(repo, refName))
.value()
.isEqualTo(IntBlob.create(getRef(refName), 123));
}
@@ -154,7 +158,7 @@ public class IntBlobTest {
String refName = "refs/foo";
IntBlob.store(
repo, rw, projectName, refName, ObjectId.zeroId(), 123, GitReferenceUpdated.DISABLED);
- assertThat(IntBlob.parse(repo, refName))
+ OptionalSubject.assertThat(IntBlob.parse(repo, refName))
.value()
.isEqualTo(IntBlob.create(getRef(refName), 123));
}
@@ -164,7 +168,7 @@ public class IntBlobTest {
String refName = "refs/foo";
ObjectId id = tr.update(refName, tr.blob("123"));
IntBlob.store(repo, rw, projectName, refName, id, 456, GitReferenceUpdated.DISABLED);
- assertThat(IntBlob.parse(repo, refName))
+ OptionalSubject.assertThat(IntBlob.parse(repo, refName))
.value()
.isEqualTo(IntBlob.create(getRef(refName), 456));
}
@@ -185,7 +189,7 @@ public class IntBlobTest {
123,
GitReferenceUpdated.DISABLED));
assertThat(thrown.getFailedRefs()).containsExactly("refs/foo");
- assertThat(IntBlob.parse(repo, refName)).isEmpty();
+ OptionalSubject.assertThat(IntBlob.parse(repo, refName)).isEmpty();
}
private ObjectId getRef(String refName) throws IOException {
diff --git a/javatests/com/google/gerrit/server/notedb/OpenRepoTest.java b/javatests/com/google/gerrit/server/notedb/OpenRepoTest.java
index 323aee9c90..b74089f158 100644
--- a/javatests/com/google/gerrit/server/notedb/OpenRepoTest.java
+++ b/javatests/com/google/gerrit/server/notedb/OpenRepoTest.java
@@ -20,7 +20,6 @@ import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ListMultimap;
import com.google.gerrit.entities.Address;
import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.Change;
@@ -55,7 +54,7 @@ public class OpenRepoTest extends AbstractChangeNotesTest {
ChangeUpdate update = newUpdate(c, changeOwner);
update.setStatus(Change.Status.NEW);
- ListMultimap<String, ChangeUpdate> changeUpdates =
+ ImmutableListMultimap<String, ChangeUpdate> changeUpdates =
new ImmutableListMultimap.Builder<String, ChangeUpdate>().put("one", update).build();
assertThrows(
@@ -73,7 +72,7 @@ public class OpenRepoTest extends AbstractChangeNotesTest {
addToAttentionSet(update);
- ListMultimap<String, ChangeUpdate> changeUpdates =
+ ImmutableListMultimap<String, ChangeUpdate> changeUpdates =
new ImmutableListMultimap.Builder<String, ChangeUpdate>().put("one", update).build();
openRepo.addUpdates(changeUpdates, NO_UPDATES_AT_ALL, MAX_PATCH_SETS);
@@ -96,7 +95,7 @@ public class OpenRepoTest extends AbstractChangeNotesTest {
submitLabel("Verified", "OK", changeOwner.getAccountId()),
submitLabel("Alternative-Code-Review", "NEED", null))));
- ListMultimap<String, ChangeUpdate> changeUpdates =
+ ImmutableListMultimap<String, ChangeUpdate> changeUpdates =
new ImmutableListMultimap.Builder<String, ChangeUpdate>().put("one", update).build();
openRepo.addUpdates(changeUpdates, NO_UPDATES_AT_ALL, MAX_PATCH_SETS);
@@ -112,7 +111,7 @@ public class OpenRepoTest extends AbstractChangeNotesTest {
ChangeUpdate update = newUpdate(c, changeOwner);
update.setStatus(Change.Status.ABANDONED);
- ListMultimap<String, ChangeUpdate> changeUpdates =
+ ImmutableListMultimap<String, ChangeUpdate> changeUpdates =
new ImmutableListMultimap.Builder<String, ChangeUpdate>().put("one", update).build();
openRepo.addUpdates(changeUpdates, NO_UPDATES_AT_ALL, MAX_PATCH_SETS);
@@ -132,7 +131,7 @@ public class OpenRepoTest extends AbstractChangeNotesTest {
ChangeUpdate update2 = newUpdateForNewChange(c1, changeOwner);
update2.setStatus(Change.Status.NEW);
- ListMultimap<String, ChangeUpdate> changeUpdates =
+ ImmutableListMultimap<String, ChangeUpdate> changeUpdates =
new ImmutableListMultimap.Builder<String, ChangeUpdate>().put("two", update2).build();
openRepo.addUpdates(changeUpdates, ONLY_TWO_UPDATES, MAX_PATCH_SETS);
@@ -153,7 +152,7 @@ public class OpenRepoTest extends AbstractChangeNotesTest {
ChangeUpdate update2 = newUpdateForNewChange(c1, changeOwner);
update2.setStatus(Change.Status.NEW);
- ListMultimap<String, ChangeUpdate> changeUpdates =
+ ImmutableListMultimap<String, ChangeUpdate> changeUpdates =
new ImmutableListMultimap.Builder<String, ChangeUpdate>().put("two", update2).build();
assertThrows(
@@ -176,7 +175,7 @@ public class OpenRepoTest extends AbstractChangeNotesTest {
ChangeUpdate update2 = newUpdateForNewChange(c1, changeOwner);
update2.setStatus(Change.Status.NEW);
- ListMultimap<String, ChangeUpdate> changeUpdates =
+ ImmutableListMultimap<String, ChangeUpdate> changeUpdates =
new ImmutableListMultimap.Builder<String, ChangeUpdate>().put("two", update2).build();
assertThrows(
@@ -197,7 +196,7 @@ public class OpenRepoTest extends AbstractChangeNotesTest {
ChangeUpdate update2 = newUpdateForNewChange(c1, changeOwner);
update2.setStatus(Change.Status.NEW);
- ListMultimap<String, ChangeUpdate> changeUpdates =
+ ImmutableListMultimap<String, ChangeUpdate> changeUpdates =
new ImmutableListMultimap.Builder<String, ChangeUpdate>().put("two", update2).build();
assertThrows(
@@ -218,7 +217,7 @@ public class OpenRepoTest extends AbstractChangeNotesTest {
ChangeUpdate update2 = newUpdateForNewChange(c1, changeOwner);
update2.setStatus(Change.Status.NEW);
- ListMultimap<String, ChangeUpdate> changeUpdates =
+ ImmutableListMultimap<String, ChangeUpdate> changeUpdates =
new ImmutableListMultimap.Builder<String, ChangeUpdate>().put("two", update2).build();
assertThrows(
@@ -235,7 +234,7 @@ public class OpenRepoTest extends AbstractChangeNotesTest {
ChangeUpdate update2 = newUpdateForNewChange(c1, changeOwner);
update2.setStatus(Change.Status.NEW);
- ListMultimap<String, ChangeUpdate> changeUpdates =
+ ImmutableListMultimap<String, ChangeUpdate> changeUpdates =
new ImmutableListMultimap.Builder<String, ChangeUpdate>().put("two", update2).build();
assertThrows(
diff --git a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
index 1b2d9065ab..df7922d376 100644
--- a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
+++ b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
@@ -28,6 +28,7 @@ import com.github.rholder.retry.StopStrategies;
import com.google.common.collect.ImmutableList;
import com.google.common.truth.Expect;
import com.google.common.util.concurrent.Runnables;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.StorageException;
@@ -64,7 +65,7 @@ public class RepoSequenceTest {
public void setUp() throws Exception {
repoManager = new InMemoryRepositoryManager();
project = Project.nameKey("project");
- repoManager.createRepository(project);
+ repoManager.createRepository(project).close();
}
@Test
@@ -378,6 +379,7 @@ public class RepoSequenceTest {
retryer);
}
+ @CanIgnoreReturnValue
private ObjectId writeBlob(String sequenceName, String value) {
String refName = RefNames.REFS_SEQUENCES + sequenceName;
try (Repository repo = repoManager.openRepository(project);
diff --git a/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java b/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
index 1c286903f9..f3fe1447fc 100644
--- a/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
+++ b/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
@@ -24,8 +24,11 @@ import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.DiffOperationsTest.FileEntity.FileType;
+import com.google.gerrit.server.patch.diff.ModifiedFilesCacheImpl;
+import com.google.gerrit.server.patch.diff.ModifiedFilesCacheKey;
import com.google.gerrit.server.patch.filediff.FileDiffOutput;
import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
+import com.google.gerrit.server.update.RepoView;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.InMemoryModule;
import com.google.inject.Guice;
@@ -52,6 +55,7 @@ import org.junit.Test;
public class DiffOperationsTest {
@Inject private GitRepositoryManager repoManager;
@Inject private DiffOperations diffOperations;
+ @Inject private ModifiedFilesCacheImpl modifiedFilesCacheImpl;
private static final Project.NameKey testProjectName = Project.nameKey("test-project");
private Repository repo;
@@ -120,7 +124,7 @@ public class DiffOperationsTest {
}
@Test
- public void loadModifiedFiles() throws Exception {
+ public void loadModifiedFilesIfNecessary() throws Exception {
ImmutableList<FileEntity> oldFiles =
ImmutableList.of(
new FileEntity(fileName1, fileContent1), new FileEntity(fileName2, fileContent2));
@@ -132,28 +136,133 @@ public class DiffOperationsTest {
new FileEntity(fileName2, fileContent2 + "\nnew line here"));
ObjectId newCommitId = createCommit(repo, oldCommitId, newFiles);
- Repository repository = repoManager.openRepository(testProjectName);
- ObjectReader objectReader = repository.newObjectReader();
- RevWalk rw = new RevWalk(objectReader);
- StoredConfig repoConfig = repository.getConfig();
-
- // This call loads modified files directly without going through the diff cache.
- Map<String, ModifiedFile> modifiedFiles =
- diffOperations.loadModifiedFiles(
- testProjectName, newCommitId, oldCommitId, DiffOptions.DEFAULTS, rw, repoConfig);
-
- assertThat(modifiedFiles)
- .containsExactly(
- fileName2,
- ModifiedFile.builder()
- .changeType(ChangeType.MODIFIED)
- .oldPath(Optional.of(fileName2))
- .newPath(Optional.of(fileName2))
- .build());
+ try (Repository repository = repoManager.openRepository(testProjectName);
+ ObjectReader objectReader = repository.newObjectReader();
+ RevWalk rw = new RevWalk(objectReader)) {
+ StoredConfig repoConfig = repository.getConfig();
+ ModifiedFilesCacheKey cacheKey =
+ ModifiedFilesCacheKey.builder()
+ .project(testProjectName)
+ .aCommit(oldCommitId)
+ .bCommit(newCommitId)
+ .disableRenameDetection()
+ .build();
+ assertThat(modifiedFilesCacheImpl.getIfPresent(cacheKey)).isEmpty();
+
+ // This call loads modified files directly without going through the diff cache.
+ Map<String, ModifiedFile> modifiedFiles =
+ diffOperations.loadModifiedFilesIfNecessary(
+ testProjectName,
+ oldCommitId,
+ newCommitId,
+ rw,
+ repoConfig,
+ /* enableRenameDetection= */ false);
+
+ ModifiedFile expectedModifiedFile =
+ ModifiedFile.builder()
+ .changeType(ChangeType.MODIFIED)
+ .oldPath(Optional.of(fileName2))
+ .newPath(Optional.of(fileName2))
+ .build();
+ assertThat(modifiedFiles).containsExactly(fileName2, expectedModifiedFile);
+ assertThat(modifiedFilesCacheImpl.getIfPresent(cacheKey))
+ .hasValue(ImmutableList.of(expectedModifiedFile));
+
+ // Check that calling loadModifiedFilesIfNecessary again retrieves the modified files from the
+ // cache, rather than loading them again.
+ Map<String, ModifiedFile> cachedModifiedFiles =
+ diffOperations.loadModifiedFilesIfNecessary(
+ testProjectName,
+ oldCommitId,
+ newCommitId,
+ /* revWalk= */ null, // makes the loading fail if attempted
+ repoConfig,
+ /* enableRenameDetection= */ false);
+ assertThat(cachedModifiedFiles).isEqualTo(modifiedFiles);
+ }
}
@Test
- public void loadModifiedFiles_withSymlinkConvertedToRegularFile() throws Exception {
+ public void loadModifiedFilesIfNecessary_withRename() throws Exception {
+ ImmutableList<FileEntity> oldFiles = ImmutableList.of(new FileEntity(fileName1, fileContent1));
+ ObjectId oldCommitId = createCommit(repo, null, oldFiles);
+
+ ImmutableList<FileEntity> newFiles = ImmutableList.of(new FileEntity(fileName2, fileContent1));
+ ObjectId newCommitId = createCommit(repo, oldCommitId, newFiles);
+
+ try (Repository repository = repoManager.openRepository(testProjectName);
+ ObjectReader objectReader = repository.newObjectReader();
+ RevWalk rw = new RevWalk(objectReader)) {
+ StoredConfig repoConfig = repository.getConfig();
+
+ // load modified files without rename detection
+ ModifiedFilesCacheKey cacheKey =
+ ModifiedFilesCacheKey.builder()
+ .project(testProjectName)
+ .aCommit(oldCommitId)
+ .bCommit(newCommitId)
+ .disableRenameDetection()
+ .build();
+ assertThat(modifiedFilesCacheImpl.getIfPresent(cacheKey)).isEmpty();
+
+ Map<String, ModifiedFile> modifiedFiles =
+ diffOperations.loadModifiedFilesIfNecessary(
+ testProjectName,
+ oldCommitId,
+ newCommitId,
+ rw,
+ repoConfig,
+ /* enableRenameDetection= */ false);
+
+ ModifiedFile expectedDeletedFile =
+ ModifiedFile.builder()
+ .changeType(ChangeType.DELETED)
+ .oldPath(Optional.of(fileName1))
+ .build();
+ ModifiedFile expectedAddedFile =
+ ModifiedFile.builder()
+ .changeType(ChangeType.ADDED)
+ .newPath(Optional.of(fileName2))
+ .build();
+ assertThat(modifiedFiles)
+ .containsExactly(fileName1, expectedDeletedFile, fileName2, expectedAddedFile);
+ assertThat(modifiedFilesCacheImpl.getIfPresent(cacheKey))
+ .hasValue(ImmutableList.of(expectedDeletedFile, expectedAddedFile));
+
+ // load modified files with rename detection
+ cacheKey =
+ ModifiedFilesCacheKey.builder()
+ .project(testProjectName)
+ .aCommit(oldCommitId)
+ .bCommit(newCommitId)
+ .renameScore(DiffOperationsImpl.RENAME_SCORE)
+ .build();
+ assertThat(modifiedFilesCacheImpl.getIfPresent(cacheKey)).isEmpty();
+
+ modifiedFiles =
+ diffOperations.loadModifiedFilesIfNecessary(
+ testProjectName,
+ oldCommitId,
+ newCommitId,
+ rw,
+ repoConfig,
+ /* enableRenameDetection= */ true);
+
+ ModifiedFile expectedRenamedFile =
+ ModifiedFile.builder()
+ .changeType(ChangeType.RENAMED)
+ .oldPath(Optional.of(fileName1))
+ .newPath(Optional.of(fileName2))
+ .build();
+ assertThat(modifiedFiles).containsExactly(fileName2, expectedRenamedFile);
+ assertThat(modifiedFilesCacheImpl.getIfPresent(cacheKey))
+ .hasValue(ImmutableList.of(expectedRenamedFile));
+ }
+ }
+
+ @Test
+ public void loadModifiedFilesIfNecessary_withSymlinkConvertedToRegularFile() throws Exception {
// Commit 1: Create a regular fileName1 with fileContent1
ImmutableList<FileEntity> oldFiles = ImmutableList.of(new FileEntity(fileName1, fileContent1));
ObjectId oldCommitId = createCommit(repo, null, oldFiles);
@@ -163,30 +272,32 @@ public class DiffOperationsTest {
ImmutableList.of(new FileEntity(fileName1, "target", FileType.SYMLINK));
ObjectId newCommitId = createCommit(repo, oldCommitId, newFiles);
- Repository repository = repoManager.openRepository(testProjectName);
- ObjectReader objectReader = repository.newObjectReader();
-
- Map<String, ModifiedFile> modifiedFiles =
- diffOperations.loadModifiedFiles(
- testProjectName,
- newCommitId,
- oldCommitId,
- DiffOptions.DEFAULTS,
- new RevWalk(objectReader),
- repository.getConfig());
-
- assertThat(modifiedFiles)
- .containsExactly(
- fileName1,
- ModifiedFile.builder()
- .changeType(ChangeType.REWRITE)
- .oldPath(Optional.empty())
- .newPath(Optional.of(fileName1))
- .build());
+ try (Repository repository = repoManager.openRepository(testProjectName);
+ ObjectReader objectReader = repository.newObjectReader();
+ RevWalk rw = new RevWalk(objectReader)) {
+
+ Map<String, ModifiedFile> modifiedFiles =
+ diffOperations.loadModifiedFilesIfNecessary(
+ testProjectName,
+ newCommitId,
+ oldCommitId,
+ rw,
+ repository.getConfig(),
+ /* enableRenameDetection= */ false);
+
+ assertThat(modifiedFiles)
+ .containsExactly(
+ fileName1,
+ ModifiedFile.builder()
+ .changeType(ChangeType.REWRITE)
+ .oldPath(Optional.empty())
+ .newPath(Optional.of(fileName1))
+ .build());
+ }
}
@Test
- public void loadModifiedFilesAgainstParent() throws Exception {
+ public void loadModifiedFilesAgainstParentIfNecessary() throws Exception {
ImmutableList<FileEntity> oldFiles =
ImmutableList.of(
new FileEntity(fileName1, fileContent1), new FileEntity(fileName2, fileContent2));
@@ -198,24 +309,116 @@ public class DiffOperationsTest {
new FileEntity(fileName2, fileContent2 + "\nnew line here"));
ObjectId newCommitId = createCommit(repo, oldCommitId, newFiles);
- Repository repository = repoManager.openRepository(testProjectName);
- ObjectReader objectReader = repository.newObjectReader();
- RevWalk rw = new RevWalk(objectReader);
- StoredConfig repoConfig = repository.getConfig();
-
- // This call loads modified files directly without going through the diff cache.
- Map<String, ModifiedFile> modifiedFiles =
- diffOperations.loadModifiedFilesAgainstParent(
- testProjectName, newCommitId, /* parentNum=*/ 0, DiffOptions.DEFAULTS, rw, repoConfig);
-
- assertThat(modifiedFiles)
- .containsExactly(
- fileName2,
- ModifiedFile.builder()
- .changeType(ChangeType.MODIFIED)
- .oldPath(Optional.of(fileName2))
- .newPath(Optional.of(fileName2))
- .build());
+ try (Repository repository = repoManager.openRepository(testProjectName);
+ ObjectInserter ins = repository.newObjectInserter();
+ ObjectReader reader = ins.newReader();
+ RevWalk rw = new RevWalk(reader)) {
+ ModifiedFilesCacheKey cacheKey =
+ ModifiedFilesCacheKey.builder()
+ .project(testProjectName)
+ .aCommit(oldCommitId)
+ .bCommit(newCommitId)
+ .disableRenameDetection()
+ .build();
+ assertThat(modifiedFilesCacheImpl.getIfPresent(cacheKey)).isEmpty();
+
+ // This call loads modified files directly without going through the diff cache.
+ Map<String, ModifiedFile> modifiedFiles =
+ diffOperations.loadModifiedFilesAgainstParentIfNecessary(
+ testProjectName,
+ newCommitId,
+ /* parentNum=*/ 0,
+ new RepoView(repository, rw, ins),
+ ins,
+ /* enableRenameDetection= */ false);
+
+ ModifiedFile expectedModifiedFile =
+ ModifiedFile.builder()
+ .changeType(ChangeType.MODIFIED)
+ .oldPath(Optional.of(fileName2))
+ .newPath(Optional.of(fileName2))
+ .build();
+ assertThat(modifiedFiles).containsExactly(fileName2, expectedModifiedFile);
+ assertThat(modifiedFilesCacheImpl.getIfPresent(cacheKey))
+ .hasValue(ImmutableList.of(expectedModifiedFile));
+ }
+ }
+
+ @Test
+ public void loadModifiedFilesAgainstParentIfNecessary_withRename() throws Exception {
+ ImmutableList<FileEntity> oldFiles = ImmutableList.of(new FileEntity(fileName1, fileContent1));
+ ObjectId oldCommitId = createCommit(repo, null, oldFiles);
+
+ ImmutableList<FileEntity> newFiles = ImmutableList.of(new FileEntity(fileName2, fileContent1));
+ ObjectId newCommitId = createCommit(repo, oldCommitId, newFiles);
+
+ try (Repository repository = repoManager.openRepository(testProjectName);
+ ObjectInserter ins = repository.newObjectInserter();
+ ObjectReader reader = ins.newReader();
+ RevWalk rw = new RevWalk(reader)) {
+ // load modified files without rename detection
+ ModifiedFilesCacheKey cacheKey =
+ ModifiedFilesCacheKey.builder()
+ .project(testProjectName)
+ .aCommit(oldCommitId)
+ .bCommit(newCommitId)
+ .disableRenameDetection()
+ .build();
+ assertThat(modifiedFilesCacheImpl.getIfPresent(cacheKey)).isEmpty();
+
+ Map<String, ModifiedFile> modifiedFiles =
+ diffOperations.loadModifiedFilesAgainstParentIfNecessary(
+ testProjectName,
+ newCommitId,
+ /* parentNum=*/ 0,
+ new RepoView(repository, rw, ins),
+ ins,
+ /* enableRenameDetection= */ false);
+
+ ModifiedFile expectedDeletedFile =
+ ModifiedFile.builder()
+ .changeType(ChangeType.DELETED)
+ .oldPath(Optional.of(fileName1))
+ .build();
+ ModifiedFile expectedAddedFile =
+ ModifiedFile.builder()
+ .changeType(ChangeType.ADDED)
+ .newPath(Optional.of(fileName2))
+ .build();
+ assertThat(modifiedFiles)
+ .containsExactly(fileName1, expectedDeletedFile, fileName2, expectedAddedFile);
+ assertThat(modifiedFilesCacheImpl.getIfPresent(cacheKey))
+ .hasValue(ImmutableList.of(expectedDeletedFile, expectedAddedFile));
+
+ // load modified files with rename detection
+ cacheKey =
+ ModifiedFilesCacheKey.builder()
+ .project(testProjectName)
+ .aCommit(oldCommitId)
+ .bCommit(newCommitId)
+ .renameScore(DiffOperationsImpl.RENAME_SCORE)
+ .build();
+ assertThat(modifiedFilesCacheImpl.getIfPresent(cacheKey)).isEmpty();
+
+ modifiedFiles =
+ diffOperations.loadModifiedFilesAgainstParentIfNecessary(
+ testProjectName,
+ newCommitId,
+ /* parentNum=*/ 0,
+ new RepoView(repository, rw, ins),
+ ins,
+ /* enableRenameDetection= */ true);
+
+ ModifiedFile expectedRenamedFile =
+ ModifiedFile.builder()
+ .changeType(ChangeType.RENAMED)
+ .oldPath(Optional.of(fileName1))
+ .newPath(Optional.of(fileName2))
+ .build();
+ assertThat(modifiedFiles).containsExactly(fileName2, expectedRenamedFile);
+ assertThat(modifiedFilesCacheImpl.getIfPresent(cacheKey))
+ .hasValue(ImmutableList.of(expectedRenamedFile));
+ }
}
static class FileEntity {
diff --git a/javatests/com/google/gerrit/server/patch/IntraLineLoaderTest.java b/javatests/com/google/gerrit/server/patch/IntraLineLoaderTest.java
index 52a81ade12..cc8685ddeb 100644
--- a/javatests/com/google/gerrit/server/patch/IntraLineLoaderTest.java
+++ b/javatests/com/google/gerrit/server/patch/IntraLineLoaderTest.java
@@ -163,7 +163,7 @@ public class IntraLineLoaderTest {
IntraLineLoader.compute(aText, bText, ImmutableList.of(lines), ImmutableSet.of());
assertThat(diff.getStatus()).isEqualTo(IntraLineDiff.Status.EDIT_LIST);
- List<Edit> actualEdits = diff.getEdits();
+ ImmutableList<Edit> actualEdits = diff.getEdits();
assertThat(actualEdits).hasSize(1);
Edit actualEdit = actualEdits.get(0);
assertThat(actualEdit.getBeginA()).isEqualTo(lines.getBeginA());
diff --git a/javatests/com/google/gerrit/server/permissions/DefaultPermissionsMappingTest.java b/javatests/com/google/gerrit/server/permissions/DefaultPermissionsMappingTest.java
index 6c5eb7a746..bd97c1bc95 100644
--- a/javatests/com/google/gerrit/server/permissions/DefaultPermissionsMappingTest.java
+++ b/javatests/com/google/gerrit/server/permissions/DefaultPermissionsMappingTest.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.permissions;
-import static com.google.common.truth.Truth8.assertThat;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.server.permissions.DefaultPermissionMappings.refPermission;
import com.google.gerrit.entities.Permission;
diff --git a/javatests/com/google/gerrit/server/permissions/RefControlTest.java b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
index 43b0ebabad..33698fe30c 100644
--- a/javatests/com/google/gerrit/server/permissions/RefControlTest.java
+++ b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
@@ -248,12 +248,14 @@ public class RefControlTest {
newLocal.commit(md);
}
- requestContext.setContext(() -> null);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(() -> null);
}
@After
public void tearDown() throws Exception {
- requestContext.setContext(null);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(null);
}
@Test
diff --git a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
index 3b7ad1e504..2917c133a6 100644
--- a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
+++ b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
@@ -16,11 +16,11 @@ package com.google.gerrit.server.project;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.entities.BooleanProjectConfig.REQUIRE_CHANGE_ID;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.RuntimeVersion;
import com.google.gerrit.entities.AccessSection;
import com.google.gerrit.entities.AccountGroup;
@@ -1012,6 +1012,7 @@ public class ProjectConfigTest {
});
}
+ @CanIgnoreReturnValue
private Path writeDefaultAllProjectsConfig(String... lines) throws IOException {
Path dir = sitePaths.etc_dir.resolve(ALL_PROJECTS.get());
Files.createDirectories(dir);
diff --git a/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java b/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java
index 180dd9285c..0f4bd2e2f6 100644
--- a/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java
+++ b/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java
@@ -98,7 +98,7 @@ public class SubmitRequirementsAdapterTest {
createLabel("Code-Review", Label.Status.OK),
createLabel("Verified", Label.Status.OK)));
- List<SubmitRequirementResult> requirements =
+ ImmutableList<SubmitRequirementResult> requirements =
SubmitRequirementsAdapter.createResult(
submitRecord, labelTypes, psCommitId, /* isForced= */ false);
@@ -127,7 +127,7 @@ public class SubmitRequirementsAdapterTest {
createLabel("Code-Review", Label.Status.NEED),
createLabel("Verified", Label.Status.NEED)));
- List<SubmitRequirementResult> requirements =
+ ImmutableList<SubmitRequirementResult> requirements =
SubmitRequirementsAdapter.createResult(
submitRecord, labelTypes, psCommitId, /* isForced= */ false);
@@ -159,7 +159,7 @@ public class SubmitRequirementsAdapterTest {
// to indicate that all other records were forced, that's why we explicitly pass isForced=true
// to the "submit requirements adapter". The resulting submit requirement result has a
// status=FORCED.
- List<SubmitRequirementResult> requirements =
+ ImmutableList<SubmitRequirementResult> requirements =
SubmitRequirementsAdapter.createResult(
submitRecord, labelTypes, psCommitId, /* isForced= */ true);
@@ -180,7 +180,7 @@ public class SubmitRequirementsAdapterTest {
Status.NOT_READY,
Arrays.asList(createLabel("ISA-Label", Label.Status.NEED)));
- List<SubmitRequirementResult> requirements =
+ ImmutableList<SubmitRequirementResult> requirements =
SubmitRequirementsAdapter.createResult(
submitRecord, labelTypes, psCommitId, /* isForced= */ false);
@@ -201,7 +201,7 @@ public class SubmitRequirementsAdapterTest {
Status.OK,
Arrays.asList(createLabel("ISA-Label", Label.Status.OK)));
- List<SubmitRequirementResult> requirements =
+ ImmutableList<SubmitRequirementResult> requirements =
SubmitRequirementsAdapter.createResult(
submitRecord, labelTypes, psCommitId, /* isForced= */ false);
@@ -222,7 +222,7 @@ public class SubmitRequirementsAdapterTest {
Status.OK,
Arrays.asList(createLabel("Non-Existing", Label.Status.OK)));
- List<SubmitRequirementResult> requirements =
+ ImmutableList<SubmitRequirementResult> requirements =
SubmitRequirementsAdapter.createResult(
submitRecord, labelTypes, psCommitId, /* isForced= */ false);
@@ -239,7 +239,7 @@ public class SubmitRequirementsAdapterTest {
createLabel("Non-Existing", Label.Status.OK),
createLabel("Code-Review", Label.Status.OK)));
- List<SubmitRequirementResult> requirements =
+ ImmutableList<SubmitRequirementResult> requirements =
SubmitRequirementsAdapter.createResult(
submitRecord, labelTypes, psCommitId, /* isForced= */ false);
@@ -258,7 +258,7 @@ public class SubmitRequirementsAdapterTest {
SubmitRecord submitRecord =
createSubmitRecord("gerrit~IgnoreSelfApprovalRule", Status.OK, Arrays.asList());
- List<SubmitRequirementResult> requirements =
+ ImmutableList<SubmitRequirementResult> requirements =
SubmitRequirementsAdapter.createResult(
submitRecord, labelTypes, psCommitId, /* isForced= */ false);
@@ -276,7 +276,7 @@ public class SubmitRequirementsAdapterTest {
SubmitRecord submitRecord =
createSubmitRecord("gerrit~IgnoreSelfApprovalRule", Status.OK, /* labels= */ null);
- List<SubmitRequirementResult> requirements =
+ ImmutableList<SubmitRequirementResult> requirements =
SubmitRequirementsAdapter.createResult(
submitRecord, labelTypes, psCommitId, /* isForced= */ false);
@@ -294,7 +294,7 @@ public class SubmitRequirementsAdapterTest {
SubmitRecord submitRecord =
createSubmitRecord("gerrit~IgnoreSelfApprovalRule", Status.NOT_READY, Arrays.asList());
- List<SubmitRequirementResult> requirements =
+ ImmutableList<SubmitRequirementResult> requirements =
SubmitRequirementsAdapter.createResult(
submitRecord, labelTypes, psCommitId, /* isForced= */ false);
@@ -317,7 +317,7 @@ public class SubmitRequirementsAdapterTest {
createLabel("custom-label-1", Label.Status.NEED),
createLabel("custom-label-2", Label.Status.REJECT)));
- List<SubmitRequirementResult> requirements =
+ ImmutableList<SubmitRequirementResult> requirements =
SubmitRequirementsAdapter.createResult(
submitRecord, labelTypes, psCommitId, /* isForced= */ false);
@@ -346,7 +346,7 @@ public class SubmitRequirementsAdapterTest {
createLabel("custom-label-1", Label.Status.OK),
createLabel("custom-label-2", Label.Status.REJECT)));
- List<SubmitRequirementResult> requirements =
+ ImmutableList<SubmitRequirementResult> requirements =
SubmitRequirementsAdapter.createResult(
submitRecord, labelTypes, psCommitId, /* isForced= */ false);
diff --git a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index c530c797d5..0075121ed0 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -16,7 +16,6 @@ package com.google.gerrit.server.query.account;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.util.stream.Collectors.toList;
@@ -25,6 +24,7 @@ import static org.junit.Assert.fail;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Project;
@@ -179,7 +179,7 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
Account.Id adminId = createAccount("admin", "Administrator", "admin@example.com", true);
admin = userFactory.create(adminId);
- requestContext.setContext(newRequestContext(adminId));
+ setRequestContextForUser(adminId);
currentUserInfo = gApi.accounts().id(adminId.get()).get();
}
@@ -191,7 +191,8 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
}
protected void setAnonymous() {
- requestContext.setContext(anonymousUser::get);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(anonymousUser::get);
}
@After
@@ -199,7 +200,8 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
if (lifecycle != null) {
lifecycle.stop();
}
- requestContext.setContext(null);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(null);
}
@Test
@@ -271,7 +273,7 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
addEmails(user1, secondaryEmail);
AccountInfo user2 = newAccount("user");
- requestContext.setContext(newRequestContext(Account.id(user2._accountId)));
+ setRequestContextForUser(Account.id(user2._accountId));
assertQuery(preferredEmail, user1);
assertQuery(secondaryEmail);
@@ -347,7 +349,7 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
AccountInfo user2 = newAccountWithFullName("jroe", "Jane Roe");
AccountInfo user3 = newAccount("user");
- requestContext.setContext(newRequestContext(Account.id(user3._accountId)));
+ setRequestContextForUser(Account.id(user3._accountId));
assertQuery("notexisting");
assertQuery("Not Existing");
@@ -403,7 +405,7 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
Project.NameKey p = createProject(name("p"));
// Create the change as User1
- requestContext.setContext(newRequestContext(Account.id(user1._accountId)));
+ setRequestContextForUser(Account.id(user1._accountId));
ChangeInfo c = createPrivateChange(p);
assertThat(c.owner).isEqualTo(user1);
@@ -412,19 +414,19 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
addReviewer(c.changeId, user3.email, ReviewerState.CC);
// Request as the owner
- requestContext.setContext(newRequestContext(Account.id(user1._accountId)));
+ setRequestContextForUser(Account.id(user1._accountId));
assertQuery("cansee:" + c.changeId, user1, user2, user3);
// Request as the reviewer
- requestContext.setContext(newRequestContext(Account.id(user2._accountId)));
+ setRequestContextForUser(Account.id(user2._accountId));
assertQuery("cansee:" + c.changeId, user1, user2, user3);
// Request as the CC
- requestContext.setContext(newRequestContext(Account.id(user3._accountId)));
+ setRequestContextForUser(Account.id(user3._accountId));
assertQuery("cansee:" + c.changeId, user1, user2, user3);
// Request as an account not in {owner, reviewer, CC}
- requestContext.setContext(newRequestContext(Account.id(user4._accountId)));
+ setRequestContextForUser(Account.id(user4._accountId));
BadRequestException exception =
assertThrows(BadRequestException.class, () -> newQuery("cansee:" + c.changeId).get());
assertThat(exception)
@@ -626,7 +628,7 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
String[] secondaryEmails = new String[] {"dfg@example.com", "hij@example.com"};
addEmails(otherUser, secondaryEmails);
- requestContext.setContext(newRequestContext(Account.id(user._accountId)));
+ setRequestContextForUser(Account.id(user._accountId));
List<AccountInfo> result = newQuery(getDefaultSearch(otherUser)).withSuggest(true).get();
assertThat(result.get(0).secondaryEmails).isNull();
@@ -667,8 +669,8 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
// we load AccountStates from the cache after reading documents from the index
// which means we always read fresh data when matching.
//
- // Reindex document
- gApi.accounts().id(user1.username).index();
+ // Reindex account document
+ gApi.accounts().id(user1._accountId).index();
assertQuery("name:" + quote(user1.name));
assertQuery("name:" + quote(newName), user1);
}
@@ -870,6 +872,11 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
}
}
+ private void setRequestContextForUser(Account.Id userId) {
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(newRequestContext(userId));
+ }
+
private void addEmails(AccountInfo account, String... emails) throws Exception {
Account.Id id = Account.id(account._accountId);
for (String email : emails) {
@@ -882,19 +889,22 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
return gApi.accounts().query(query.toString());
}
+ @CanIgnoreReturnValue
protected List<AccountInfo> assertQuery(Object query, AccountInfo... accounts) throws Exception {
return assertQuery(newQuery(query), accounts);
}
+ @CanIgnoreReturnValue
protected List<AccountInfo> assertQuery(QueryRequest query, AccountInfo... accounts)
throws Exception {
return assertQuery(query, Arrays.asList(accounts));
}
+ @CanIgnoreReturnValue
protected List<AccountInfo> assertQuery(QueryRequest query, List<AccountInfo> accounts)
throws Exception {
List<AccountInfo> result = query.get();
- Iterable<Integer> ids = ids(result);
+ List<Integer> ids = ids(result);
assertWithMessage(format(query, result, accounts))
.that(ids)
.containsExactlyElementsIn(ids(accounts))
@@ -944,11 +954,11 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
return b.toString();
}
- protected static Iterable<Integer> ids(AccountInfo... accounts) {
+ protected static List<Integer> ids(AccountInfo... accounts) {
return ids(Arrays.asList(accounts));
}
- protected static Iterable<Integer> ids(List<AccountInfo> accounts) {
+ protected static List<Integer> ids(List<AccountInfo> accounts) {
return accounts.stream().map(a -> a._accountId).collect(toList());
}
diff --git a/javatests/com/google/gerrit/server/query/account/BUILD b/javatests/com/google/gerrit/server/query/account/BUILD
index 2c31aeff6c..fb0e73958c 100644
--- a/javatests/com/google/gerrit/server/query/account/BUILD
+++ b/javatests/com/google/gerrit/server/query/account/BUILD
@@ -24,6 +24,7 @@ java_library(
"//java/com/google/gerrit/testing:gerrit-test-util",
"//lib:guava",
"//lib:jgit",
+ "//lib/errorprone:annotations",
"//lib/guice",
"//lib/truth",
"//lib/truth:truth-java8-extension",
@@ -41,6 +42,7 @@ junit_tests(
":abstract_query_tests",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib:guava",
"//lib:jgit",
"//lib/guice",
],
@@ -57,6 +59,7 @@ junit_tests(
":abstract_query_tests",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib:guava",
"//lib:jgit",
"//lib/guice",
],
diff --git a/javatests/com/google/gerrit/server/query/account/FakeQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/FakeQueryAccountsTest.java
index 31d256e02e..69ed9483eb 100644
--- a/javatests/com/google/gerrit/server/query/account/FakeQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/FakeQueryAccountsTest.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.account;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.InMemoryModule;
@@ -21,7 +22,6 @@ import com.google.gerrit.testing.IndexConfig;
import com.google.gerrit.testing.IndexVersions;
import com.google.inject.Guice;
import com.google.inject.Injector;
-import java.util.List;
import java.util.Map;
import org.eclipse.jgit.lib.Config;
@@ -34,7 +34,7 @@ public class FakeQueryAccountsTest extends AbstractQueryAccountsTest {
@ConfigSuite.Configs
public static Map<String, Config> againstPreviousIndexVersion() {
// the current schema version is already tested by the inherited default config suite
- List<Integer> schemaVersions =
+ ImmutableList<Integer> schemaVersions =
IndexVersions.getWithoutLatest(AccountSchemaDefinitions.INSTANCE);
return IndexVersions.asConfigMap(
AccountSchemaDefinitions.INSTANCE, schemaVersions, "againstIndexVersion", defaultConfig());
diff --git a/javatests/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java
index e36b79e6f6..424b5c4319 100644
--- a/javatests/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.account;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.InMemoryModule;
@@ -21,7 +22,6 @@ import com.google.gerrit.testing.IndexConfig;
import com.google.gerrit.testing.IndexVersions;
import com.google.inject.Guice;
import com.google.inject.Injector;
-import java.util.List;
import java.util.Map;
import org.eclipse.jgit.lib.Config;
@@ -34,7 +34,7 @@ public class LuceneQueryAccountsTest extends AbstractQueryAccountsTest {
@ConfigSuite.Configs
public static Map<String, Config> againstPreviousIndexVersion() {
// the current schema version is already tested by the inherited default config suite
- List<Integer> schemaVersions =
+ ImmutableList<Integer> schemaVersions =
IndexVersions.getWithoutLatest(AccountSchemaDefinitions.INSTANCE);
return IndexVersions.asConfigMap(
AccountSchemaDefinitions.INSTANCE, schemaVersions, "againstIndexVersion", defaultConfig());
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index b3a2658294..7a7cff58f4 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -41,6 +41,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
@@ -186,7 +187,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Inject protected BatchUpdate.Factory updateFactory;
@Inject protected AllProjectsName allProjectsName;
@Inject protected ChangeInserter.Factory changeFactory;
- @Inject protected ChangeQueryBuilder queryBuilder;
+ @Inject protected Provider<ChangeQueryBuilder> queryBuilderProvider;
@Inject protected GerritApi gApi;
@Inject protected IdentifiedUser.GenericFactory userFactory;
@Inject protected ChangeIndexCollection indexes;
@@ -279,7 +280,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
protected void resetUser() throws ConfigInvalidException, IOException {
user = userFactory.create(userId);
userAccount = accounts.get(userId).get().account();
- requestContext.setContext(newRequestContext(userId));
+ setRequestContextForUser(userId);
}
@After
@@ -287,7 +288,8 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
if (lifecycle != null) {
lifecycle.stop();
}
- requestContext.setContext(null);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(null);
}
@Before
@@ -314,9 +316,10 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byId() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChange(repo));
assertQuery("12345");
assertQuery(change1.getId().get(), change1);
@@ -325,8 +328,9 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byKey() throws Exception {
- repo = createAndOpenProject("repo");
- Change change = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change = insert(project, newChange(repo));
String key = change.getKey().get();
assertQuery("I0000000000000000000000000000000000000000");
@@ -338,8 +342,9 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byTriplet() throws Exception {
- repo = createAndOpenProject("iabcde");
- Change change = insert("iabcde", newChangeForBranch(repo, "branch"));
+ Project.NameKey project = Project.nameKey("iabcde");
+ repo = createAndOpenProject(project);
+ Change change = insert(project, newChangeForBranch(repo, "branch"));
String k = change.getKey().get();
assertQuery("iabcde~branch~" + k, change);
@@ -361,11 +366,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byStatus() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW);
- Change change1 = insert("repo", ins1);
+ Change change1 = insert(project, ins1);
ChangeInserter ins2 = newChangeWithStatus(repo, Change.Status.MERGED);
- Change change2 = insert("repo", ins2);
+ Change change2 = insert(project, ins2);
assertQuery("status:new", change1);
assertQuery("status:NEW", change1);
@@ -380,11 +386,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byStatusOr() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW);
- Change change1 = insert("repo", ins1);
+ Change change1 = insert(project, ins1);
ChangeInserter ins2 = newChangeWithStatus(repo, Change.Status.MERGED);
- Change change2 = insert("repo", ins2);
+ Change change2 = insert(project, ins2);
assertQuery("status:new OR status:merged", change2, change1);
assertQuery("status:new or status:merged", change2, change1);
@@ -392,10 +399,11 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byStatusOpen() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW);
- Change change1 = insert("repo", ins1);
- insert("repo", newChangeWithStatus(repo, Change.Status.MERGED));
+ Change change1 = insert(project, ins1);
+ insert(project, newChangeWithStatus(repo, Change.Status.MERGED));
Change[] expected = new Change[] {change1};
assertQuery("status:open", expected);
@@ -414,12 +422,13 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byStatusClosed() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.MERGED);
- Change change1 = insert("repo", ins1);
+ Change change1 = insert(project, ins1);
ChangeInserter ins2 = newChangeWithStatus(repo, Change.Status.ABANDONED);
- Change change2 = insert("repo", ins2);
- insert("repo", newChangeWithStatus(repo, Change.Status.NEW));
+ Change change2 = insert(project, ins2);
+ insert(project, newChangeWithStatus(repo, Change.Status.NEW));
Change[] expected = new Change[] {change2, change1};
assertQuery("status:closed", expected);
@@ -435,12 +444,13 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byStatusAbandoned() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.MERGED);
- insert("repo", ins1);
+ insert(project, ins1);
ChangeInserter ins2 = newChangeWithStatus(repo, Change.Status.ABANDONED);
- Change change1 = insert("repo", ins2);
- insert("repo", newChangeWithStatus(repo, Change.Status.NEW));
+ Change change1 = insert(project, ins2);
+ insert(project, newChangeWithStatus(repo, Change.Status.NEW));
assertQuery("status:abandoned", change1);
assertQuery("status:ABANDONED", change1);
@@ -449,10 +459,11 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byStatusPrefix() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW);
- Change change1 = insert("repo", ins1);
- Change change2 = insert("repo", newChangeWithStatus(repo, Change.Status.MERGED));
+ Change change1 = insert(project, ins1);
+ Change change2 = insert(project, newChangeWithStatus(repo, Change.Status.MERGED));
assertQuery("status:n", change1);
assertQuery("status:ne", change1);
@@ -469,11 +480,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byPrivate() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo), userId);
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo), userId);
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- Change change2 = insert("repo", newChange(repo), user2);
+ Change change2 = insert(project, newChange(repo), user2);
// No private changes.
assertQuery("is:open", change2, change1);
@@ -486,15 +498,16 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
assertQuery("is:private", change1);
// Switch request context to user2.
- requestContext.setContext(newRequestContext(user2));
+ setRequestContextForUser(user2);
assertQuery("is:open", change2);
assertQuery("is:private");
}
@Test
public void byWip() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo), userId);
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo), userId);
assertQuery("is:open", change1);
assertQuery("is:wip");
@@ -511,8 +524,9 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void excludeWipChangeFromReviewersDashboards() throws Exception {
Account.Id user1 = createAccount("user1");
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChangeWorkInProgress(repo), userId);
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChangeWorkInProgress(repo), userId);
assertQuery("is:wip", change1);
assertQuery("reviewer:" + user1);
@@ -528,8 +542,9 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byStarted() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChangeWorkInProgress(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChangeWorkInProgress(repo));
assertQuery("is:started");
@@ -564,11 +579,11 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void restorePendingReviewers() throws Exception {
Project.NameKey project = Project.nameKey("repo");
- repo = createAndOpenProject(project.get());
+ repo = createAndOpenProject(project);
ConfigInput conf = new ConfigInput();
conf.enableReviewerByEmail = InheritableBoolean.TRUE;
gApi.projects().name(project.get()).config(conf);
- Change change1 = insert("repo", newChangeWorkInProgress(repo));
+ Change change1 = insert(project, newChangeWorkInProgress(repo));
Account.Id user1 = createAccount("user1");
Account.Id user2 = createAccount("user2");
String email1 = "email1@example.com";
@@ -621,9 +636,10 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byCommit() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
ChangeInserter ins = newChange(repo);
- Change change = insert("repo", ins);
+ Change change = insert(project, ins);
String sha = ins.getCommitId().name();
assertQuery("0000000000000000000000000000000000000000");
@@ -637,11 +653,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byOwner() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo), userId);
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo), userId);
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- Change change2 = insert("repo", newChange(repo), user2);
+ Change change2 = insert(project, newChange(repo), user2);
assertQuery("is:owner", change1);
assertQuery("owner:" + userId.get(), change1);
@@ -655,20 +672,21 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
public void byUploader() throws Exception {
assume().that(getSchema().hasField(ChangeField.UPLOADER_SPEC)).isTrue();
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo), userId);
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo), userId);
assertQuery("is:uploader", change1);
assertQuery("uploader:" + userId.get(), change1);
Account.Id user2 = createAccount("anotheruser");
CurrentUser user2CurrentUser = userFactory.create(user2);
- change1 = newPatchSet("repo", change1, user2CurrentUser, /* message= */ Optional.empty());
+ change1 = newPatchSet(project, change1, user2CurrentUser, /* message= */ Optional.empty());
// Uploader has changed
assertQuery("uploader:" + userId.get());
assertQuery("uploader:" + user2.get(), change1);
- requestContext.setContext(newRequestContext(user2));
+ setRequestContextForUser(user2);
assertQuery("is:uploader", change1); // self (user2)
String nameEmail = user2CurrentUser.asIdentifiedUser().getNameEmail();
@@ -706,7 +724,8 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
}
private void byAuthorOrCommitterExact(String searchOperator) throws Exception {
- createProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ createProject(project);
PersonIdent johnDoe = new PersonIdent("John Doe", "john.doe@example.com");
PersonIdent john = new PersonIdent("John", "john@example.com");
PersonIdent doeSmith = new PersonIdent("Doe Smith", "doe_smith@example.com");
@@ -714,10 +733,10 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
PersonIdent myself = new PersonIdent("I Am", ua.preferredEmail());
PersonIdent selfName = new PersonIdent("My Self", "my.self@example.com");
- Change change1 = createChange("repo", johnDoe);
- Change change2 = createChange("repo", john);
- Change change3 = createChange("repo", doeSmith);
- createChange("repo", selfName);
+ Change change1 = createChange(project, johnDoe);
+ Change change2 = createChange(project, john);
+ Change change3 = createChange(project, doeSmith);
+ createChange(project, selfName);
// Only email address.
assertQuery(searchOperator + "john.doe@example.com", change1);
@@ -742,19 +761,20 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
assertQuery(searchOperator + "self");
// ':self' matches a change created with the current user's email address
- Change change5 = createChange("repo", myself);
+ Change change5 = createChange(project, myself);
assertQuery(searchOperator + "me", change5);
assertQuery(searchOperator + "self", change5);
}
private void byAuthorOrCommitterFullText(String searchOperator) throws Exception {
- createProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ createProject(project);
PersonIdent johnDoe = new PersonIdent("John Doe", "john.doe@example.com");
PersonIdent john = new PersonIdent("John", "john@example.com");
PersonIdent doeSmith = new PersonIdent("Doe Smith", "doe_smith@example.com");
- Change change1 = createChange("repo", johnDoe);
- Change change2 = createChange("repo", john);
- Change change3 = createChange("repo", doeSmith);
+ Change change1 = createChange(project, johnDoe);
+ Change change2 = createChange(project, john);
+ Change change3 = createChange(project, doeSmith);
// By exact name.
assertQuery(searchOperator + "\"John Doe\"", change1);
@@ -776,24 +796,25 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
}
@CanIgnoreReturnValue
- protected Change createChange(String repoName, PersonIdent person) throws Exception {
+ protected Change createChange(Project.NameKey project, PersonIdent person) throws Exception {
try (TestRepository<Repository> repo =
- new TestRepository<>(repoManager.openRepository(Project.nameKey(repoName)))) {
+ new TestRepository<>(repoManager.openRepository(project))) {
RevCommit commit =
repo.parseBody(
repo.commit().message("message").author(person).committer(person).create());
- return insert("repo", newChangeForCommit(repo, commit), null);
+ return insert(project, newChangeForCommit(repo, commit), null);
}
}
@Test
public void byOwnerIn() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo), userId);
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo), userId);
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- Change change2 = insert("repo", newChange(repo), user2);
- Change change3 = insert("repo", newChange(repo), user2);
+ Change change2 = insert(project, newChange(repo), user2);
+ Change change3 = insert(project, newChange(repo), user2);
getChangeApi(change3).current().review(ReviewInput.approve());
getChangeApi(change3).current().submit();
@@ -841,14 +862,15 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byUploaderIn() throws Exception {
assume().that(getSchema().hasField(ChangeField.UPLOADER_SPEC)).isTrue();
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo), userId);
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo), userId);
assertQuery("uploaderin:Administrators", change1);
Account.Id user2 = createAccount("anotheruser");
CurrentUser user2CurrentUser = userFactory.create(user2);
- change1 = newPatchSet("repo", change1, user2CurrentUser, /* message= */ Optional.empty());
+ change1 = newPatchSet(project, change1, user2CurrentUser, /* message= */ Optional.empty());
assertQuery("uploaderin:Administrators");
assertQuery("uploaderin:\"Registered Users\"", change1);
@@ -887,10 +909,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byProject() throws Exception {
- createProject("repo1");
- createProject("repo2");
- Change change1 = insert("repo1", newChange("repo1"));
- Change change2 = insert("repo2", newChange("repo2"));
+ Project.NameKey project1 = Project.nameKey("repo1");
+ repo = createAndOpenProject(project1);
+ Project.NameKey project2 = Project.nameKey("repo2");
+ repo = createAndOpenProject(project2);
+ Change change1 = insert(project1, newChange(project1));
+ Change change2 = insert(project2, newChange(project2));
assertQuery("project:foo");
assertQuery("project:repo");
@@ -900,16 +924,18 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byProjectWithHidden() throws Exception {
- createProject("hiddenProject");
- insert("hiddenProject", newChange("hiddenProject"));
+ Project.NameKey hiddenProject = Project.nameKey("hiddenProject");
+ createProject(hiddenProject);
+ insert(hiddenProject, newChange(hiddenProject));
projectOperations
- .project(Project.nameKey("hiddenProject"))
+ .project(hiddenProject)
.forUpdate()
.add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
.update();
- createProject("visibleProject");
- Change visibleChange = insert("visibleProject", newChange("visibleProject"));
+ Project.NameKey visibleProject = Project.nameKey("visibleProject");
+ createProject(visibleProject);
+ Change visibleChange = insert(visibleProject, newChange(visibleProject));
assertQuery("project:visibleProject", visibleChange);
assertQuery("project:hiddenProject");
assertQuery("project:visibleProject OR project:hiddenProject", visibleChange);
@@ -917,13 +943,14 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byParentOf() throws Exception {
- repo = createAndOpenProject("repo1");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
RevCommit commit1 = repo.parseBody(repo.commit().message("message").create());
- Change change1 = insert("repo1", newChangeForCommit(repo, commit1));
+ Change change1 = insert(project, newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit(commit1));
- Change change2 = insert("repo1", newChangeForCommit(repo, commit2));
+ Change change2 = insert(project, newChangeForCommit(repo, commit2));
RevCommit commit3 = repo.parseBody(repo.commit(commit1, commit2));
- Change change3 = insert("repo1", newChangeForCommit(repo, commit3));
+ Change change3 = insert(project, newChangeForCommit(repo, commit3));
assertQuery("parentof:" + change1.getId().get());
assertQuery("parentof:" + change1.getKey().get());
@@ -935,10 +962,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byParentProject() throws Exception {
- createProject("repo1");
- createProject("repo2", "repo1");
- Change change1 = insert("repo1", newChange("repo1"));
- Change change2 = insert("repo2", newChange("repo2"));
+ Project.NameKey project1 = Project.nameKey("repo1");
+ createProject(project1);
+ Project.NameKey project2 = Project.nameKey("repo2");
+ createProject(project2, project1);
+ Change change1 = insert(project1, newChange(project1));
+ Change change2 = insert(project2, newChange(project2));
assertQuery("parentproject:repo1", change2, change1);
assertQuery("parentproject:repo2", change2);
@@ -946,10 +975,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byProjectPrefix() throws Exception {
- createProject("repo1");
- createProject("repo2");
- Change change1 = insert("repo1", newChange("repo1"));
- Change change2 = insert("repo2", newChange("repo2"));
+ Project.NameKey project1 = Project.nameKey("repo1");
+ Project.NameKey project2 = Project.nameKey("repo2");
+ createProject(project1);
+ createProject(project2);
+ Change change1 = insert(project1, newChange(project1));
+ Change change2 = insert(project2, newChange(project2));
assertQuery("projects:foo");
assertQuery("projects:repo1", change1);
@@ -959,10 +990,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byRepository() throws Exception {
- createProject("repo1");
- createProject("repo2");
- Change change1 = insert("repo1", newChange("repo1"));
- Change change2 = insert("repo2", newChange("repo2"));
+ Project.NameKey project1 = Project.nameKey("repo1");
+ createProject(project1);
+ Project.NameKey project2 = Project.nameKey("repo2");
+ createProject(project2);
+ Change change1 = insert(project1, newChange(project1));
+ Change change2 = insert(project2, newChange(project2));
assertQuery("repository:foo");
assertQuery("repository:repo");
@@ -972,10 +1005,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byParentRepository() throws Exception {
- createProject("repo1");
- createProject("repo2", "repo1");
- Change change1 = insert("repo1", newChange("repo1"));
- Change change2 = insert("repo2", newChange("repo2"));
+ Project.NameKey project1 = Project.nameKey("repo1");
+ createProject(project1);
+ Project.NameKey project2 = Project.nameKey("repo2");
+ createProject(project2, project1);
+ Change change1 = insert(project1, newChange(project1));
+ Change change2 = insert(project2, newChange(project2));
assertQuery("parentrepository:repo1", change2, change1);
assertQuery("parentrepository:repo2", change2);
@@ -983,10 +1018,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byRepositoryPrefix() throws Exception {
- createProject("repo1");
- createProject("repo2");
- Change change1 = insert("repo1", newChange("repo1"));
- Change change2 = insert("repo2", newChange("repo2"));
+ Project.NameKey project1 = Project.nameKey("repo1");
+ createProject(project1);
+ Project.NameKey project2 = Project.nameKey("repo2");
+ createProject(project2);
+ Change change1 = insert(project1, newChange(project1));
+ Change change2 = insert(project2, newChange(project2));
assertQuery("repositories:foo");
assertQuery("repositories:repo1", change1);
@@ -996,10 +1033,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byRepo() throws Exception {
- createProject("repo1");
- createProject("repo2");
- Change change1 = insert("repo1", newChange("repo1"));
- Change change2 = insert("repo2", newChange("repo2"));
+ Project.NameKey project1 = Project.nameKey("repo1");
+ createProject(project1);
+ Project.NameKey project2 = Project.nameKey("repo2");
+ createProject(project2);
+ Change change1 = insert(project1, newChange(project1));
+ Change change2 = insert(project2, newChange(project2));
assertQuery("repo:foo");
assertQuery("repo:repo");
@@ -1009,10 +1048,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byParentRepo() throws Exception {
- createProject("repo1");
- createProject("repo2", "repo1");
- Change change1 = insert("repo1", newChange("repo1"));
- Change change2 = insert("repo2", newChange("repo2"));
+ Project.NameKey project1 = Project.nameKey("repo1");
+ createProject(project1);
+ Project.NameKey project2 = Project.nameKey("repo2");
+ createProject(project2, project1);
+ Change change1 = insert(project1, newChange(project1));
+ Change change2 = insert(project2, newChange(project2));
assertQuery("parentrepo:repo1", change2, change1);
assertQuery("parentrepo:repo2", change2);
@@ -1020,10 +1061,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byRepoPrefix() throws Exception {
- createProject("repo1");
- createProject("repo2");
- Change change1 = insert("repo1", newChange("repo1"));
- Change change2 = insert("repo2", newChange("repo2"));
+ Project.NameKey project1 = Project.nameKey("repo1");
+ createProject(project1);
+ Project.NameKey project2 = Project.nameKey("repo2");
+ createProject(project2);
+ Change change1 = insert(project1, newChange(project1));
+ Change change2 = insert(project2, newChange(project2));
assertQuery("repos:foo");
assertQuery("repos:repo1", change1);
@@ -1033,9 +1076,10 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byBranchAndRef() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChangeForBranch(repo, "master"));
- Change change2 = insert("repo", newChangeForBranch(repo, "branch"));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChangeForBranch(repo, "master"));
+ Change change2 = insert(project, newChangeForBranch(repo, "branch"));
assertQuery("branch:foo");
assertQuery("branch:master", change1);
@@ -1051,27 +1095,27 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byTopic() throws Exception {
-
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
ChangeInserter ins1 = newChangeWithTopic(repo, "feature1");
- Change change1 = insert("repo", ins1);
+ Change change1 = insert(project, ins1);
ChangeInserter ins2 = newChangeWithTopic(repo, "feature2");
- Change change2 = insert("repo", ins2);
+ Change change2 = insert(project, ins2);
ChangeInserter ins3 = newChangeWithTopic(repo, "Cherrypick-feature2");
- Change change3 = insert("repo", ins3);
+ Change change3 = insert(project, ins3);
ChangeInserter ins4 = newChangeWithTopic(repo, "feature2-fixup");
- Change change4 = insert("repo", ins4);
+ Change change4 = insert(project, ins4);
ChangeInserter ins5 = newChangeWithTopic(repo, "https://gerrit.local");
- Change change5 = insert("repo", ins5);
+ Change change5 = insert(project, ins5);
ChangeInserter ins6 = newChangeWithTopic(repo, "git_gerrit_training");
- Change change6 = insert("repo", ins6);
+ Change change6 = insert(project, ins6);
- Change changeNoTopic = insert("repo", newChange(repo));
+ Change changeNoTopic = insert(project, newChange(repo));
assertQuery("intopic:foo");
assertQuery("intopic:feature1", change1);
@@ -1091,16 +1135,17 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byTopicRegex() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
ChangeInserter ins1 = newChangeWithTopic(repo, "feature1");
- Change change1 = insert("repo", ins1);
+ Change change1 = insert(project, ins1);
ChangeInserter ins2 = newChangeWithTopic(repo, "Cherrypick-feature1");
- Change change2 = insert("repo", ins2);
+ Change change2 = insert(project, ins2);
ChangeInserter ins3 = newChangeWithTopic(repo, "feature1-fixup");
- Change change3 = insert("repo", ins3);
+ Change change3 = insert(project, ins3);
assertQuery("intopic:^feature1.*", change3, change1);
assertQuery("intopic:{^.*feature1$}", change2, change1);
@@ -1108,25 +1153,25 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byMessageExact_byAlias_d() throws Exception {
- byMessageExact("d:", "d_repo");
+ byMessageExact("d:", Project.nameKey("d_repo"));
}
@Test
public void byMessageExact_byAlias_description() throws Exception {
- byMessageExact("description:", "description_repo");
+ byMessageExact("description:", Project.nameKey("description_repo"));
}
@Test
public void byMessageExact_byAlias_m() throws Exception {
- byMessageExact("m:", "m_repo");
+ byMessageExact("m:", Project.nameKey("m_repo"));
}
@Test
public void byMessageExact_byMainOperator() throws Exception {
- byMessageExact("message:", "message_repo");
+ byMessageExact("message:", Project.nameKey("message_repo"));
}
- private void byMessageExact(String searchOperator, String projectName) throws Exception {
+ private void byMessageExact(String searchOperator, Project.NameKey projectName) throws Exception {
repo = createAndOpenProject(projectName);
RevCommit commit1 = repo.parseBody(repo.commit().message("one").create());
Change change1 = insert(projectName, newChangeForCommit(repo, commit1));
@@ -1143,25 +1188,25 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byMessageRegEx_byAlias_d() throws Exception {
- byMessageRegEx("d:", "d_repo");
+ byMessageRegEx("d:", Project.nameKey("d_repo"));
}
@Test
public void byMessageRegEx_byAlias_description() throws Exception {
- byMessageRegEx("description:", "description_repo");
+ byMessageRegEx("description:", Project.nameKey("description_repo"));
}
@Test
public void byMessageRegEx_byAlias_m() throws Exception {
- byMessageRegEx("m:", "m_repo");
+ byMessageRegEx("m:", Project.nameKey("m_repo"));
}
@Test
public void byMessageRegEx_byMainOperator() throws Exception {
- byMessageRegEx("message:", "message_repo");
+ byMessageRegEx("message:", Project.nameKey("message_repo"));
}
- private void byMessageRegEx(String searchOperator, String projectName) throws Exception {
+ private void byMessageRegEx(String searchOperator, Project.NameKey projectName) throws Exception {
assume().that(getSchema().hasField(ChangeField.COMMIT_MESSAGE_EXACT)).isTrue();
repo = createAndOpenProject(projectName);
RevCommit commit1 = repo.parseBody(repo.commit().message("aaaabcc").create());
@@ -1186,7 +1231,8 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void bySubject() throws Exception {
assume().that(getSchema().hasField(ChangeField.SUBJECT_SPEC)).isTrue();
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
RevCommit commit1 =
repo.parseBody(
repo.commit()
@@ -1195,7 +1241,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
+ "Message body\n\n"
+ "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b920")
.create());
- Change change1 = insert("repo", newChangeForCommit(repo, commit1));
+ Change change1 = insert(project, newChangeForCommit(repo, commit1));
RevCommit commit2 =
repo.parseBody(
repo.commit()
@@ -1204,7 +1250,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
+ "Message body for another commit\n\n"
+ "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b921")
.create());
- Change change2 = insert("repo", newChangeForCommit(repo, commit2));
+ Change change2 = insert(project, newChangeForCommit(repo, commit2));
RevCommit commit3 =
repo.parseBody(
repo.commit()
@@ -1213,7 +1259,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
+ "Last message body\n\n"
+ "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b921")
.create());
- Change change3 = insert("repo", newChangeForCommit(repo, commit3));
+ Change change3 = insert(project, newChangeForCommit(repo, commit3));
assertQuery("subject:First", change1);
assertQuery("subject:Second", change2);
@@ -1223,7 +1269,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
assertQuery("subject:body");
change1 =
newPatchSet(
- "repo",
+ project,
change1,
user,
Optional.of("Rework of commit with test subject\n\n" + "Message body\n\n"));
@@ -1235,7 +1281,8 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void bySubjectPrefix() throws Exception {
assume().that(getSchema().hasField(ChangeField.PREFIX_SUBJECT_SPEC)).isTrue();
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
RevCommit commit1 =
repo.parseBody(
repo.commit()
@@ -1244,7 +1291,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
+ "Message body\n\n"
+ "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b920")
.create());
- Change change1 = insert("repo", newChangeForCommit(repo, commit1));
+ Change change1 = insert(project, newChangeForCommit(repo, commit1));
RevCommit commit2 =
repo.parseBody(
repo.commit()
@@ -1253,7 +1300,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
+ "Message body for another commit\n\n"
+ "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b921")
.create());
- Change change2 = insert("repo", newChangeForCommit(repo, commit2));
+ Change change2 = insert(project, newChangeForCommit(repo, commit2));
RevCommit commit3 =
repo.parseBody(
repo.commit()
@@ -1262,7 +1309,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
+ "Last message body\n\n"
+ "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b921")
.create());
- Change change3 = insert("repo", newChangeForCommit(repo, commit3));
+ Change change3 = insert(project, newChangeForCommit(repo, commit3));
assertQuery("prefixsubject:\"[FOO\"", change3, change1);
assertQuery("prefixsubject:\"[BAR\"", change2);
@@ -1272,7 +1319,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
assertQuery("prefixsubject:FOO");
change1 =
newPatchSet(
- "repo",
+ project,
change1,
user,
Optional.of("[BAR123] Rework of commit with test subject\n\n" + "Message body\n\n"));
@@ -1282,11 +1329,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void fullTextWithNumbers() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
RevCommit commit1 = repo.parseBody(repo.commit().message("12345 67890").create());
- Change change1 = insert("repo", newChangeForCommit(repo, commit1));
+ Change change1 = insert(project, newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("12346 67891").create());
- Change change2 = insert("repo", newChangeForCommit(repo, commit2));
+ Change change2 = insert(project, newChangeForCommit(repo, commit2));
assertQuery("message:1234");
assertQuery("message:12345", change1);
@@ -1295,13 +1343,14 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void fullTextMultipleTerms() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
RevCommit commit1 = repo.parseBody(repo.commit().message("Signed-off: owner").create());
- Change change1 = insert("repo", newChangeForCommit(repo, commit1));
+ Change change1 = insert(project, newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("Signed by owner").create());
- Change change2 = insert("repo", newChangeForCommit(repo, commit2));
+ Change change2 = insert(project, newChangeForCommit(repo, commit2));
RevCommit commit3 = repo.parseBody(repo.commit().message("This change is off").create());
- Change change3 = insert("repo", newChangeForCommit(repo, commit3));
+ Change change3 = insert(project, newChangeForCommit(repo, commit3));
assertQuery("message:\"Signed-off: owner\"", change1);
assertQuery("message:\"Signed\"", change2, change1);
@@ -1310,11 +1359,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byMessageMixedCase() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
RevCommit commit1 = repo.parseBody(repo.commit().message("Hello gerrit").create());
- Change change1 = insert("repo", newChangeForCommit(repo, commit1));
+ Change change1 = insert(project, newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("Hello Gerrit").create());
- Change change2 = insert("repo", newChangeForCommit(repo, commit2));
+ Change change2 = insert(project, newChangeForCommit(repo, commit2));
assertQuery("message:gerrit", change2, change1);
assertQuery("message:Gerrit", change2, change1);
@@ -1322,16 +1372,18 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byMessageSubstring() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
RevCommit commit1 = repo.parseBody(repo.commit().message("https://gerrit.local").create());
- Change change1 = insert("repo", newChangeForCommit(repo, commit1));
+ Change change1 = insert(project, newChangeForCommit(repo, commit1));
assertQuery("message:gerrit", change1);
}
@Test
public void byLabel() throws Exception {
Account.Id anotherUser = createAccount("anotheruser");
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
ChangeInserter ins = newChange(repo);
ChangeInserter ins2 = newChange(repo);
ChangeInserter ins3 = newChange(repo);
@@ -1339,24 +1391,24 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
ChangeInserter ins5 = newChange(repo);
ChangeInserter ins6 = newChange(repo);
- Change reviewMinus2Change = insert("repo", ins);
+ Change reviewMinus2Change = insert(project, ins);
getChangeApi(reviewMinus2Change).current().review(ReviewInput.reject());
- Change reviewMinus1Change = insert("repo", ins2);
+ Change reviewMinus1Change = insert(project, ins2);
getChangeApi(reviewMinus1Change).current().review(ReviewInput.dislike());
- Change noLabelChange = insert("repo", ins3);
+ Change noLabelChange = insert(project, ins3);
- Change reviewPlus1Change = insert("repo", ins4);
+ Change reviewPlus1Change = insert(project, ins4);
getChangeApi(reviewPlus1Change).current().review(ReviewInput.recommend());
- Change reviewTwoPlus1Change = insert("repo", ins5);
+ Change reviewTwoPlus1Change = insert(project, ins5);
getChangeApi(reviewTwoPlus1Change).current().review(ReviewInput.recommend());
- requestContext.setContext(newRequestContext(createAccount("user1")));
+ setRequestContextForUser(createAccount("user1"));
getChangeApi(reviewTwoPlus1Change).current().review(ReviewInput.recommend());
- requestContext.setContext(newRequestContext(userId));
+ setRequestContextForUser(userId);
- Change reviewPlus2Change = insert("repo", ins6);
+ Change reviewPlus2Change = insert(project, ins6);
getChangeApi(reviewPlus2Change).current().review(ReviewInput.approve());
Map<String, Short> m =
@@ -1364,7 +1416,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
assertThat(m).hasSize(1);
assertThat(m).containsEntry("Code-Review", Short.valueOf((short) 1));
- Multimap<Integer, Change> changes =
+ ListMultimap<Integer, Change> changes =
Multimaps.newListMultimap(Maps.newLinkedHashMap(), () -> Lists.newArrayList());
changes.put(2, reviewPlus2Change);
changes.put(1, reviewTwoPlus1Change);
@@ -1502,7 +1554,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byLabelMulti() throws Exception {
Project.NameKey project = Project.nameKey("repo");
- repo = createAndOpenProject(project.get());
+ repo = createAndOpenProject(project);
LabelType verified =
label(LabelId.VERIFIED, value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
@@ -1529,25 +1581,25 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
ChangeInserter ins5 = newChange(repo);
// CR+1
- Change reviewCRplus1 = insert(project.get(), ins);
+ Change reviewCRplus1 = insert(project, ins);
getChangeApi(reviewCRplus1).current().review(ReviewInput.recommend());
// CR+2
- Change reviewCRplus2 = insert(project.get(), ins2);
+ Change reviewCRplus2 = insert(project, ins2);
getChangeApi(reviewCRplus2).current().review(ReviewInput.approve());
// CR+1 VR+1
- Change reviewCRplus1VRplus1 = insert(project.get(), ins3);
+ Change reviewCRplus1VRplus1 = insert(project, ins3);
getChangeApi(reviewCRplus1VRplus1).current().review(ReviewInput.recommend());
getChangeApi(reviewCRplus1VRplus1).current().review(reviewVerified);
// CR+2 VR+1
- Change reviewCRplus2VRplus1 = insert(project.get(), ins4);
+ Change reviewCRplus2VRplus1 = insert(project, ins4);
getChangeApi(reviewCRplus2VRplus1).current().review(ReviewInput.approve());
getChangeApi(reviewCRplus2VRplus1).current().review(reviewVerified);
// VR+1
- Change reviewVRplus1 = insert(project.get(), ins5);
+ Change reviewVRplus1 = insert(project, ins5);
getChangeApi(reviewVRplus1).current().review(reviewVerified);
assertQuery("label:Code-Review=+1", reviewCRplus1VRplus1, reviewCRplus1);
@@ -1566,14 +1618,15 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byLabelNotOwner() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
ChangeInserter ins = newChange(repo);
Account.Id user1 = createAccount("user1");
- Change reviewPlus1Change = insert("repo", ins);
+ Change reviewPlus1Change = insert(project, ins);
// post a review with user1
- requestContext.setContext(newRequestContext(user1));
+ setRequestContextForUser(user1);
getChangeApi(reviewPlus1Change).current().review(ReviewInput.recommend());
assertQuery("label:Code-Review=+1,user=" + user1, reviewPlus1Change);
@@ -1582,19 +1635,20 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byLabelNonUploader() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
ChangeInserter ins = newChange(repo);
Account.Id user1 = createAccount("user1");
// create a change with "user"
- Change reviewPlus1Change = insert("repo", ins);
+ Change reviewPlus1Change = insert(project, ins);
// add a +1 vote with "user". Query doesn't match since voter is the uploader.
getChangeApi(reviewPlus1Change).current().review(ReviewInput.recommend());
assertQuery("label:Code-Review=+1,user=non_uploader");
// add a +1 vote with "user1". Query will match since voter is a non-uploader.
- requestContext.setContext(newRequestContext(user1));
+ setRequestContextForUser(user1);
getChangeApi(reviewPlus1Change).current().review(ReviewInput.recommend());
assertQuery("label:Code-Review=+1,user=non_uploader", reviewPlus1Change);
assertQuery("label:Code-Review=+1,non_uploader", reviewPlus1Change);
@@ -1611,6 +1665,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
return range.toArray(new Change[0]);
}
+ @CanIgnoreReturnValue
private String createGroup(String name, String owner) throws Exception {
GroupInput in = new GroupInput();
in.name = name;
@@ -1627,7 +1682,8 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
public void byLabelGroup() throws Exception {
Account.Id user1 = createAccount("user1");
Account.Id user2 = createAccount("user2");
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
// create group and add users
String g1 = createGroup("group1", "Administrators");
@@ -1636,14 +1692,14 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
gApi.groups().id(g2).addMembers("user2");
// create a change
- Change change1 = insert("repo", newChange(repo), user1);
+ Change change1 = insert(project, newChange(repo), user1);
// post a review with user1
- requestContext.setContext(newRequestContext(user1));
+ setRequestContextForUser(user1);
getChangeApi(change1).current().review(new ReviewInput().label("Code-Review", 1));
// verify that query with user1 will return results.
- requestContext.setContext(newRequestContext(userId));
+ setRequestContextForUser(userId);
assertQuery("label:Code-Review=+1,group1", change1);
assertQuery("label:Code-Review=+1,group=group1", change1);
assertQuery("label:Code-Review=+1,user=" + user1, change1);
@@ -1655,7 +1711,8 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
public void byLabelExternalGroup() throws Exception {
Account.Id user1 = createAccount("user1");
Account.Id user2 = createAccount("user2");
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
// create group and add users
AccountGroup.UUID external_group1 = AccountGroup.uuid("testbackend:group1");
@@ -1680,13 +1737,13 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
.addSubgroup(uuidOfGroupThatContainsExternalGroupAsSubgroup)
.create();
- Change change1 = insert("repo", newChange(repo), user1);
- Change change2 = insert("repo", newChange(repo), user1);
+ Change change1 = insert(project, newChange(repo), user1);
+ Change change2 = insert(project, newChange(repo), user1);
// post a review with user1 and other_user
- requestContext.setContext(newRequestContext(user1));
+ setRequestContextForUser(user1);
getChangeApi(change1).current().review(new ReviewInput().label("Code-Review", 1));
- requestContext.setContext(newRequestContext(userId));
+ setRequestContextForUser(userId);
getChangeApi(change2).current().review(new ReviewInput().label("Code-Review", 1));
assertQuery("label:Code-Review=+1," + external_group1.get(), change1);
@@ -1714,11 +1771,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void limit() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
Change last = null;
int n = 5;
for (int i = 0; i < n; i++) {
- last = insert("repo", newChange(repo));
+ last = insert(project, newChange(repo));
}
for (int i = 1; i <= n + 2; i++) {
@@ -1743,10 +1801,11 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void start() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
List<Change> changes = new ArrayList<>();
for (int i = 0; i < 2; i++) {
- changes.add(insert("repo", newChange(repo)));
+ changes.add(insert(project, newChange(repo)));
}
assertQuery("status:new", changes.get(1), changes.get(0));
@@ -1763,10 +1822,11 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void startWithLimit() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
List<Change> changes = new ArrayList<>();
for (int i = 0; i < 3; i++) {
- changes.add(insert("repo", newChange(repo)));
+ changes.add(insert(project, newChange(repo)));
}
assertQuery("status:new limit:2", changes.get(2), changes.get(1));
@@ -1777,8 +1837,9 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void maxPages() throws Exception {
- repo = createAndOpenProject("repo");
- Change change = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change = insert(project, newChange(repo));
QueryRequest query = newQuery("status:new").withLimit(10);
assertQuery(query, change);
@@ -1793,12 +1854,13 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void updateOrder() throws Exception {
resetTimeWithClockStep(2, MINUTES);
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
List<ChangeInserter> inserters = new ArrayList<>();
List<Change> changes = new ArrayList<>();
for (int i = 0; i < 5; i++) {
inserters.add(newChange(repo));
- changes.add(insert("repo", inserters.get(i)));
+ changes.add(insert(project, inserters.get(i)));
}
for (int i : ImmutableList.of(2, 0, 1, 4, 3)) {
@@ -1817,10 +1879,11 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void updatedOrder() throws Exception {
resetTimeWithClockStep(1, SECONDS);
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
ChangeInserter ins1 = newChange(repo);
- Change change1 = insert("repo", ins1);
- Change change2 = insert("repo", newChange(repo));
+ Change change1 = insert(project, ins1);
+ Change change2 = insert(project, newChange(repo));
assertThat(lastUpdatedMs(change1)).isLessThan(lastUpdatedMs(change2));
assertQuery("status:new", change2, change1);
@@ -1838,12 +1901,13 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void filterOutMoreThanOnePageOfResults() throws Exception {
- repo = createAndOpenProject("repo");
- Change change = insert("repo", newChange(repo), userId);
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change = insert(project, newChange(repo), userId);
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
for (int i = 0; i < 5; i++) {
- insert("repo", newChange(repo), user2);
+ insert(project, newChange(repo), user2);
}
assertQuery("status:new ownerin:Administrators", change);
@@ -1852,11 +1916,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void filterOutAllResults() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
for (int i = 0; i < 5; i++) {
- insert("repo", newChange(repo), user2);
+ insert(project, newChange(repo), user2);
}
assertQuery("status:new ownerin:Administrators");
@@ -1865,8 +1930,9 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byFileExact() throws Exception {
- repo = createAndOpenProject("repo");
- Change change = insert("repo", newChangeWithFiles(repo, "dir/file1", "dir/file2"));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change = insert(project, newChangeWithFiles(repo, "dir/file1", "dir/file2"));
assertQuery("file:file");
assertQuery("file:dir", change);
@@ -1878,8 +1944,9 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byFileRegex() throws Exception {
- repo = createAndOpenProject("repo");
- Change change = insert("repo", newChangeWithFiles(repo, "dir/file1", "dir/file2"));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change = insert(project, newChangeWithFiles(repo, "dir/file1", "dir/file2"));
assertQuery("file:.*file.*");
assertQuery("file:^file.*"); // Whole path only.
@@ -1888,8 +1955,9 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byPathExact() throws Exception {
- repo = createAndOpenProject("repo");
- Change change = insert("repo", newChangeWithFiles(repo, "dir/file1", "dir/file2"));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change = insert(project, newChangeWithFiles(repo, "dir/file1", "dir/file2"));
assertQuery("path:file");
assertQuery("path:dir");
@@ -1901,8 +1969,9 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byPathRegex() throws Exception {
- repo = createAndOpenProject("repo");
- Change change = insert("repo", newChangeWithFiles(repo, "dir/file1", "dir/file2"));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change = insert(project, newChangeWithFiles(repo, "dir/file1", "dir/file2"));
assertQuery("path:.*file.*");
assertQuery("path:^dir.file.*", change);
@@ -1910,12 +1979,13 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byExtension() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChangeWithFiles(repo, "foo.h", "foo.cc"));
- Change change2 = insert("repo", newChangeWithFiles(repo, "bar.H", "bar.CC"));
- Change change3 = insert("repo", newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc"));
- Change change4 = insert("repo", newChangeWithFiles(repo, "Quux.java", "foo"));
- Change change5 = insert("repo", newChangeWithFiles(repo, "foo"));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChangeWithFiles(repo, "foo.h", "foo.cc"));
+ Change change2 = insert(project, newChangeWithFiles(repo, "bar.H", "bar.CC"));
+ Change change3 = insert(project, newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc"));
+ Change change4 = insert(project, newChangeWithFiles(repo, "Quux.java", "foo"));
+ Change change5 = insert(project, newChangeWithFiles(repo, "foo"));
assertQuery("extension:java", change4);
assertQuery("ext:java", change4);
@@ -1931,14 +2001,15 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byOnlyExtensions() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChangeWithFiles(repo, "foo.h", "foo.cc", "bar.cc"));
- Change change2 = insert("repo", newChangeWithFiles(repo, "bar.H", "bar.CC", "foo.H"));
- Change change3 = insert("repo", newChangeWithFiles(repo, "foo.CC", "bar.cc"));
- Change change4 = insert("repo", newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc"));
- Change change5 = insert("repo", newChangeWithFiles(repo, "Quux.java"));
- Change change6 = insert("repo", newChangeWithFiles(repo, "foo.txt", "foo"));
- Change change7 = insert("repo", newChangeWithFiles(repo, "foo"));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChangeWithFiles(repo, "foo.h", "foo.cc", "bar.cc"));
+ Change change2 = insert(project, newChangeWithFiles(repo, "bar.H", "bar.CC", "foo.H"));
+ Change change3 = insert(project, newChangeWithFiles(repo, "foo.CC", "bar.cc"));
+ Change change4 = insert(project, newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc"));
+ Change change5 = insert(project, newChangeWithFiles(repo, "Quux.java"));
+ Change change6 = insert(project, newChangeWithFiles(repo, "foo.txt", "foo"));
+ Change change7 = insert(project, newChangeWithFiles(repo, "foo"));
// case doesn't matter
assertQuery("onlyextensions:cc,h", change4, change2, change1);
@@ -1978,23 +2049,24 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byFooter() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
RevCommit commit1 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar").create());
- Change change1 = insert("repo", newChangeForCommit(repo, commit1));
+ Change change1 = insert(project, newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("Test\n\nfoo: baz").create());
- Change change2 = insert("repo", newChangeForCommit(repo, commit2));
+ Change change2 = insert(project, newChangeForCommit(repo, commit2));
RevCommit commit3 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar\nfoo:baz").create());
- Change change3 = insert("repo", newChangeForCommit(repo, commit3));
+ Change change3 = insert(project, newChangeForCommit(repo, commit3));
RevCommit commit4 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar=baz").create());
- Change change4 = insert("repo", newChangeForCommit(repo, commit4));
+ Change change4 = insert(project, newChangeForCommit(repo, commit4));
// create a changes with lines that look like footers, but which are not
RevCommit commit5 =
repo.parseBody(
repo.commit().message("Test\n\nfoo: bar\n\nfoo=bar").insertChangeId().create());
- Change change5 = insert("repo", newChangeForCommit(repo, commit5));
+ Change change5 = insert(project, newChangeForCommit(repo, commit5));
RevCommit commit6 = repo.parseBody(repo.commit().message("Test\n\na=b: c").create());
- insert("repo", newChangeForCommit(repo, commit6));
+ insert(project, newChangeForCommit(repo, commit6));
// matching by 'key=value' works
assertQuery("footer:foo=bar", change3, change1);
@@ -2028,15 +2100,16 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byFooterName() throws Exception {
assume().that(getSchema().hasField(ChangeField.FOOTER_NAME)).isTrue();
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
RevCommit commit1 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar").create());
- Change change1 = insert("repo", newChangeForCommit(repo, commit1));
+ Change change1 = insert(project, newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("Test\n\nBaR: baz").create());
- Change change2 = insert("repo", newChangeForCommit(repo, commit2));
+ Change change2 = insert(project, newChangeForCommit(repo, commit2));
// create a changes with lines that look like footers, but which are not
RevCommit commit6 = repo.parseBody(repo.commit().message("Test\n\na=b: c").create());
- insert("repo", newChangeForCommit(repo, commit6));
+ insert(project, newChangeForCommit(repo, commit6));
// matching by 'key=value' works
assertQuery("hasfooter:foo", change1);
@@ -2048,14 +2121,16 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byDirectory() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChangeWithFiles(repo, "src/foo.h", "src/foo.cc"));
- Change change2 = insert("repo", newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js"));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChangeWithFiles(repo, "src/foo.h", "src/foo.cc"));
+ Change change2 =
+ insert(project, newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js"));
Change change3 =
- insert("repo", newChangeWithFiles(repo, "documentation/training/slides/README.txt"));
- Change change4 = insert("repo", newChangeWithFiles(repo, "a.txt"));
- Change change5 = insert("repo", newChangeWithFiles(repo, "a/b/c/d/e/foo.txt"));
- Change change6 = insert("repo", newChangeWithFiles(repo, "all/caps/DIRECTORY/file.txt"));
+ insert(project, newChangeWithFiles(repo, "documentation/training/slides/README.txt"));
+ Change change4 = insert(project, newChangeWithFiles(repo, "a.txt"));
+ Change change5 = insert(project, newChangeWithFiles(repo, "a/b/c/d/e/foo.txt"));
+ Change change6 = insert(project, newChangeWithFiles(repo, "all/caps/DIRECTORY/file.txt"));
// matching by directory prefix works
assertQuery("directory:src", change2, change1);
@@ -2116,10 +2191,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byDirectoryRegex() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js"));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 =
+ insert(project, newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js"));
Change change2 =
- insert("repo", newChangeWithFiles(repo, "documentation/training/slides/README.txt"));
+ insert(project, newChangeWithFiles(repo, "documentation/training/slides/README.txt"));
// match by regexp
assertQuery("directory:^.*va.*", change1);
@@ -2129,9 +2206,10 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byComment() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
ChangeInserter ins = newChange(repo);
- Change change = insert("repo", ins);
+ Change change = insert(project, ins);
ReviewInput input = new ReviewInput();
input.message = "toplevel";
@@ -2157,11 +2235,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
public void byAge() throws Exception {
long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
long startMs = TestTimeUtil.START.toEpochMilli();
- Change change1 = insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs));
+ Change change1 = insert(project, newChange(repo), null, Instant.ofEpochMilli(startMs));
Change change2 =
- insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
+ insert(project, newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
// Stop time so age queries use the same endpoint.
TestTimeUtil.setClockStep(0, MILLISECONDS);
@@ -2198,11 +2277,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
public void byBeforeUntil() throws Exception {
long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
long startMs = TestTimeUtil.START.toEpochMilli();
- Change change1 = insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs));
+ Change change1 = insert(project, newChange(repo), null, Instant.ofEpochMilli(startMs));
Change change2 =
- insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
+ insert(project, newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
TestTimeUtil.setClockStep(0, MILLISECONDS);
// Change1 was last updated on 2009-09-30 21:00:00 -0000
@@ -2250,11 +2330,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
public void byAfterSince() throws Exception {
long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
long startMs = TestTimeUtil.START.toEpochMilli();
- Change change1 = insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs));
+ Change change1 = insert(project, newChange(repo), null, Instant.ofEpochMilli(startMs));
Change change2 =
- insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
+ insert(project, newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
TestTimeUtil.setClockStep(0, MILLISECONDS);
// Change1 was last updated on 2009-09-30 21:00:00 -0000
@@ -2294,12 +2375,13 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
// Stop the clock, will set time to specific test values.
resetTimeWithClockStep(0, MILLISECONDS);
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
long startMs = TestTimeUtil.START.toEpochMilli();
TestTimeUtil.setClock(new Timestamp(startMs));
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChange(repo));
- Change change3 = insert("repo", newChange(repo));
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChange(repo));
+ Change change3 = insert(project, newChange(repo));
TestTimeUtil.setClock(new Timestamp(startMs + thirtyHoursInMs));
submit(change3);
@@ -2354,12 +2436,13 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
// Stop the clock, will set time to specific test values.
resetTimeWithClockStep(0, MILLISECONDS);
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
long startMs = TestTimeUtil.START.toEpochMilli();
TestTimeUtil.setClock(new Timestamp(startMs));
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChange(repo));
- Change change3 = insert("repo", newChange(repo));
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChange(repo));
+ Change change3 = insert(project, newChange(repo));
assertThat(TimeUtil.nowMs()).isEqualTo(startMs);
TestTimeUtil.setClock(new Timestamp(startMs + thirtyHoursInMs));
@@ -2424,13 +2507,14 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
// Stop the clock, will set time to specific test values.
resetTimeWithClockStep(0, MILLISECONDS);
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
long startMs = TestTimeUtil.START.toEpochMilli();
TestTimeUtil.setClock(new Timestamp(startMs));
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChange(repo));
- Change change3 = insert("repo", newChange(repo));
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChange(repo));
+ Change change3 = insert(project, newChange(repo));
TestTimeUtil.setClock(new Timestamp(startMs + thirtyHoursInMs));
submit(change2);
@@ -2457,15 +2541,16 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void bySize() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
// added = 3, deleted = 0, delta = 3
RevCommit commit1 = repo.parseBody(repo.commit().add("file1", "foo\n\foo\nfoo").create());
// added = 0, deleted = 2, delta = 2
RevCommit commit2 = repo.parseBody(repo.commit().parent(commit1).add("file1", "foo").create());
- Change change1 = insert("repo", newChangeForCommit(repo, commit1));
- Change change2 = insert("repo", newChangeForCommit(repo, commit2));
+ Change change1 = insert(project, newChangeForCommit(repo, commit1));
+ Change change2 = insert(project, newChangeForCommit(repo, commit2));
assertQuery("added:>4");
assertQuery("-added:<=4");
@@ -2512,10 +2597,11 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
}
}
- private List<Change> setUpHashtagChanges() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChange(repo));
+ private ImmutableList<Change> setUpHashtagChanges() throws Exception {
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChange(repo));
addHashtags(change1, "foo", "aaa-bbb-ccc");
addHashtags(change2, "foo", "bar", "a tag", "ACamelCaseTag");
@@ -2530,7 +2616,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byHashtag() throws Exception {
- List<Change> changes = setUpHashtagChanges();
+ ImmutableList<Change> changes = setUpHashtagChanges();
assertQuery("hashtag:foo", changes.get(1), changes.get(0));
assertQuery("hashtag:bar", changes.get(1));
assertQuery("hashtag:\"a tag\"", changes.get(1));
@@ -2545,7 +2631,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byHashtagFullText() throws Exception {
assume().that(getSchema().hasField(ChangeField.FUZZY_HASHTAG)).isTrue();
- List<Change> changes = setUpHashtagChanges();
+ ImmutableList<Change> changes = setUpHashtagChanges();
assertQuery("inhashtag:foo", changes.get(1), changes.get(0));
assertQuery("inhashtag:bbb", changes.get(0));
assertQuery("inhashtag:tag", changes.get(1));
@@ -2554,7 +2640,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byHashtagPrefix() throws Exception {
assume().that(getSchema().hasField(ChangeField.PREFIX_HASHTAG)).isTrue();
- List<Change> changes = setUpHashtagChanges();
+ ImmutableList<Change> changes = setUpHashtagChanges();
assertQuery("prefixhashtag:a", changes.get(1), changes.get(0));
assertQuery("prefixhashtag:aa", changes.get(0));
assertQuery("prefixhashtag:bar", changes.get(1));
@@ -2562,10 +2648,11 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byHashtagRegex() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChange(repo));
- Change change3 = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChange(repo));
+ Change change3 = insert(project, newChange(repo));
addHashtags(change1, "feature1");
addHashtags(change1, "trending");
addHashtags(change2, "Cherrypick-feature1");
@@ -2578,27 +2665,28 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byDefault() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
- Change change1 = insert("repo", newChange(repo));
+ Change change1 = insert(project, newChange(repo));
RevCommit commit2 = repo.parseBody(repo.commit().message("foosubject").create());
- Change change2 = insert("repo", newChangeForCommit(repo, commit2));
+ Change change2 = insert(project, newChangeForCommit(repo, commit2));
RevCommit commit3 = repo.parseBody(repo.commit().add("Foo.java", "foo contents").create());
- Change change3 = insert("repo", newChangeForCommit(repo, commit3));
+ Change change3 = insert(project, newChangeForCommit(repo, commit3));
ChangeInserter ins4 = newChange(repo);
- Change change4 = insert("repo", ins4);
+ Change change4 = insert(project, ins4);
ReviewInput ri4 = new ReviewInput();
ri4.message = "toplevel";
ri4.labels = ImmutableMap.of("Code-Review", (short) 1);
getChangeApi(change4).current().review(ri4);
ChangeInserter ins5 = newChangeWithTopic(repo, "feature5");
- Change change5 = insert("repo", ins5);
+ Change change5 = insert(project, ins5);
- Change change6 = insert("repo", newChangeForBranch(repo, "branch6"));
+ Change change6 = insert(project, newChangeForBranch(repo, "branch6"));
assertQuery(change1.getId().get(), change1);
assertQuery(ChangeTriplet.format(change1), change1);
@@ -2619,23 +2707,27 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byDefaultWithCommitPrefix() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
RevCommit commit = repo.parseBody(repo.commit().message("message").create());
- Change change = insert("repo", newChangeForCommit(repo, commit));
+ Change change = insert(project, newChangeForCommit(repo, commit));
assertQuery(commit.getId().getName().substring(0, 6), change);
}
@Test
public void visible() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChangePrivate(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChangePrivate(repo));
String q = "project:repo";
// Bad request for query with non-existent user
- assertThatQueryException(q + " visibleto:notexisting");
+ assertThatQueryException(q + " visibleto:notexisting")
+ .hasMessageThat()
+ .isEqualTo("No user or group matches \"notexisting\".");
// Current user can see all changes
assertQuery(q, change2, change1);
@@ -2667,7 +2759,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
assertQuery(q, change2, change1);
}
- requestContext.setContext(newRequestContext(user2));
+ setRequestContextForUser(user2);
assertQuery("is:visible", change1);
Account.Id user3 = createAccount("user3");
@@ -2683,9 +2775,9 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
accountManager.authenticate(authRequest);
// Switch to user3
- requestContext.setContext(newRequestContext(user3));
- Change change3 = insert("repo", newChange(repo), user3);
- Change change4 = insert("repo", newChangePrivate(repo), user3);
+ setRequestContextForUser(user3);
+ Change change3 = insert(project, newChange(repo), user3);
+ Change change4 = insert(project, newChangePrivate(repo), user3);
// User3 can see both their changes and the first user's change
assertQuery(q + " visibleto:" + user3.get(), change4, change3, change1);
@@ -2725,9 +2817,10 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void visibleToSelf() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChange(repo));
getChangeApi(change2).setPrivate(true, "private");
@@ -2736,17 +2829,19 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
assertQuery(q + " visibleto:me", change2, change1);
// Anonymous user cannot see first user's private change.
- requestContext.setContext(anonymousUserProvider::get);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(anonymousUserProvider::get);
+
assertQuery(q + " visibleto:self", change1);
assertQuery(q + " visibleto:me", change1);
}
@Test
public void byCommentBy() throws Exception {
-
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChange(repo));
Account.Id user2 = createAccount("anotheruser");
ReviewInput input = new ReviewInput();
@@ -2769,8 +2864,9 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
public void bySubmitRuleResult() throws Exception {
try (Registration registration =
extensionRegistry.newRegistration().add(new FakeSubmitRule())) {
- repo = createAndOpenProject("repo");
- Change change = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change = insert(project, newChange(repo));
// The fake submit rule exports its ruleName as "FakeSubmitRule"
assertQuery("rule:FakeSubmitRule");
@@ -2789,17 +2885,19 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
public void byNonExistingSubmitRule_returnsEmpty() throws Exception {
try (Registration registration =
extensionRegistry.newRegistration().add(new FakeSubmitRule())) {
- repo = createAndOpenProject("repo");
- insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ insert(project, newChange(repo));
assertQuery("rule:non-existent-rule");
}
}
@Test
public void byHasDraft() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChange(repo));
assertQuery("has:draft");
@@ -2820,7 +2918,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
assertQuery("has:draft", change2, change1);
- requestContext.setContext(newRequestContext(user2));
+ setRequestContextForUser(user2);
assertQuery("has:draft");
}
@@ -2831,8 +2929,8 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
*/
public void byHasDraftExcludesZombieDrafts() throws Exception {
Project.NameKey project = Project.nameKey("repo");
- repo = createAndOpenProject(project.get());
- Change change = insert("repo", newChange(repo));
+ repo = createAndOpenProject(project);
+ Change change = insert(project, newChange(repo));
Change.Id id = change.getId();
DraftInput in = new DraftInput();
@@ -2868,15 +2966,16 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byHasDraftWithManyDrafts() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
Change[] changesWithDrafts = new Change[30];
// unrelated change not shown in the result.
- insert("repo", newChange(repo));
+ insert(project, newChange(repo));
for (int i = 0; i < changesWithDrafts.length; i++) {
// put the changes in reverse order since this is the order we receive them from the index.
- changesWithDrafts[changesWithDrafts.length - 1 - i] = insert("repo", newChange(repo));
+ changesWithDrafts[changesWithDrafts.length - 1 - i] = insert(project, newChange(repo));
DraftInput in = new DraftInput();
in.line = 1;
in.message = "nit: trailing whitespace";
@@ -2887,16 +2986,17 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- requestContext.setContext(newRequestContext(user2));
+ setRequestContextForUser(user2);
assertQuery("has:draft");
}
@Test
public void byStarredBy() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChange(repo));
- insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChange(repo));
+ insert(project, newChange(repo));
gApi.accounts().self().starChange(change1.getId().toString());
gApi.accounts().self().starChange(change2.getId().toString());
@@ -2906,19 +3006,20 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
assertQuery("has:star", change2, change1);
- requestContext.setContext(newRequestContext(user2));
+ setRequestContextForUser(user2);
assertQuery("has:star");
}
@Test
public void byStar_withStarOptionSet() throws Exception {
// When star option is set, the 'starred' field is set in the change infos in response.
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChangeWithStatus(repo, Change.Status.MERGED));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChangeWithStatus(repo, Change.Status.MERGED));
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- requestContext.setContext(newRequestContext(user2));
+ setRequestContextForUser(user2);
gApi.accounts().self().starChange(change1.getId().toString());
@@ -2935,12 +3036,13 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byStar_withStarOptionNotSet() throws Exception {
// When star option is not set, the 'starred' field is not set in the change infos in response.
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChangeWithStatus(repo, Change.Status.MERGED));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChangeWithStatus(repo, Change.Status.MERGED));
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- requestContext.setContext(newRequestContext(user2));
+ setRequestContextForUser(user2);
gApi.accounts().self().starChange(change1.getId().toString());
@@ -2956,15 +3058,19 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byStar_withStarOptionSet_notPopulatedForAnonymousUsers() throws Exception {
// Create a random change and star it as some user
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChangeWithStatus(repo, Change.Status.NEW));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChangeWithStatus(repo, Change.Status.NEW));
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- requestContext.setContext(newRequestContext(user2));
+ setRequestContextForUser(user2);
+
gApi.accounts().self().starChange(change1.getId().toString());
// Request a change query for all open changes. The star field is not set on the single change.
- requestContext.setContext(anonymousUserProvider::get);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(anonymousUserProvider::get);
+
List<ChangeInfo> changeInfos =
gApi.changes().query("is:open").withOptions(ListChangesOption.STAR).get();
assertThat(changeInfos.get(0)._number).isEqualTo(change1.getId().get());
@@ -2973,11 +3079,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byStarWithManyStars() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
Change[] changesWithDrafts = new Change[30];
for (int i = 0; i < changesWithDrafts.length; i++) {
// put the changes in reverse order since this is the order we receive them from the index.
- changesWithDrafts[changesWithDrafts.length - 1 - i] = insert("repo", newChange(repo));
+ changesWithDrafts[changesWithDrafts.length - 1 - i] = insert(project, newChange(repo));
// star the change
gApi.accounts()
@@ -2991,12 +3098,13 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byFrom() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- Change change2 = insert("repo", newChange(repo), user2);
+ Change change2 = insert(project, newChange(repo), user2);
ReviewInput input = new ReviewInput();
input.message = "toplevel";
@@ -3012,7 +3120,8 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void conflicts() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
RevCommit commit1 =
repo.parseBody(
repo.commit()
@@ -3024,10 +3133,10 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
RevCommit commit3 =
repo.parseBody(repo.commit().add("dir/file2", "contents2 different").create());
RevCommit commit4 = repo.parseBody(repo.commit().add("file4", "contents4").create());
- Change change1 = insert("repo", newChangeForCommit(repo, commit1));
- Change change2 = insert("repo", newChangeForCommit(repo, commit2));
- Change change3 = insert("repo", newChangeForCommit(repo, commit3));
- Change change4 = insert("repo", newChangeForCommit(repo, commit4));
+ Change change1 = insert(project, newChangeForCommit(repo, commit1));
+ Change change2 = insert(project, newChangeForCommit(repo, commit2));
+ Change change3 = insert(project, newChangeForCommit(repo, commit3));
+ Change change4 = insert(project, newChangeForCommit(repo, commit4));
assertQuery("conflicts:" + change1.getId().get(), change3);
assertQuery("conflicts:" + change2.getId().get());
@@ -3041,11 +3150,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
value = "API_REF_UPDATED_AND_CHANGE_REINDEX")
public void mergeable() throws Exception {
assume().that(getSchema().hasField(ChangeField.MERGEABLE_SPEC)).isTrue();
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
RevCommit commit1 = repo.parseBody(repo.commit().add("file1", "contents1").create());
RevCommit commit2 = repo.parseBody(repo.commit().add("file1", "contents2").create());
- Change change1 = insert("repo", newChangeForCommit(repo, commit1));
- Change change2 = insert("repo", newChangeForCommit(repo, commit2));
+ Change change1 = insert(project, newChangeForCommit(repo, commit1));
+ Change change2 = insert(project, newChangeForCommit(repo, commit2));
assertQuery("conflicts:" + change1.getId().get(), change2);
assertQuery("conflicts:" + change2.getId().get(), change1);
@@ -3069,9 +3179,10 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void cherrypick() throws Exception {
assume().that(getSchema().hasField(ChangeField.CHERRY_PICK_SPEC)).isTrue();
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newCherryPickChange(repo, "foo", change1.currentPatchSetId()));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newCherryPickChange(repo, "foo", change1.currentPatchSetId()));
assertQuery("is:cherrypick", change2);
assertQuery("-is:cherrypick", change1);
@@ -3080,14 +3191,15 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void merge() throws Exception {
assume().that(getSchema().hasField(ChangeField.MERGE_SPEC)).isTrue();
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
RevCommit commit1 = repo.parseBody(repo.commit().add("file1", "contents1").create());
RevCommit commit2 = repo.parseBody(repo.commit().add("file1", "contents2").create());
RevCommit commit3 =
repo.parseBody(repo.commit().parent(commit2).add("file1", "contents3").create());
- Change change1 = insert("repo", newChangeForCommit(repo, commit1));
- Change change2 = insert("repo", newChangeForCommit(repo, commit2));
- Change change3 = insert("repo", newChangeForCommit(repo, commit3));
+ Change change1 = insert(project, newChangeForCommit(repo, commit1));
+ Change change2 = insert(project, newChangeForCommit(repo, commit2));
+ Change change3 = insert(project, newChangeForCommit(repo, commit3));
RevCommit mergeCommit =
repo.branch("master")
.commit()
@@ -3096,7 +3208,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
.parent(commit3)
.insertChangeId()
.create();
- Change mergeChange = insert("repo", newChangeForCommit(repo, mergeCommit));
+ Change mergeChange = insert(project, newChangeForCommit(repo, mergeCommit));
assertQuery("status:open is:merge", mergeChange);
assertQuery("status:open -is:merge", change3, change2, change1);
@@ -3106,21 +3218,22 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void reviewedBy() throws Exception {
resetTimeWithClockStep(2, MINUTES);
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChange(repo));
- Change change3 = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChange(repo));
+ Change change3 = insert(project, newChange(repo));
getChangeApi(change1).current().review(new ReviewInput().message("comment"));
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- requestContext.setContext(newRequestContext(user2));
+ setRequestContextForUser(user2);
getChangeApi(change2).current().review(new ReviewInput().message("comment"));
PatchSet.Id ps3_1 = change3.currentPatchSetId();
- change3 = newPatchSet("repo", change3, user, /* message= */ Optional.empty());
+ change3 = newPatchSet(project, change3, user, /* message= */ Optional.empty());
assertThat(change3.currentPatchSetId()).isNotEqualTo(ps3_1);
// Response to previous patch set still counts as reviewing.
getChangeApi(change3).revision(ps3_1.get()).review(new ReviewInput().message("comment"));
@@ -3144,11 +3257,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void reviewerAndCc() throws Exception {
Account.Id user1 = createAccount("user1");
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChange(repo));
- Change change3 = insert("repo", newChange(repo));
- insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChange(repo));
+ Change change3 = insert(project, newChange(repo));
+ insert(project, newChange(repo));
ReviewerInput rin = new ReviewerInput();
rin.reviewer = user1.toString();
@@ -3166,7 +3280,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
assertQuery("is:reviewer", change3);
assertQuery("reviewer:self", change3);
- requestContext.setContext(newRequestContext(user1));
+ setRequestContextForUser(user1);
assertQuery("reviewer:" + user1, change1);
assertQuery("cc:" + user1, change2);
assertQuery("is:cc", change2);
@@ -3175,18 +3289,19 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byReviewed() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
Account.Id otherUser =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChange(repo));
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChange(repo));
assertQuery("is:reviewed");
assertQuery("status:reviewed");
assertQuery("-is:reviewed", change2, change1);
assertQuery("-status:reviewed", change2, change1);
- requestContext.setContext(newRequestContext(otherUser));
+ setRequestContextForUser(otherUser);
getChangeApi(change1).current().review(ReviewInput.recommend());
assertQuery("is:reviewed", change1);
@@ -3203,11 +3318,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
accountManager.authenticate(authRequestFactory.createForUser("user2")).getAccountId();
Account.Id user3 =
accountManager.authenticate(authRequestFactory.createForUser("user3")).getAccountId();
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChange(repo));
- Change change3 = insert("repo", newChange(repo));
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChange(repo));
+ Change change3 = insert(project, newChange(repo));
ReviewerInput rin = new ReviewerInput();
rin.reviewer = user1.toString();
@@ -3247,7 +3363,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void reviewerAndCcByEmail() throws Exception {
Project.NameKey project = Project.nameKey("repo");
- repo = createAndOpenProject(project.get());
+ repo = createAndOpenProject(project);
ConfigInput conf = new ConfigInput();
conf.enableReviewerByEmail = InheritableBoolean.TRUE;
gApi.projects().name(project.get()).config(conf);
@@ -3255,9 +3371,9 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
String userByEmail = "un.registered@reviewer.com";
String userByEmailWithName = "John Doe <" + userByEmail + ">";
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChange(repo));
- insert("repo", newChange(repo));
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChange(repo));
+ insert(project, newChange(repo));
ReviewerInput rin = new ReviewerInput();
rin.reviewer = userByEmailWithName;
@@ -3280,16 +3396,16 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void reviewerAndCcByEmailWithQueryForDifferentUser() throws Exception {
Project.NameKey project = Project.nameKey("repo");
- repo = createAndOpenProject(project.get());
+ repo = createAndOpenProject(project);
ConfigInput conf = new ConfigInput();
conf.enableReviewerByEmail = InheritableBoolean.TRUE;
gApi.projects().name(project.get()).config(conf);
String userByEmail = "John Doe <un.registered@reviewer.com>";
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChange(repo));
- insert("repo", newChange(repo));
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChange(repo));
+ insert(project, newChange(repo));
ReviewerInput rin = new ReviewerInput();
rin.reviewer = userByEmail;
@@ -3308,14 +3424,16 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void submitRecords() throws Exception {
Account.Id user1 = createAccount("user1");
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChange(repo));
getChangeApi(change1).current().review(ReviewInput.approve());
- requestContext.setContext(newRequestContext(user1));
+ setRequestContextForUser(user1);
+
getChangeApi(change2).current().review(ReviewInput.recommend());
- requestContext.setContext(newRequestContext(user.getAccountId()));
+ setRequestContextForUser(user.getAccountId());
assertQuery("is:submittable", change1);
assertQuery("-is:submittable", change2);
@@ -3340,34 +3458,36 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
public void hasEdit() throws Exception {
Account.Id user1 = createAccount("user1");
Account.Id user2 = createAccount("user2");
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
String changeId1 = change1.getKey().get();
- Change change2 = insert("repo", newChange(repo));
+ Change change2 = insert(project, newChange(repo));
String changeId2 = change2.getKey().get();
- requestContext.setContext(newRequestContext(user1));
+ setRequestContextForUser(user1);
assertQuery("has:edit");
gApi.changes().id(changeId1).edit().create();
gApi.changes().id(changeId2).edit().create();
- requestContext.setContext(newRequestContext(user2));
+ setRequestContextForUser(user2);
assertQuery("has:edit");
gApi.changes().id(changeId2).edit().create();
- requestContext.setContext(newRequestContext(user1));
+ setRequestContextForUser(user1);
assertQuery("has:edit", change2, change1);
- requestContext.setContext(newRequestContext(user2));
+ setRequestContextForUser(user2);
assertQuery("has:edit", change2);
}
@Test
public void byUnresolved() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChange(repo));
- Change change3 = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChange(repo));
+ Change change3 = insert(project, newChange(repo));
// Change1 has one resolved comment (unresolvedcount = 0)
// Change2 has one unresolved comment (unresolvedcount = 1)
@@ -3395,13 +3515,15 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byCommitsOnBranchNotMerged() throws Exception {
- createProject("repo");
- testByCommitsOnBranchNotMerged("repo", ImmutableSet.of());
+ Project.NameKey project = Project.nameKey("repo");
+ createProject(project);
+ testByCommitsOnBranchNotMerged(project, ImmutableSet.of());
}
@Test
public void byCommitsOnBranchNotMergedSkipsMissingChanges() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
ObjectId missing =
repo.branch(PatchSet.id(Change.id(987654), 1).toRefName())
.commit()
@@ -3409,10 +3531,10 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
.insertChangeId()
.create()
.copy();
- testByCommitsOnBranchNotMerged("repo", ImmutableSet.of(missing));
+ testByCommitsOnBranchNotMerged(project, ImmutableSet.of(missing));
}
- private void testByCommitsOnBranchNotMerged(String repo, Collection<ObjectId> extra)
+ private void testByCommitsOnBranchNotMerged(Project.NameKey project, Collection<ObjectId> extra)
throws Exception {
int n = 10;
List<String> shas = new ArrayList<>(n + extra.size());
@@ -3420,10 +3542,10 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
List<Integer> expectedIds = new ArrayList<>(n);
BranchNameKey dest = null;
try (TestRepository<Repository> repository =
- new TestRepository<>(repoManager.openRepository(Project.nameKey(repo)))) {
+ new TestRepository<>(repoManager.openRepository(project))) {
for (int i = 0; i < n; i++) {
ChangeInserter ins = newChange(repository);
- insert("repo", ins);
+ insert(project, ins);
if (dest == null) {
dest = ins.getChange().getDest();
}
@@ -3431,7 +3553,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
expectedIds.add(ins.getChange().getId().get());
}
}
- try (Repository repository = repoManager.openRepository(Project.nameKey(repo))) {
+ try (Repository repository = repoManager.openRepository(project)) {
for (int i = 1; i <= 11; i++) {
Iterable<ChangeData> cds =
queryProvider.get().byCommitsOnBranchNotMerged(repository, dest, shas, i);
@@ -3446,16 +3568,15 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void reindexIfStale() throws Exception {
Project.NameKey project = Project.nameKey("repo");
- repo = createAndOpenProject(project.get());
- Change change = insert("repo", newChange(repo));
- String changeId = change.getKey().get();
+ repo = createAndOpenProject(project);
+ Change change = insert(project, newChange(repo));
Account.Id anotherUser = createAccount("another-user");
- requestContext.setContext(newRequestContext(anotherUser));
- gApi.changes().id(changeId).addReviewer(anotherUser.toString());
+ setRequestContextForUser(anotherUser);
+ getChangeApi(change).addReviewer(anotherUser.toString());
assertQuery("reviewer:self", change);
- assertThat(indexer.reindexIfStale(project, change.getId()).get()).isFalse();
+ assertThat(indexer.reindexIfStale(project, change.getId())).isFalse();
// Remove reviewer behind index's back.
ChangeUpdate update = newUpdate(change);
@@ -3464,7 +3585,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
// Index is stale.
assertQuery("reviewer:self", change);
- assertThat(indexer.reindexIfStale(project, change.getId()).get()).isTrue();
+ assertThat(indexer.reindexIfStale(project, change.getId())).isTrue();
assertQuery("reviewer:self");
// Index is not stale when a draft comment exists
@@ -3472,20 +3593,22 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
in.line = 1;
in.message = "nit: trailing whitespace";
in.path = Patch.COMMIT_MSG;
- gApi.changes().id(project.get(), change.getId().get()).current().createDraft(in);
- assertThat(indexer.reindexIfStale(project, change.getId()).get()).isFalse();
+ getChangeApi(change).current().createDraft(in);
+ assertThat(indexer.reindexIfStale(project, change.getId())).isFalse();
}
@Test
public void watched() throws Exception {
- createProject("repo");
- ChangeInserter ins1 = newChangeWithStatus("repo", Change.Status.NEW);
- Change change1 = insert("repo", ins1);
+ Project.NameKey project = Project.nameKey("repo");
+ createProject(project);
+ ChangeInserter ins1 = newChangeWithStatus(project, Change.Status.NEW);
+ Change change1 = insert(project, ins1);
- createProject("repo2");
+ Project.NameKey project2 = Project.nameKey("repo2");
+ createProject(project2);
- ChangeInserter ins2 = newChangeWithStatus("repo2", Change.Status.NEW);
- insert("repo2", ins2);
+ ChangeInserter ins2 = newChangeWithStatus(project2, Change.Status.NEW);
+ insert(project2, ins2);
assertQuery("is:watched");
@@ -3505,17 +3628,18 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void trackingid() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
RevCommit commit1 =
repo.parseBody(repo.commit().message("Change one\n\nBug:QUERY123").create());
- Change change1 = insert("repo", newChangeForCommit(repo, commit1));
+ Change change1 = insert(project, newChangeForCommit(repo, commit1));
RevCommit commit2 =
repo.parseBody(repo.commit().message("Change two\n\nIssue: Issue 16038\n").create());
- Change change2 = insert("repo", newChangeForCommit(repo, commit2));
+ Change change2 = insert(project, newChangeForCommit(repo, commit2));
RevCommit commit3 =
repo.parseBody(repo.commit().message("Change two\n\nGoogle-Bug-Id: b/16039\n").create());
- Change change3 = insert("repo", newChangeForCommit(repo, commit3));
+ Change change3 = insert(project, newChangeForCommit(repo, commit3));
assertQuery("tr:QUERY123", change1);
assertQuery("bug:QUERY123", change1);
@@ -3541,9 +3665,10 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void revertOf() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
// Create two commits and revert second commit (initial commit can't be reverted)
- Change initial = insert("repo", newChange(repo));
+ Change initial = insert(project, newChange(repo));
getChangeApi(initial).current().review(ReviewInput.approve());
getChangeApi(initial).current().submit();
@@ -3558,10 +3683,11 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void submissionId() throws Exception {
- repo = createAndOpenProject("repo");
- Change change = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change = insert(project, newChange(repo));
// create irrelevant change
- insert("repo", newChange(repo));
+ insert(project, newChange(repo));
getChangeApi(change).current().review(ReviewInput.approve());
getChangeApi(change).current().submit();
String submissionId = getChangeApi(change).get().submissionId;
@@ -3625,9 +3751,11 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
return this;
}
- DashboardChangeState create(TestRepository<Repository> repo) throws Exception {
- requestContext.setContext(newRequestContext(ownerId));
- Change change = insert("repo", newChange(repo), ownerId);
+ @CanIgnoreReturnValue
+ DashboardChangeState create(Project.NameKey project, TestRepository<Repository> repo)
+ throws Exception {
+ setRequestContextForUser(ownerId);
+ Change change = insert(project, newChange(repo), ownerId);
id = change.getId();
ChangeApi cApi = getChangeApi(change);
if (wip) {
@@ -3649,24 +3777,25 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
in.path = Patch.COMMIT_MSG;
in.message = "message";
for (Account.Id commenterId : draftCommentBy) {
- requestContext.setContext(newRequestContext(commenterId));
+ setRequestContextForUser(commenterId);
getChangeApi(change).current().createDraft(in);
}
for (Account.Id commenterId : deleteDraftCommentBy) {
- requestContext.setContext(newRequestContext(commenterId));
+ setRequestContextForUser(commenterId);
getChangeApi(change).current().createDraft(in).delete();
}
if (mergedBy != null) {
- requestContext.setContext(newRequestContext(mergedBy));
+ setRequestContextForUser(mergedBy);
cApi = getChangeApi(change);
cApi.current().review(ReviewInput.approve());
cApi.current().submit();
}
- requestContext.setContext(newRequestContext(user.getAccountId()));
+ setRequestContextForUser(user.getAccountId());
return this;
}
}
+ @CanIgnoreReturnValue
protected List<ChangeInfo> assertDashboardQuery(
String viewedUser, String query, DashboardChangeState... expected) throws Exception {
Change.Id[] ids = new Change.Id[expected.length];
@@ -3676,6 +3805,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
return assertQueryByIds(query.replaceAll("\\$\\{user}", viewedUser), ids);
}
+ @CanIgnoreReturnValue
protected List<ChangeInfo> assertDashboardQueryWithStart(
String viewedUser, String query, int start, DashboardChangeState... expected)
throws Exception {
@@ -3683,24 +3813,29 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
for (int i = 0; i < expected.length; i++) {
ids[i] = expected[i].id;
}
- QueryRequest queryRequest = newQuery(query.replaceAll("\\$\\{user}", viewedUser));
- queryRequest.withStart(start);
+ QueryRequest queryRequest =
+ newQuery(query.replaceAll("\\$\\{user}", viewedUser)).withStart(start);
return assertQueryByIds(queryRequest, ids);
}
@Test
public void dashboardHasUnpublishedDrafts() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
Account.Id otherAccountId = createAccount("other");
DashboardChangeState hasUnpublishedDraft =
- new DashboardChangeState(otherAccountId).draftCommentBy(user.getAccountId()).create(repo);
+ new DashboardChangeState(otherAccountId)
+ .draftCommentBy(user.getAccountId())
+ .create(project, repo);
// Create changes that should not be returned by query.
- new DashboardChangeState(user.getAccountId()).create(repo);
- new DashboardChangeState(user.getAccountId()).draftCommentBy(otherAccountId).create(repo);
+ new DashboardChangeState(user.getAccountId()).create(project, repo);
+ new DashboardChangeState(user.getAccountId())
+ .draftCommentBy(otherAccountId)
+ .create(project, repo);
new DashboardChangeState(user.getAccountId())
.draftAndDeleteCommentBy(user.getAccountId())
- .create(repo);
+ .create(project, repo);
assertDashboardQuery(
"self", IndexPreloadingUtil.DASHBOARD_HAS_UNPUBLISHED_DRAFTS_QUERY, hasUnpublishedDraft);
@@ -3708,14 +3843,17 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void dashboardWorkInProgressReviews() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
DashboardChangeState ownedOpenWip =
- new DashboardChangeState(user.getAccountId()).wip().create(repo);
+ new DashboardChangeState(user.getAccountId()).wip().create(project, repo);
// Create changes that should not be returned by query.
- new DashboardChangeState(user.getAccountId()).wip().abandon().create(repo);
- new DashboardChangeState(user.getAccountId()).mergeBy(user.getAccountId()).create(repo);
- new DashboardChangeState(createAccount("other")).wip().create(repo);
+ new DashboardChangeState(user.getAccountId()).wip().abandon().create(project, repo);
+ new DashboardChangeState(user.getAccountId())
+ .mergeBy(user.getAccountId())
+ .create(project, repo);
+ new DashboardChangeState(createAccount("other")).wip().create(project, repo);
assertDashboardQuery(
"self", IndexPreloadingUtil.DASHBOARD_WORK_IN_PROGRESS_QUERY, ownedOpenWip);
@@ -3723,80 +3861,90 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void dashboardOutgoingReviews() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
Account.Id otherAccountId = createAccount("other");
DashboardChangeState ownedOpenReviewable =
- new DashboardChangeState(user.getAccountId()).create(repo);
+ new DashboardChangeState(user.getAccountId()).create(project, repo);
// Create changes that should not be returned by any queries in this test.
- new DashboardChangeState(user.getAccountId()).wip().create(repo);
- new DashboardChangeState(otherAccountId).create(repo);
+ new DashboardChangeState(user.getAccountId()).wip().create(project, repo);
+ new DashboardChangeState(otherAccountId).create(project, repo);
// Viewing one's own dashboard.
assertDashboardQuery("self", IndexPreloadingUtil.DASHBOARD_OUTGOING_QUERY, ownedOpenReviewable);
// Viewing another user's dashboard.
- requestContext.setContext(newRequestContext(otherAccountId));
+ setRequestContextForUser(otherAccountId);
assertDashboardQuery(
userId.toString(), IndexPreloadingUtil.DASHBOARD_OUTGOING_QUERY, ownedOpenReviewable);
}
@Test
public void dashboardIncomingReviews() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
Account.Id otherAccountId = createAccount("other");
DashboardChangeState reviewingReviewable =
- new DashboardChangeState(otherAccountId).addReviewer(user.getAccountId()).create(repo);
+ new DashboardChangeState(otherAccountId)
+ .addReviewer(user.getAccountId())
+ .create(project, repo);
// Create changes that should not be returned by any queries in this test.
- new DashboardChangeState(otherAccountId).wip().addReviewer(user.getAccountId()).create(repo);
- new DashboardChangeState(otherAccountId).addReviewer(otherAccountId).create(repo);
+ new DashboardChangeState(otherAccountId)
+ .wip()
+ .addReviewer(user.getAccountId())
+ .create(project, repo);
+ new DashboardChangeState(otherAccountId).addReviewer(otherAccountId).create(project, repo);
new DashboardChangeState(otherAccountId)
.addReviewer(user.getAccountId())
.mergeBy(user.getAccountId())
- .create(repo);
+ .create(project, repo);
// Viewing one's own dashboard.
assertDashboardQuery("self", IndexPreloadingUtil.DASHBOARD_INCOMING_QUERY, reviewingReviewable);
// Viewing another user's dashboard.
- requestContext.setContext(newRequestContext(otherAccountId));
+ setRequestContextForUser(otherAccountId);
assertDashboardQuery(
userId.toString(), IndexPreloadingUtil.DASHBOARD_INCOMING_QUERY, reviewingReviewable);
}
@Test
public void dashboardRecentlyClosedReviews() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
Account.Id otherAccountId = createAccount("other");
DashboardChangeState mergedOwned =
- new DashboardChangeState(user.getAccountId()).mergeBy(user.getAccountId()).create(repo);
+ new DashboardChangeState(user.getAccountId())
+ .mergeBy(user.getAccountId())
+ .create(project, repo);
DashboardChangeState mergedReviewing =
new DashboardChangeState(otherAccountId)
.addReviewer(user.getAccountId())
.mergeBy(user.getAccountId())
- .create(repo);
+ .create(project, repo);
DashboardChangeState mergedCced =
new DashboardChangeState(otherAccountId)
.addCc(user.getAccountId())
.mergeBy(user.getAccountId())
- .create(repo);
+ .create(project, repo);
DashboardChangeState abandonedOwned =
- new DashboardChangeState(user.getAccountId()).abandon().create(repo);
+ new DashboardChangeState(user.getAccountId()).abandon().create(project, repo);
DashboardChangeState abandonedOwnedWip =
- new DashboardChangeState(user.getAccountId()).wip().abandon().create(repo);
+ new DashboardChangeState(user.getAccountId()).wip().abandon().create(project, repo);
DashboardChangeState abandonedReviewing =
new DashboardChangeState(otherAccountId)
.addReviewer(user.getAccountId())
.abandon()
- .create(repo);
+ .create(project, repo);
// Create changes that should not be returned by any queries in this test.
new DashboardChangeState(otherAccountId)
.addReviewer(user.getAccountId())
.wip()
.abandon()
- .create(repo);
+ .create(project, repo);
// Viewing one's own dashboard.
assertDashboardQuery(
@@ -3810,7 +3958,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
mergedOwned);
// Viewing another user's dashboard.
- requestContext.setContext(newRequestContext(otherAccountId));
+ setRequestContextForUser(otherAccountId);
assertDashboardQuery(
userId.toString(),
IndexPreloadingUtil.DASHBOARD_RECENTLY_CLOSED_QUERY,
@@ -3825,9 +3973,10 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
public void attentionSetIndexed() throws Exception {
assume().that(getSchema().hasField(ChangeField.ATTENTION_SET_USERS)).isTrue();
assume().that(getSchema().hasField(ChangeField.ATTENTION_SET_USERS_COUNT)).isTrue();
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChange(repo));
AttentionSetInput input = new AttentionSetInput(userId.toString(), "some reason");
getChangeApi(change1).addToAttentionSet(input);
@@ -3848,8 +3997,9 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void attentionSetStored() throws Exception {
assume().that(getSchema().hasField(ChangeField.ATTENTION_SET_USERS)).isTrue();
- repo = createAndOpenProject("repo");
- Change change = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change = insert(project, newChange(repo));
AttentionSetInput input = new AttentionSetInput(userId.toString(), "reason 1");
getChangeApi(change).addToAttentionSet(input);
@@ -3878,10 +4028,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@GerritConfig(name = "accounts.visibility", value = "NONE")
@Test
public void namedDestination() throws Exception {
- createProject("repo1");
- Change change1 = insert("repo1", newChange("repo1"));
- createProject("repo2");
- Change change2 = insert("repo2", newChange("repo2"));
+ Project.NameKey project1 = Project.nameKey("repo1");
+ createProject(project1);
+ Change change1 = insert(project1, newChange(project1));
+ Project.NameKey project2 = Project.nameKey("repo2");
+ createProject(project2);
+ Change change2 = insert(project2, newChange(project2));
assertThatQueryException("destination:foo")
.hasMessageThat()
@@ -3959,14 +4111,14 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
.hasMessageThat()
.isEqualTo("Account 'non-existent' not found");
- requestContext.setContext(newRequestContext(anotherUserId));
+ setRequestContextForUser(anotherUserId);
// account userId is not visible to 'anotheruser' as they are not an admin
assertThatQueryException("destination:destination3,user=" + userId)
.hasMessageThat()
.isEqualTo(String.format("Account '%s' not found", userId));
// Group destinations
- requestContext.setContext(newRequestContext(userId));
+ setRequestContextForUser(userId);
assertThatQueryException("destination:non-existent-dest,group=" + group)
.hasMessageThat()
.isEqualTo("Unknown named destination: non-existent-dest");
@@ -3987,9 +4139,10 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@GerritConfig(name = "accounts.visibility", value = "NONE")
@Test
public void namedQuery() throws Exception {
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo));
- Change change2 = insert("repo", newChangeForBranch(repo, "stable"));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
+ Change change2 = insert(project, newChangeForBranch(repo, "stable"));
String group = "test-group";
AccountGroup.UUID groupId = groupOperations.newGroup().name(group).create();
@@ -4034,12 +4187,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
.hasMessageThat()
.isEqualTo("Account 'non-existent' not found");
- requestContext.setContext(newRequestContext(anotherUserId));
+ setRequestContextForUser(anotherUserId);
// account 1000000 is not visible to 'anotheruser' as they are not an admin
assertThatQueryException("query:query1,user=" + userId)
.hasMessageThat()
.isEqualTo(String.format("Account '%s' not found", userId));
- requestContext.setContext(newRequestContext(userId));
+ setRequestContextForUser(userId);
assertQuery("query:query1", change2, change1);
assertQuery("query:query2", change2, change1);
@@ -4074,8 +4227,9 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byDeletedChange() throws Exception {
- repo = createAndOpenProject("repo");
- Change change = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change = insert(project, newChange(repo));
String query = "change:" + change.getId();
assertQuery(query, change);
@@ -4086,8 +4240,9 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void byUrlEncodedProject() throws Exception {
- repo = createAndOpenProject("repo+foo");
- Change change = insert("repo+foo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo+foo");
+ repo = createAndOpenProject(project);
+ Change change = insert(project, newChange(repo));
assertQuery("project:repo+foo", change);
}
@@ -4120,14 +4275,15 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@Test
public void isPureRevert() throws Exception {
assume().that(getSchema().hasField(ChangeField.IS_PURE_REVERT_SPEC)).isTrue();
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
// Create two commits and revert second commit (initial commit can't be reverted)
- Change initial = insert("repo", newChange(repo));
+ Change initial = insert(project, newChange(repo));
getChangeApi(initial).current().review(ReviewInput.approve());
getChangeApi(initial).current().submit();
ChangeInfo changeToRevert =
- gApi.changes().create(new ChangeInput("repo", "master", "commit to revert")).get();
+ gApi.changes().create(new ChangeInput(project.get(), "master", "commit to revert")).get();
gApi.changes().id(changeToRevert.id).current().review(ReviewInput.approve());
gApi.changes().id(changeToRevert.id).current().submit();
@@ -4151,12 +4307,14 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
RequestContext oldContext = requestContext.setContext(anonymousUserProvider::get);
try {
- requestContext.setContext(anonymousUserProvider::get);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(anonymousUserProvider::get);
assertThatAuthException(query)
.hasMessageThat()
.isEqualTo("Must be signed-in to use this operator");
} finally {
- requestContext.setContext(oldContext);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(oldContext);
}
}
}
@@ -4166,32 +4324,35 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- repo = createAndOpenProject("repo");
- Change change = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change = insert(project, newChange(repo));
getChangeApi(change).addReviewer(user2.toString());
RequestContext adminContext = requestContext.setContext(newRequestContext(user2));
assertQuery("reviewer:self", change);
- requestContext.setContext(adminContext);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(adminContext);
gApi.accounts().id(user2.get()).setActive(false);
- requestContext.setContext(newRequestContext(user2));
+ setRequestContextForUser(user2);
assertQuery("reviewer:self", change);
}
@Test
public void none() throws Exception {
- repo = createAndOpenProject("repo");
- Change change = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change = insert(project, newChange(repo));
assertQuery(ChangeIndexPredicate.none());
for (Predicate<ChangeData> matchingOneChange :
ImmutableList.of(
// One index query, one post-filtering query.
- queryBuilder.parse(change.getId().toString()),
- queryBuilder.parse("ownerin:Administrators"))) {
+ queryBuilderProvider.get().parse(change.getId().toString()),
+ queryBuilderProvider.get().parse("ownerin:Administrators"))) {
assertQuery(matchingOneChange, change);
assertQuery(Predicate.or(ChangeIndexPredicate.none(), matchingOneChange), change);
assertQuery(Predicate.and(ChangeIndexPredicate.none(), matchingOneChange));
@@ -4204,9 +4365,10 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
@GerritConfig(name = "change.mergeabilityComputationBehavior", value = "NEVER")
public void mergeableFailsWhenNotIndexed() throws Exception {
assume().that(getSchema().hasField(ChangeField.MERGE_SPEC)).isTrue();
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
RevCommit commit1 = repo.parseBody(repo.commit().add("file1", "contents1").create());
- insert("repo", newChangeForCommit(repo, commit1));
+ insert(project, newChangeForCommit(repo, commit1));
Throwable thrown = assertThrows(Throwable.class, () -> assertQuery("status:open is:mergeable"));
assertThat(thrown.getCause()).isInstanceOf(QueryParseException.class);
@@ -4219,20 +4381,21 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
public void customKeyedValue() throws Exception {
assume().that(getSchema().hasField(ChangeField.CUSTOM_KEYED_VALUES_SPEC)).isTrue();
- repo = createAndOpenProject("repo");
- Change change1 = insert("repo", newChange(repo));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change change1 = insert(project, newChange(repo));
CustomKeyedValuesInput in = new CustomKeyedValuesInput();
in.add = ImmutableMap.of("workspace", "my-ws");
getChangeApi(change1).setCustomKeyedValues(in);
- Change change2 = insert("repo", newChange(repo));
+ Change change2 = insert(project, newChange(repo));
in = new CustomKeyedValuesInput();
in.add = ImmutableMap.of("workspace", "123");
getChangeApi(change2).setCustomKeyedValues(in);
// Insert a change without a KV pair
- insert("repo", newChange(repo));
+ insert(project, newChange(repo));
assertThat(customKeyedValues("workspace="))
.containsExactly(change1.getChangeId(), change2.getChangeId());
@@ -4271,9 +4434,9 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
return newChange(repo, null, null, status, null, null, false, false);
}
- protected ChangeInserter newChangeWithStatus(String repoName, Change.Status status)
+ protected ChangeInserter newChangeWithStatus(Project.NameKey project, Change.Status status)
throws Exception {
- return newChange(repoName, null, null, status, null, null, false, false);
+ return newChange(project, null, null, status, null, null, false, false);
}
protected ChangeInserter newChangeWithTopic(TestRepository<Repository> repo, String topic)
@@ -4295,12 +4458,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
return newChange(repo, null, branch, null, null, cherryPickOf, false, true);
}
- protected ChangeInserter newChange(String repoName) throws Exception {
- return newChange(repoName, null, null, null, null, null, false, false);
+ protected ChangeInserter newChange(Project.NameKey project) throws Exception {
+ return newChange(project, null, null, null, null, null, false, false);
}
protected ChangeInserter newChange(
- String repoName,
+ Project.NameKey project,
@Nullable RevCommit commit,
@Nullable String branch,
@Nullable Change.Status status,
@@ -4310,7 +4473,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
boolean isPrivate)
throws Exception {
try (TestRepository<Repository> repo =
- new TestRepository<>(repoManager.openRepository(Project.nameKey(repoName)))) {
+ new TestRepository<>(repoManager.openRepository(project))) {
return newChange(
repo, commit, branch, status, topic, cherryPickOf, workInProgress, isPrivate);
}
@@ -4351,21 +4514,20 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
}
@CanIgnoreReturnValue
- protected Change insert(String repoName, ChangeInserter ins, @Nullable Account.Id owner)
+ protected Change insert(Project.NameKey project, ChangeInserter ins, @Nullable Account.Id owner)
throws Exception {
- return insert(repoName, ins, owner, TimeUtil.now());
+ return insert(project, ins, owner, TimeUtil.now());
}
@CanIgnoreReturnValue
- protected Change insert(String repoName, ChangeInserter ins) throws Exception {
- return insert(repoName, ins, null, TimeUtil.now());
+ protected Change insert(Project.NameKey project, ChangeInserter ins) throws Exception {
+ return insert(project, ins, null, TimeUtil.now());
}
@CanIgnoreReturnValue
protected Change insert(
- String repoName, ChangeInserter ins, @Nullable Account.Id owner, Instant createdOn)
+ Project.NameKey project, ChangeInserter ins, @Nullable Account.Id owner, Instant createdOn)
throws Exception {
- Project.NameKey project = Project.nameKey(repoName);
Account.Id ownerId = owner != null ? owner : userId;
IdentifiedUser user = userFactory.create(ownerId);
return testRefAction(
@@ -4379,9 +4541,10 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
}
protected Change newPatchSet(
- String repoName, Change c, CurrentUser user, Optional<String> message) throws Exception {
+ Project.NameKey project, Change c, CurrentUser user, Optional<String> message)
+ throws Exception {
try (TestRepository<Repository> repo =
- new TestRepository<>(repoManager.openRepository(Project.nameKey(repoName)))) {
+ new TestRepository<>(repoManager.openRepository(project))) {
// Add a new file so the patch set is not a trivial rebase, to avoid default
// Code-Review label copying.
int n = c.currentPatchSetId().get() + 1;
@@ -4420,7 +4583,8 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
protected ThrowableSubject assertThatQueryException(QueryRequest query) throws Exception {
try {
- query.get();
+ @SuppressWarnings("unused")
+ var unused = query.get();
throw new AssertionError("expected BadRequestException for query: " + query);
} catch (BadRequestException e) {
return assertThat(e);
@@ -4429,7 +4593,8 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
protected ThrowableSubject assertThatAuthException(Object query) throws Exception {
try {
- newQuery(query).get();
+ @SuppressWarnings("unused")
+ var unused = newQuery(query).get();
throw new AssertionError("expected AuthException for query: " + query);
} catch (AuthException e) {
return assertThat(e);
@@ -4437,103 +4602,105 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
}
@CanIgnoreReturnValue
- protected TestRepository<Repository> createAndOpenProject(String name) throws Exception {
- createProject(name);
- return new TestRepository<>(repoManager.openRepository(Project.nameKey(name)));
+ protected TestRepository<Repository> createAndOpenProject(Project.NameKey project)
+ throws Exception {
+ createProject(project);
+ return new TestRepository<>(repoManager.openRepository(project));
}
- protected TestRepository<Repository> createAndOpenProject(String name, String parent)
- throws Exception {
- createProject(name, parent);
- return new TestRepository<>(repoManager.openRepository(Project.nameKey(name)));
+ protected TestRepository<Repository> createAndOpenProject(
+ Project.NameKey project, Project.NameKey parent) throws Exception {
+ createProject(project, parent);
+ return new TestRepository<>(repoManager.openRepository(project));
}
- protected void createProject(String name) throws Exception {
- gApi.projects().create(name).get();
+ protected void createProject(Project.NameKey project) throws Exception {
+ gApi.projects().create(project.get());
}
- protected void createProject(String name, String parent) throws Exception {
+ protected void createProject(Project.NameKey project, Project.NameKey parent) throws Exception {
ProjectInput input = new ProjectInput();
- input.name = name;
- input.parent = parent;
- gApi.projects().create(input).get();
+ input.name = project.get();
+ input.parent = parent.get();
+ gApi.projects().create(input);
}
protected QueryRequest newQuery(Object query) {
return gApi.changes().query(query.toString());
}
+ @CanIgnoreReturnValue
protected List<ChangeInfo> assertQuery(Object query, Change... changes) throws Exception {
return assertQuery(newQuery(query), changes);
}
+ @CanIgnoreReturnValue
protected List<ChangeInfo> assertQueryByIds(Object query, Change.Id... changes) throws Exception {
return assertQueryByIds(newQuery(query), changes);
}
+ @CanIgnoreReturnValue
protected List<ChangeInfo> assertQuery(QueryRequest query, Change... changes) throws Exception {
return assertQueryByIds(
query, Arrays.stream(changes).map(Change::getId).toArray(Change.Id[]::new));
}
- protected List<ChangeInfo> assertQueryByIds(QueryRequest query, Change.Id... changes)
+ @CanIgnoreReturnValue
+ protected List<ChangeInfo> assertQueryByIds(QueryRequest query, Change.Id... expectedChangeIds)
throws Exception {
List<ChangeInfo> result = query.get();
- Iterable<Change.Id> ids = ids(result);
- assertWithMessage(format(query.getQuery(), ids, changes))
- .that(ids)
- .containsExactlyElementsIn(Arrays.asList(changes))
- .inOrder();
+ List<Change.Id> actualIds = ids(result);
+ assertThat(actualIds).containsExactlyElementsIn(Arrays.asList(expectedChangeIds)).inOrder();
return result;
}
protected void assertQuery(Predicate<ChangeData> predicate, Change... changes) throws Exception {
- ImmutableList<Change.Id> actualIds =
+ ImmutableList<Change> actualChanges =
queryProvider.get().query(predicate).stream()
- .map(ChangeData::getId)
+ .map(ChangeData::change)
.collect(toImmutableList());
+ ImmutableList<Change.Id> actualIds =
+ actualChanges.stream().map(Change::getId).collect(toImmutableList());
Change.Id[] expectedIds = Arrays.stream(changes).map(Change::getId).toArray(Change.Id[]::new);
- assertWithMessage(format(predicate.toString(), actualIds, expectedIds))
+ assertWithMessage(format(predicate.toString(), actualChanges, changes))
.that(actualIds)
.containsExactlyElementsIn(expectedIds)
.inOrder();
}
- private String format(String query, Iterable<Change.Id> actualIds, Change.Id... expectedChanges)
- throws RestApiException {
+ private String format(String query, Iterable<Change> actualChanges, Change... expectedChanges) {
return "query '"
+ query
+ "' with expected changes "
+ format(Arrays.asList(expectedChanges))
+ " and result "
- + format(actualIds);
+ + format(actualChanges);
}
- private String format(Iterable<Change.Id> changeIds) throws RestApiException {
- return format(changeIds.iterator());
+ private String format(Iterable<Change> changes) {
+ return format(changes.iterator());
}
- private String format(Iterator<Change.Id> changeIds) throws RestApiException {
+ private String format(Iterator<Change> changes) {
StringBuilder b = new StringBuilder();
b.append("[");
- while (changeIds.hasNext()) {
- Change.Id id = changeIds.next();
- ChangeInfo c = gApi.changes().id(id.get()).get();
+ while (changes.hasNext()) {
+ Change c = changes.next();
b.append("{")
- .append(id)
+ .append(c.getChangeId())
.append(" (")
- .append(c.changeId)
+ .append(c.getKey().get())
.append("), ")
.append("dest=")
- .append(BranchNameKey.create(Project.nameKey(c.project), c.branch))
+ .append(c.getDest())
.append(", ")
.append("status=")
- .append(c.status)
+ .append(c.getStatus().name())
.append(", ")
.append("lastUpdated=")
- .append(c.updated.getTime())
+ .append(c.getLastUpdatedOn().toEpochMilli())
.append("}");
- if (changeIds.hasNext()) {
+ if (changes.hasNext()) {
b.append(", ");
}
}
@@ -4541,11 +4708,11 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
return b.toString();
}
- protected static Iterable<Change.Id> ids(Change... changes) {
+ protected static List<Change.Id> ids(Change... changes) {
return Arrays.stream(changes).map(Change::getId).collect(toList());
}
- protected static Iterable<Change.Id> ids(Iterable<ChangeInfo> changes) {
+ protected static List<Change.Id> ids(Iterable<ChangeInfo> changes) {
return Streams.stream(changes).map(c -> Change.id(c._number)).collect(toList());
}
@@ -4619,6 +4786,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
getChangeApi(change).current().review(input);
}
+ @CanIgnoreReturnValue
private Account.Id createAccount(String username, String fullName, String email, boolean active)
throws Exception {
try (ManualRequestContext ctx = oneOffRequestContext.open()) {
@@ -4639,6 +4807,11 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
}
}
+ private void setRequestContextForUser(Account.Id userId) {
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(newRequestContext(userId));
+ }
+
protected void assertFailingQuery(String query) throws Exception {
assertFailingQuery(query, null);
}
diff --git a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
index 5d54baf498..d00cc451a7 100644
--- a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
@@ -24,6 +24,7 @@ import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
@@ -67,17 +68,19 @@ public abstract class FakeQueryChangesTest extends AbstractQueryChangesTest {
@UseClockStep
public void stopQueryIfNoMoreResults() throws Exception {
// create 2 visible changes
- try (TestRepository<Repository> testRepo = createAndOpenProject("repo")) {
- insert("repo", newChange(testRepo));
- insert("repo", newChange(testRepo));
+ Project.NameKey project = Project.nameKey("repo");
+ try (TestRepository<Repository> testRepo = createAndOpenProject(project)) {
+ insert(project, newChange(testRepo));
+ insert(project, newChange(testRepo));
}
// create 2 invisible changes
- try (TestRepository<Repository> hiddenProject = createAndOpenProject("hiddenProject")) {
- insert("hiddenProject", newChange(hiddenProject));
- insert("hiddenProject", newChange(hiddenProject));
+ Project.NameKey hiddenProject = Project.nameKey("hiddenProject");
+ try (TestRepository<Repository> hiddenRepo = createAndOpenProject(hiddenProject)) {
+ insert(hiddenProject, newChange(hiddenRepo));
+ insert(hiddenProject, newChange(hiddenRepo));
projectOperations
- .project(Project.nameKey("hiddenProject"))
+ .project(hiddenProject)
.forUpdate()
.add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
.update();
@@ -85,7 +88,10 @@ public abstract class FakeQueryChangesTest extends AbstractQueryChangesTest {
AbstractFakeIndex<?, ?, ?> idx =
(AbstractFakeIndex<?, ?, ?>) changeIndexCollection.getSearchIndex();
- newQuery("status:new").withLimit(5).get();
+
+ @SuppressWarnings("unused")
+ var unused = newQuery("status:new").withLimit(5).get();
+
// Since the limit of the query (i.e. 5) is more than the total number of changes (i.e. 4),
// only 1 index search is expected.
assertThatSearchQueryWasNotPaginated(idx.getQueryCount());
@@ -94,18 +100,19 @@ public abstract class FakeQueryChangesTest extends AbstractQueryChangesTest {
@Test
@UseClockStep
public void queryRightNumberOfTimes() throws Exception {
- TestRepository<Repository> repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ TestRepository<Repository> repo = createAndOpenProject(project);
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
// create 1 visible change
- Change visibleChange1 = insert("repo", newChangeWithStatus(repo, Change.Status.NEW));
+ Change visibleChange1 = insert(project, newChangeWithStatus(repo, Change.Status.NEW));
// create 4 private changes
- Change invisibleChange2 = insert("repo", newChangeWithStatus(repo, Change.Status.NEW), user2);
- Change invisibleChange3 = insert("repo", newChangeWithStatus(repo, Change.Status.NEW), user2);
- Change invisibleChange4 = insert("repo", newChangeWithStatus(repo, Change.Status.NEW), user2);
- Change invisibleChange5 = insert("repo", newChangeWithStatus(repo, Change.Status.NEW), user2);
+ Change invisibleChange2 = insert(project, newChangeWithStatus(repo, Change.Status.NEW), user2);
+ Change invisibleChange3 = insert(project, newChangeWithStatus(repo, Change.Status.NEW), user2);
+ Change invisibleChange4 = insert(project, newChangeWithStatus(repo, Change.Status.NEW), user2);
+ Change invisibleChange5 = insert(project, newChangeWithStatus(repo, Change.Status.NEW), user2);
gApi.changes().id(invisibleChange2.getKey().get()).setPrivate(true, null);
gApi.changes().id(invisibleChange3.getKey().get()).setPrivate(true, null);
gApi.changes().id(invisibleChange4.getKey().get()).setPrivate(true, null);
@@ -131,11 +138,12 @@ public abstract class FakeQueryChangesTest extends AbstractQueryChangesTest {
public void noLimitQueryPaginates() throws Exception {
assumeFalse(PaginationType.NONE == getCurrentPaginationType());
- try (TestRepository<Repository> testRepo = createAndOpenProject("repo")) {
- insert("repo", newChange(testRepo));
- insert("repo", newChange(testRepo));
- insert("repo", newChange(testRepo));
- insert("repo", newChange(testRepo));
+ Project.NameKey project = Project.nameKey("repo");
+ try (TestRepository<Repository> testRepo = createAndOpenProject(project)) {
+ insert(project, newChange(testRepo));
+ insert(project, newChange(testRepo));
+ insert(project, newChange(testRepo));
+ insert(project, newChange(testRepo));
}
// Set queryLimit to 2
projectOperations
@@ -150,7 +158,9 @@ public abstract class FakeQueryChangesTest extends AbstractQueryChangesTest {
// 2 index searches are expected. The first index search will run with size 3 (i.e.
// the configured query-limit+1), and then we will paginate to get the remaining
// changes with the second index search.
- newQuery("status:new").withNoLimit().get();
+ @SuppressWarnings("unused")
+ var unused = newQuery("status:new").withNoLimit().get();
+
assertThatSearchQueryWasPaginated(idx.getQueryCount(), 2);
}
@@ -158,8 +168,12 @@ public abstract class FakeQueryChangesTest extends AbstractQueryChangesTest {
@UseClockStep
public void noLimitQueryDoesNotPaginatesWithNonePaginationType() throws Exception {
assumeTrue(PaginationType.NONE == getCurrentPaginationType());
+
AbstractFakeIndex<?, ?, ?> idx = setupRepoWithFourChanges();
- newQuery("status:new").withNoLimit().get();
+
+ @SuppressWarnings("unused")
+ var unused = newQuery("status:new").withNoLimit().get();
+
assertThatSearchQueryWasNotPaginated(idx.getQueryCount());
}
@@ -182,7 +196,9 @@ public abstract class FakeQueryChangesTest extends AbstractQueryChangesTest {
.add(allowCapability(QUERY_LIMIT).group(ANONYMOUS_USERS).range(0, LIMIT))
.update();
- requestContext.setContext(anonymousUserProvider::get);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(anonymousUserProvider::get);
+
List<ChangeInfo> result = newQuery("status:new").withLimit(LIMIT).get();
assertThat(result.size()).isEqualTo(0);
assertThatSearchQueryWasPaginated(idx.getQueryCount(), 2);
@@ -196,11 +212,12 @@ public abstract class FakeQueryChangesTest extends AbstractQueryChangesTest {
assumeFalse(PaginationType.NONE == getCurrentPaginationType());
final int LIMIT = 2;
- try (TestRepository<Repository> testRepo = createAndOpenProject("repo")) {
- insert("repo", newChange(testRepo));
- insert("repo", newChange(testRepo));
- insert("repo", newChange(testRepo));
- insert("repo", newChange(testRepo));
+ Project.NameKey project = Project.nameKey("repo");
+ try (TestRepository<Repository> testRepo = createAndOpenProject(project)) {
+ insert(project, newChange(testRepo));
+ insert(project, newChange(testRepo));
+ insert(project, newChange(testRepo));
+ insert(project, newChange(testRepo));
}
// Set queryLimit to 2
projectOperations
@@ -230,7 +247,8 @@ public abstract class FakeQueryChangesTest extends AbstractQueryChangesTest {
@SuppressWarnings("unused")
private void executeQuery(String query) throws QueryParseException {
- List<ChangeData> unused = queryProvider.get().query(queryBuilder.parse(query));
+ ImmutableList<ChangeData> unused =
+ queryProvider.get().query(queryBuilderProvider.get().parse(query));
}
private void assertThatSearchQueryWasNotPaginated(int queryCount) {
@@ -242,11 +260,12 @@ public abstract class FakeQueryChangesTest extends AbstractQueryChangesTest {
}
private AbstractFakeIndex<?, ?, ?> setupRepoWithFourChanges() throws Exception {
- try (TestRepository<Repository> testRepo = createAndOpenProject("repo")) {
- insert("repo", newChange(testRepo));
- insert("repo", newChange(testRepo));
- insert("repo", newChange(testRepo));
- insert("repo", newChange(testRepo));
+ Project.NameKey project = Project.nameKey("repo");
+ try (TestRepository<Repository> testRepo = createAndOpenProject(project)) {
+ insert(project, newChange(testRepo));
+ insert(project, newChange(testRepo));
+ insert(project, newChange(testRepo));
+ insert(project, newChange(testRepo));
}
// Set queryLimit to 2
diff --git a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
index e7600d94fe..82c90653a1 100644
--- a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
@@ -22,6 +22,7 @@ import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.testing.InMemoryModule;
@@ -44,11 +45,12 @@ public abstract class LuceneQueryChangesTest extends AbstractQueryChangesTest {
@Test
public void fullTextWithSpecialChars() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
RevCommit commit1 = repo.parseBody(repo.commit().message("foo_bar_foo").create());
- Change change1 = insert("repo", newChangeForCommit(repo, commit1));
+ Change change1 = insert(project, newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("one.two.three").create());
- Change change2 = insert("repo", newChangeForCommit(repo, commit2));
+ Change change2 = insert(project, newChangeForCommit(repo, commit2));
assertQuery("message:foo_ba");
assertQuery("message:bar", change1);
@@ -60,6 +62,23 @@ public abstract class LuceneQueryChangesTest extends AbstractQueryChangesTest {
}
@Test
+ public void byChangeId() throws Exception {
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ RevCommit commit1 = repo.parseBody(repo.commit().message("foo_bar_foo").create());
+ Change change1 = insert(project, newChangeForCommit(repo, commit1));
+
+ assertQuery(String.format("change:%s", change1.getChangeId()), change1);
+ assertQuery(String.format("change:%s", change1.getId()), change1);
+ assertQuery(
+ String.format("change:%s~%s", change1.getProject(), change1.getChangeId()), change1);
+ assertQuery(
+ String.format(
+ "change:%s~%s~%s", change1.getProject(), change1.getDest().branch(), change1.getKey()),
+ change1);
+ }
+
+ @Test
public void invalidQuery() throws Exception {
BadRequestException thrown =
assertThrows(BadRequestException.class, () -> newQuery("\\").get());
@@ -68,17 +87,18 @@ public abstract class LuceneQueryChangesTest extends AbstractQueryChangesTest {
@Test
public void openAndClosedChanges() throws Exception {
- repo = createAndOpenProject("repo");
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
// create 3 closed changes
- Change change1 = insert("repo", newChangeWithStatus(repo, Change.Status.MERGED));
- Change change2 = insert("repo", newChangeWithStatus(repo, Change.Status.MERGED));
- Change change3 = insert("repo", newChangeWithStatus(repo, Change.Status.MERGED));
+ Change change1 = insert(project, newChangeWithStatus(repo, Change.Status.MERGED));
+ Change change2 = insert(project, newChangeWithStatus(repo, Change.Status.MERGED));
+ Change change3 = insert(project, newChangeWithStatus(repo, Change.Status.MERGED));
// create 3 new changes
- Change change4 = insert("repo", newChangeWithStatus(repo, Change.Status.NEW));
- Change change5 = insert("repo", newChangeWithStatus(repo, Change.Status.NEW));
- Change change6 = insert("repo", newChangeWithStatus(repo, Change.Status.NEW));
+ Change change4 = insert(project, newChangeWithStatus(repo, Change.Status.NEW));
+ Change change5 = insert(project, newChangeWithStatus(repo, Change.Status.NEW));
+ Change change6 = insert(project, newChangeWithStatus(repo, Change.Status.NEW));
// Set queryLimit to 1
projectOperations
@@ -94,8 +114,9 @@ public abstract class LuceneQueryChangesTest extends AbstractQueryChangesTest {
@Test
public void skipChangesNotVisible() throws Exception {
// create 1 new change on a repo
- repo = createAndOpenProject("repo");
- Change visibleChange = insert("repo", newChangeWithStatus(repo, Change.Status.NEW));
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project);
+ Change visibleChange = insert(project, newChangeWithStatus(repo, Change.Status.NEW));
Change[] expected = new Change[] {visibleChange};
// pagination does not need to restart the datasource, the request is fulfilled
@@ -105,8 +126,8 @@ public abstract class LuceneQueryChangesTest extends AbstractQueryChangesTest {
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- Change invisibleChange1 = insert("repo", newChangeWithStatus(repo, Change.Status.NEW), user2);
- Change invisibleChange2 = insert("repo", newChangeWithStatus(repo, Change.Status.NEW), user2);
+ Change invisibleChange1 = insert(project, newChangeWithStatus(repo, Change.Status.NEW), user2);
+ Change invisibleChange2 = insert(project, newChangeWithStatus(repo, Change.Status.NEW), user2);
gApi.changes().id(invisibleChange1.getKey().get()).setPrivate(true, null);
gApi.changes().id(invisibleChange2.getKey().get()).setPrivate(true, null);
diff --git a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index 12bafd5d71..572e7afba3 100644
--- a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -16,13 +16,13 @@ package com.google.gerrit.server.query.group;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.util.stream.Collectors.toList;
import static org.junit.Assert.fail;
import com.google.common.base.CharMatcher;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
@@ -144,7 +144,7 @@ public abstract class AbstractQueryGroupsTest extends GerritServerTests {
Account.Id userId =
createAccountOutsideRequestContext("user", "User", "user@example.com", true);
user = userFactory.create(userId);
- requestContext.setContext(newRequestContext(userId));
+ setRequestContextForUser(userId);
currentUserInfo = gApi.accounts().id(userId.get()).get();
}
@@ -156,7 +156,8 @@ public abstract class AbstractQueryGroupsTest extends GerritServerTests {
}
protected void setAnonymous() {
- requestContext.setContext(anonymousUser::get);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(anonymousUser::get);
}
@After
@@ -165,7 +166,8 @@ public abstract class AbstractQueryGroupsTest extends GerritServerTests {
lifecycle.stop();
}
if (requestContext != null) {
- requestContext.setContext(null);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(null);
}
}
@@ -436,14 +438,22 @@ public abstract class AbstractQueryGroupsTest extends GerritServerTests {
return gApi.accounts().create(accountInput).get();
}
+ private void setRequestContextForUser(Account.Id userId) {
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(newRequestContext(userId));
+ }
+
+ @CanIgnoreReturnValue
protected GroupInfo createGroup(String name, AccountInfo... members) throws Exception {
return createGroupWithDescription(name, null, members);
}
+ @CanIgnoreReturnValue
protected GroupInfo createGroup(GroupInput in) throws Exception {
return gApi.groups().create(in).get();
}
+ @CanIgnoreReturnValue
protected GroupInfo createGroupWithDescription(
String name, String description, AccountInfo... members) throws Exception {
GroupInput in = new GroupInput();
@@ -454,6 +464,7 @@ public abstract class AbstractQueryGroupsTest extends GerritServerTests {
return createGroup(in);
}
+ @CanIgnoreReturnValue
protected GroupInfo createGroupWithOwner(String name, GroupInfo ownerGroup) throws Exception {
GroupInput in = new GroupInput();
in.name = name;
@@ -461,6 +472,7 @@ public abstract class AbstractQueryGroupsTest extends GerritServerTests {
return createGroup(in);
}
+ @CanIgnoreReturnValue
protected GroupInfo createGroupThatIsVisibleToAll(String name) throws Exception {
GroupInput in = new GroupInput();
in.name = name;
@@ -478,18 +490,21 @@ public abstract class AbstractQueryGroupsTest extends GerritServerTests {
return gApi.groups().id(uuid.get()).get();
}
+ @CanIgnoreReturnValue
protected List<GroupInfo> assertQuery(Object query, GroupInfo... groups) throws Exception {
return assertQuery(newQuery(query), groups);
}
+ @CanIgnoreReturnValue
protected List<GroupInfo> assertQuery(QueryRequest query, GroupInfo... groups) throws Exception {
return assertQuery(query, Arrays.asList(groups));
}
+ @CanIgnoreReturnValue
protected List<GroupInfo> assertQuery(QueryRequest query, List<GroupInfo> groups)
throws Exception {
List<GroupInfo> result = query.get();
- Iterable<String> uuids = uuids(result);
+ List<String> uuids = uuids(result);
assertWithMessage(format(query, result, groups))
.that(uuids)
.containsExactlyElementsIn(uuids(groups))
@@ -562,11 +577,11 @@ public abstract class AbstractQueryGroupsTest extends GerritServerTests {
return b == null ? false : b;
}
- protected static Iterable<String> ids(GroupInfo... groups) {
+ protected static List<String> ids(GroupInfo... groups) {
return uuids(Arrays.asList(groups));
}
- protected static Iterable<String> uuids(List<GroupInfo> groups) {
+ protected static List<String> uuids(List<GroupInfo> groups) {
return groups.stream().map(g -> g.id).sorted().collect(toList());
}
diff --git a/javatests/com/google/gerrit/server/query/group/BUILD b/javatests/com/google/gerrit/server/query/group/BUILD
index 550cb410ce..25cd76bd20 100644
--- a/javatests/com/google/gerrit/server/query/group/BUILD
+++ b/javatests/com/google/gerrit/server/query/group/BUILD
@@ -24,6 +24,7 @@ java_library(
"//java/com/google/gerrit/testing:gerrit-test-util",
"//lib:guava",
"//lib:jgit",
+ "//lib/errorprone:annotations",
"//lib/guice",
"//lib/truth",
"//lib/truth:truth-java8-extension",
@@ -41,6 +42,7 @@ junit_tests(
":abstract_query_tests",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib:guava",
"//lib:jgit",
"//lib/guice",
],
@@ -57,6 +59,7 @@ junit_tests(
":abstract_query_tests",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib:guava",
"//lib:jgit",
"//lib/guice",
],
diff --git a/javatests/com/google/gerrit/server/query/group/FakeQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/FakeQueryGroupsTest.java
index d347716923..d623f30edd 100644
--- a/javatests/com/google/gerrit/server/query/group/FakeQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/FakeQueryGroupsTest.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.group;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.InMemoryModule;
@@ -21,7 +22,6 @@ import com.google.gerrit.testing.IndexConfig;
import com.google.gerrit.testing.IndexVersions;
import com.google.inject.Guice;
import com.google.inject.Injector;
-import java.util.List;
import java.util.Map;
import org.eclipse.jgit.lib.Config;
@@ -48,7 +48,8 @@ public class FakeQueryGroupsTest extends AbstractFakeQueryGroupsTest {
@ConfigSuite.Configs
public static Map<String, Config> againstPreviousIndexVersion() {
// the current schema version is already tested by the inherited default config suite
- List<Integer> schemaVersions = IndexVersions.getWithoutLatest(GroupSchemaDefinitions.INSTANCE);
+ ImmutableList<Integer> schemaVersions =
+ IndexVersions.getWithoutLatest(GroupSchemaDefinitions.INSTANCE);
return IndexVersions.asConfigMap(
GroupSchemaDefinitions.INSTANCE, schemaVersions, "againstIndexVersion", defaultConfig());
}
diff --git a/javatests/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java
index 2a453a0554..3f3eb58142 100644
--- a/javatests/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.group;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.InMemoryModule;
@@ -21,7 +22,6 @@ import com.google.gerrit.testing.IndexConfig;
import com.google.gerrit.testing.IndexVersions;
import com.google.inject.Guice;
import com.google.inject.Injector;
-import java.util.List;
import java.util.Map;
import org.eclipse.jgit.lib.Config;
@@ -34,7 +34,8 @@ public class LuceneQueryGroupsTest extends AbstractQueryGroupsTest {
@ConfigSuite.Configs
public static Map<String, Config> againstPreviousIndexVersion() {
// the current schema version is already tested by the inherited default config suite
- List<Integer> schemaVersions = IndexVersions.getWithoutLatest(GroupSchemaDefinitions.INSTANCE);
+ ImmutableList<Integer> schemaVersions =
+ IndexVersions.getWithoutLatest(GroupSchemaDefinitions.INSTANCE);
return IndexVersions.asConfigMap(
GroupSchemaDefinitions.INSTANCE, schemaVersions, "againstIndexVersion", defaultConfig());
}
diff --git a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
index 47d485df0b..d766e162f4 100644
--- a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
@@ -147,7 +147,7 @@ public abstract class AbstractQueryProjectsTest extends GerritServerTests {
Account.Id userId = createAccount("user", "User", "user@example.com", true);
user = userFactory.create(userId);
- requestContext.setContext(newRequestContext(userId));
+ setRequestContextForUser(userId);
currentUserInfo = gApi.accounts().id(userId.get()).get();
// All-Projects and All-Users are not indexed, index them now.
@@ -166,7 +166,8 @@ public abstract class AbstractQueryProjectsTest extends GerritServerTests {
}
protected void setAnonymous() {
- requestContext.setContext(anonymousUser::get);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(anonymousUser::get);
}
@After
@@ -174,7 +175,8 @@ public abstract class AbstractQueryProjectsTest extends GerritServerTests {
if (lifecycle != null) {
lifecycle.stop();
}
- requestContext.setContext(null);
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(null);
}
@Test
@@ -426,6 +428,12 @@ public abstract class AbstractQueryProjectsTest extends GerritServerTests {
}
}
+ private void setRequestContextForUser(Account.Id userId) {
+ @SuppressWarnings("unused")
+ var unused = requestContext.setContext(newRequestContext(userId));
+ }
+
+ @CanIgnoreReturnValue
protected ProjectInfo createProject(String name) throws Exception {
ProjectInput in = new ProjectInput();
in.name = name;
@@ -440,6 +448,7 @@ public abstract class AbstractQueryProjectsTest extends GerritServerTests {
return gApi.projects().create(in).get();
}
+ @CanIgnoreReturnValue
protected ProjectInfo createProjectWithDescription(String name, String description)
throws Exception {
ProjectInput in = new ProjectInput();
@@ -448,6 +457,7 @@ public abstract class AbstractQueryProjectsTest extends GerritServerTests {
return gApi.projects().create(in).get();
}
+ @CanIgnoreReturnValue
protected ProjectInfo createProjectWithState(String name, ProjectState state) throws Exception {
ProjectInfo info = createProject(name);
ConfigInput config = new ConfigInput();
@@ -456,6 +466,7 @@ public abstract class AbstractQueryProjectsTest extends GerritServerTests {
return info;
}
+ @CanIgnoreReturnValue
protected ProjectInfo createProjectRestrictedToRegisteredUsers(String name) throws Exception {
createProject(name);
@@ -475,19 +486,22 @@ public abstract class AbstractQueryProjectsTest extends GerritServerTests {
return gApi.projects().name(nameKey.get()).get();
}
+ @CanIgnoreReturnValue
protected List<ProjectInfo> assertQuery(Object query, ProjectInfo... projects) throws Exception {
return assertQuery(newQuery(query), projects);
}
+ @CanIgnoreReturnValue
protected List<ProjectInfo> assertQuery(QueryRequest query, ProjectInfo... projects)
throws Exception {
return assertQuery(query, Arrays.asList(projects));
}
+ @CanIgnoreReturnValue
protected List<ProjectInfo> assertQuery(QueryRequest query, List<ProjectInfo> projects)
throws Exception {
List<ProjectInfo> result = query.get();
- Iterable<String> names = names(result);
+ List<String> names = names(result);
assertWithMessage(format(query, result, projects))
.that(names)
.containsExactlyElementsIn(names(projects))
@@ -552,11 +566,11 @@ public abstract class AbstractQueryProjectsTest extends GerritServerTests {
return indexes.getSearchIndex().getSchema();
}
- protected static Iterable<String> names(ProjectInfo... projects) {
+ protected static List<String> names(ProjectInfo... projects) {
return names(Arrays.asList(projects));
}
- protected static Iterable<String> names(List<ProjectInfo> projects) {
+ protected static List<String> names(List<ProjectInfo> projects) {
return projects.stream().map(p -> p.name).collect(toList());
}
diff --git a/javatests/com/google/gerrit/server/query/project/BUILD b/javatests/com/google/gerrit/server/query/project/BUILD
index 2ae73b3b78..ae94e699e9 100644
--- a/javatests/com/google/gerrit/server/query/project/BUILD
+++ b/javatests/com/google/gerrit/server/query/project/BUILD
@@ -37,7 +37,9 @@ junit_tests(
deps = [
":abstract_query_tests",
"//java/com/google/gerrit/index/project",
+ "//java/com/google/gerrit/server",
"//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib:guava",
"//lib:jgit",
"//lib/guice",
],
@@ -53,7 +55,9 @@ junit_tests(
deps = [
":abstract_query_tests",
"//java/com/google/gerrit/index/project",
+ "//java/com/google/gerrit/server",
"//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib:guava",
"//lib:jgit",
"//lib/guice",
],
diff --git a/javatests/com/google/gerrit/server/query/project/FakeQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/FakeQueryProjectsTest.java
index 6fc05681fb..19fbe66d83 100644
--- a/javatests/com/google/gerrit/server/query/project/FakeQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/FakeQueryProjectsTest.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.project;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.index.project.ProjectSchemaDefinitions;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.InMemoryModule;
@@ -21,7 +22,6 @@ import com.google.gerrit.testing.IndexConfig;
import com.google.gerrit.testing.IndexVersions;
import com.google.inject.Guice;
import com.google.inject.Injector;
-import java.util.List;
import java.util.Map;
import org.eclipse.jgit.lib.Config;
@@ -34,7 +34,7 @@ public class FakeQueryProjectsTest extends AbstractQueryProjectsTest {
@ConfigSuite.Configs
public static Map<String, Config> againstPreviousIndexVersion() {
// the current schema version is already tested by the inherited default config suite
- List<Integer> schemaVersions =
+ ImmutableList<Integer> schemaVersions =
IndexVersions.getWithoutLatest(
com.google.gerrit.index.project.ProjectSchemaDefinitions.INSTANCE);
return IndexVersions.asConfigMap(
diff --git a/javatests/com/google/gerrit/server/query/project/LuceneQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/LuceneQueryProjectsTest.java
index 77a56edde8..09e8388a9b 100644
--- a/javatests/com/google/gerrit/server/query/project/LuceneQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/LuceneQueryProjectsTest.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.project;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.index.project.ProjectSchemaDefinitions;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.InMemoryModule;
@@ -21,7 +22,6 @@ import com.google.gerrit.testing.IndexConfig;
import com.google.gerrit.testing.IndexVersions;
import com.google.inject.Guice;
import com.google.inject.Injector;
-import java.util.List;
import java.util.Map;
import org.eclipse.jgit.lib.Config;
@@ -34,7 +34,7 @@ public class LuceneQueryProjectsTest extends AbstractQueryProjectsTest {
@ConfigSuite.Configs
public static Map<String, Config> againstPreviousIndexVersion() {
// the current schema version is already tested by the inherited default config suite
- List<Integer> schemaVersions =
+ ImmutableList<Integer> schemaVersions =
IndexVersions.getWithoutLatest(
com.google.gerrit.index.project.ProjectSchemaDefinitions.INSTANCE);
return IndexVersions.asConfigMap(
diff --git a/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java b/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
index eb1d275498..6f504c49b4 100644
--- a/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
+++ b/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
@@ -86,7 +86,7 @@ public class ListChangeCommentsTest {
linkAuthor(comments.get(5), accountId1);
// Change massages have exactly same timestamps
- List<ChangeMessage> changeMessages =
+ ImmutableList<ChangeMessage> changeMessages =
ImmutableList.of(
createChangeMessage("cm0", "10", Account.id(accountId1)),
createChangeMessage("cm1", "11", Account.id(accountId1)),
@@ -142,7 +142,7 @@ public class ListChangeCommentsTest {
linkAuthor(comments.get(1), accountId2Imported);
linkAuthor(comments.get(2), accountId3);
- List<ChangeMessage> changeMessages =
+ ImmutableList<ChangeMessage> changeMessages =
ImmutableList.of(
createChangeMessage("changeMessage0", tsCm0, Account.id(accountId1)),
createChangeMessage("changeMessage1", tsCm1, Account.id(accountId2)),
diff --git a/javatests/com/google/gerrit/server/restapi/project/TagSorterTest.java b/javatests/com/google/gerrit/server/restapi/project/TagSorterTest.java
new file mode 100644
index 0000000000..9a926a2cdd
--- /dev/null
+++ b/javatests/com/google/gerrit/server/restapi/project/TagSorterTest.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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.
+
+package com.google.gerrit.server.restapi.project;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.common.ListTagSortOption;
+import com.google.gerrit.extensions.common.WebLinkInfo;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TagSorterTest {
+ private static final String revision = "dfdd715e31db256dfba48239f83f9b8da4bc243f";
+ private static final boolean canDelete = true;
+ private static final List<WebLinkInfo> webLinks = new ArrayList<>();
+ private static final TagSorter tagSorter = new TagSorter();
+ private List<TagInfo> tags;
+
+ @Before
+ public void initializeTags() {
+ tags = createTags();
+ }
+
+ @Test
+ public void testSortTagsByRef() {
+ tagSorter.sort(ListTagSortOption.REF, tags, false);
+
+ assertThat(tags.get(0).ref).isEqualTo("refs/tags/v1.0");
+ assertThat(tags.get(1).ref).isEqualTo("refs/tags/v2.0");
+ assertThat(tags.get(2).ref).isEqualTo("refs/tags/v3.0");
+ assertThat(tags.get(3).ref).isEqualTo("refs/tags/v4.0");
+ }
+
+ @Test
+ public void testSortTagsByCreationTime() {
+ tagSorter.sort(ListTagSortOption.CREATION_TIME, tags, false);
+
+ assertThat(tags.get(0).ref).isEqualTo("refs/tags/v2.0");
+ assertThat(tags.get(1).ref).isEqualTo("refs/tags/v3.0");
+ assertThat(tags.get(2).ref).isEqualTo("refs/tags/v1.0");
+ assertThat(tags.get(3).ref).isEqualTo("refs/tags/v4.0");
+ }
+
+ @Test
+ public void testSortTagsByCreationTimeDescendingOrder() {
+ tagSorter.sort(ListTagSortOption.CREATION_TIME, tags, true);
+
+ assertThat(tags.get(0).ref).isEqualTo("refs/tags/v4.0");
+ assertThat(tags.get(1).ref).isEqualTo("refs/tags/v2.0");
+ assertThat(tags.get(2).ref).isEqualTo("refs/tags/v3.0");
+ assertThat(tags.get(3).ref).isEqualTo("refs/tags/v1.0");
+ }
+
+ private List<TagInfo> createTags() {
+ Instant t1 = Instant.now();
+ Instant t2 = t1.minusSeconds(10);
+ Instant t3 = t1.minusSeconds(1);
+
+ List<TagInfo> tags = new ArrayList<>();
+ tags.add(new TagInfo("refs/tags/v1.0", revision, canDelete, webLinks, t1));
+ tags.add(new TagInfo("refs/tags/v2.0", revision, canDelete, webLinks, t2));
+ tags.add(new TagInfo("refs/tags/v3.0", revision, canDelete, webLinks, t3));
+ tags.add(new TagInfo("refs/tags/v4.0", revision, canDelete, webLinks, (Instant) null));
+
+ return tags;
+ }
+}
diff --git a/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java b/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
index 509447a064..c7ac533146 100644
--- a/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
+++ b/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
@@ -26,7 +26,6 @@ import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import java.time.Instant;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.List;
import org.junit.Test;
@@ -42,7 +41,7 @@ public class IgnoreSelfApprovalRuleTest {
PatchSetApproval approvalVerified = makeApproval(VERIFIED.getLabelId(), USER1, 2);
PatchSetApproval approvalCr = makeApproval(codeReview.getLabelId(), USER1, 2);
- Collection<PatchSetApproval> filteredApprovals =
+ ImmutableList<PatchSetApproval> filteredApprovals =
IgnoreSelfApprovalRule.filterApprovalsByLabel(
ImmutableList.of(approvalVerified, approvalCr), VERIFIED);
@@ -62,7 +61,7 @@ public class IgnoreSelfApprovalRuleTest {
makeApproval(VERIFIED.getLabelId(), USER1, +1),
makeApproval(VERIFIED.getLabelId(), USER1, +2));
- Collection<PatchSetApproval> filteredApprovals =
+ ImmutableList<PatchSetApproval> filteredApprovals =
IgnoreSelfApprovalRule.filterOutPositiveApprovalsOfUser(approvals, USER1);
assertThat(filteredApprovals).containsExactly(approvalM1, approvalM2);
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
index 5d5ef4e64b..4970228075 100644
--- a/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.schema;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.server.schema.NoteDbSchemaUpdater.requiredUpgrades;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
@@ -146,14 +145,16 @@ public class NoteDbSchemaUpdaterTest {
}
protected void seedGroupSequenceRef() {
- new RepoSequence(
- repoManager,
- GitReferenceUpdated.DISABLED,
- allUsersName,
- Sequence.NAME_GROUPS,
- () -> 1,
- 1)
- .next();
+ @SuppressWarnings("unused")
+ var unused =
+ new RepoSequence(
+ repoManager,
+ GitReferenceUpdated.DISABLED,
+ allUsersName,
+ Sequence.NAME_GROUPS,
+ () -> 1,
+ 1)
+ .next();
}
/** Test-specific setup. */
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionCheckTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionCheckTest.java
index d2ccaa9a6d..2c9aad45ac 100644
--- a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionCheckTest.java
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionCheckTest.java
@@ -24,7 +24,7 @@ import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.testing.InMemoryRepositoryManager;
import com.google.inject.ProvisionException;
import java.io.IOException;
-import java.nio.file.Paths;
+import java.nio.file.Path;
import org.eclipse.jgit.lib.Config;
import org.junit.Before;
import org.junit.Test;
@@ -37,11 +37,11 @@ public class NoteDbSchemaVersionCheckTest {
public void setup() throws Exception {
AllProjectsName allProjectsName = new AllProjectsName("All-Projects");
GitRepositoryManager repoManager = new InMemoryRepositoryManager();
- repoManager.createRepository(allProjectsName);
+ repoManager.createRepository(allProjectsName).close();
versionManager = new NoteDbSchemaVersionManager(allProjectsName, repoManager);
testRefAction(() -> versionManager.init());
- sitePaths = new SitePaths(Paths.get("/tmp/foo"));
+ sitePaths = new SitePaths(Path.of("/tmp/foo"));
}
@Test
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java
index 31697fd855..bd3b8af67d 100644
--- a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java
@@ -16,7 +16,6 @@ package com.google.gerrit.server.schema;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.server.schema.NoteDbSchemaVersions.guessVersion;
import com.google.common.collect.ImmutableList;
@@ -67,7 +66,8 @@ public class NoteDbSchemaVersionsTest {
@Test
public void schemaConstructors() throws Exception {
for (int version : NoteDbSchemaVersions.ALL.keySet()) {
- NoteDbSchemaVersions.get(NoteDbSchemaVersions.ALL, version);
+ @SuppressWarnings("unused")
+ var unused = NoteDbSchemaVersions.get(NoteDbSchemaVersions.ALL, version);
}
}
}
diff --git a/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java b/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java
index 1304c53f3a..325961cceb 100644
--- a/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java
+++ b/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.schema;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.GroupReference;
@@ -110,7 +109,7 @@ public class SchemaCreatorImplTest {
private boolean hasGroup(String name) throws Exception {
try (Repository repo = repositoryManager.openRepository(allUsersName)) {
- List<GroupReference> nameNotes = GroupNameNotes.loadAllGroups(repo);
+ ImmutableList<GroupReference> nameNotes = GroupNameNotes.loadAllGroups(repo);
return nameNotes.stream().anyMatch(g -> g.getName().equals(name));
}
}
diff --git a/javatests/com/google/gerrit/server/submit/SubmoduleCommitsTest.java b/javatests/com/google/gerrit/server/submit/SubmoduleCommitsTest.java
index a391c03439..8724b9a9c7 100644
--- a/javatests/com/google/gerrit/server/submit/SubmoduleCommitsTest.java
+++ b/javatests/com/google/gerrit/server/submit/SubmoduleCommitsTest.java
@@ -16,7 +16,6 @@ package com.google.gerrit.server.submit;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
diff --git a/javatests/com/google/gerrit/server/update/BUILD b/javatests/com/google/gerrit/server/update/BUILD
index 345681d276..a602ee920e 100644
--- a/javatests/com/google/gerrit/server/update/BUILD
+++ b/javatests/com/google/gerrit/server/update/BUILD
@@ -12,11 +12,13 @@ junit_tests(
"//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/entities",
"//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/git",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server/util/time",
"//java/com/google/gerrit/testing:gerrit-test-util",
"//java/com/google/gerrit/testing:test-ref-update-context",
"//lib:guava",
+ "//lib:guava-retrying",
"//lib:jgit",
"//lib:jgit-junit",
"//lib/guice",
diff --git a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
index f7a2afa1bb..79a688ccc6 100644
--- a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
+++ b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
@@ -22,6 +22,9 @@ import static com.google.gerrit.testing.TestActionRefUpdateContext.openTestRefUp
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import com.github.rholder.retry.Attempt;
+import com.github.rholder.retry.RetryException;
+import com.github.rholder.retry.RetryListener;
import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
@@ -37,7 +40,10 @@ import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.events.AttentionSetListener;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.git.LockFailureException;
+import com.google.gerrit.git.RefUpdateUtil;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.Sequences;
@@ -53,6 +59,7 @@ import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.patch.DiffSummary;
import com.google.gerrit.server.patch.DiffSummaryKey;
+import com.google.gerrit.server.update.RetryableAction.ActionType;
import com.google.gerrit.server.update.context.RefUpdateContext;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.InMemoryTestEnvironment;
@@ -61,12 +68,17 @@ import com.google.inject.Provider;
import com.google.inject.name.Named;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -107,6 +119,8 @@ public class BatchUpdateTest {
@Inject private IdentifiedUser.GenericFactory userFactory;
@Inject private InternalUser.Factory internalUserFactory;
@Inject private AbandonOp.Factory abandonOpFactory;
+ @Inject @GerritPersonIdent private PersonIdent serverIdent;
+ @Inject private RetryHelper retryHelper;
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
@@ -566,6 +580,245 @@ public class BatchUpdateTest {
assertThat(metaCommit.getParent(0)).isEqualTo(oldHead);
}
+ @Test
+ public void lockFailureOnConcurrentUpdate() throws Exception {
+ Change.Id changeId = createChange();
+ ObjectId metaId = getMetaId(changeId);
+
+ ChangeNotes notes = changeNotesFactory.create(project, changeId);
+ assertThat(notes.getChange().getStatus()).isEqualTo(Change.Status.NEW);
+
+ AtomicBoolean doneBackgroundUpdate = new AtomicBoolean(false);
+
+ // Create a listener that updates the change meta ref concurrently on the first attempt.
+ BatchUpdateListener listener =
+ new BatchUpdateListener() {
+ @Override
+ public BatchRefUpdate beforeUpdateRefs(BatchRefUpdate bru) {
+ try (RevWalk rw = new RevWalk(repo.getRepository())) {
+ RevCommit old = rw.parseCommit(metaId);
+ RevCommit commit =
+ repo.commit()
+ .parent(old)
+ .author(serverIdent)
+ .committer(serverIdent)
+ .setTopLevelTree(old.getTree())
+ .message("Concurrent Update\n\nPatch-Set: 1")
+ .create();
+ RefUpdate ru = repo.getRepository().updateRef(RefNames.changeMetaRef(changeId));
+ ru.setExpectedOldObjectId(metaId);
+ ru.setNewObjectId(commit);
+ ru.update();
+ RefUpdateUtil.checkResult(ru);
+ doneBackgroundUpdate.set(true);
+ } catch (Exception e) {
+ // Ignore. If an exception happens doneBackgroundUpdate is false and we fail later
+ // when doneBackgroundUpdate is checked.
+ }
+ return bru;
+ }
+ };
+
+ // Do a batch update, expect that it fails with LOCK_FAILURE due to the concurrent update.
+ assertThat(doneBackgroundUpdate.get()).isFalse();
+ UpdateException exception =
+ assertThrows(
+ UpdateException.class,
+ () -> {
+ try (BatchUpdate bu =
+ batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
+ bu.addOp(changeId, abandonOpFactory.create(null, "abandon"));
+ bu.execute(listener);
+ }
+ });
+ assertThat(exception).hasCauseThat().isInstanceOf(LockFailureException.class);
+ assertThat(doneBackgroundUpdate.get()).isTrue();
+
+ // Check that the change was not updated.
+ notes = changeNotesFactory.create(project, changeId);
+ assertThat(notes.getChange().getStatus()).isEqualTo(Change.Status.NEW);
+ }
+
+ @Test
+ public void useRetryHelperToRetryOnLockFailure() throws Exception {
+ Change.Id changeId = createChange();
+ ObjectId metaId = getMetaId(changeId);
+
+ ChangeNotes notes = changeNotesFactory.create(project, changeId);
+ assertThat(notes.getChange().getStatus()).isEqualTo(Change.Status.NEW);
+
+ AtomicBoolean doneBackgroundUpdate = new AtomicBoolean(false);
+
+ // Create a listener that updates the change meta ref concurrently on the first attempt.
+ BatchUpdateListener listener =
+ new BatchUpdateListener() {
+ @Override
+ public BatchRefUpdate beforeUpdateRefs(BatchRefUpdate bru) {
+ if (!doneBackgroundUpdate.getAndSet(true)) {
+ try (RevWalk rw = new RevWalk(repo.getRepository())) {
+ RevCommit old = rw.parseCommit(metaId);
+ RevCommit commit =
+ repo.commit()
+ .parent(old)
+ .author(serverIdent)
+ .committer(serverIdent)
+ .setTopLevelTree(old.getTree())
+ .message("Concurrent Update\n\nPatch-Set: 1")
+ .create();
+ RefUpdate ru = repo.getRepository().updateRef(RefNames.changeMetaRef(changeId));
+ ru.setExpectedOldObjectId(metaId);
+ ru.setNewObjectId(commit);
+ ru.update();
+ RefUpdateUtil.checkResult(ru);
+ } catch (Exception e) {
+ // Ignore. If an exception happens doneBackgroundUpdate is false and we fail later
+ // when doneBackgroundUpdate is checked.
+ }
+ }
+ return bru;
+ }
+ };
+
+ // Do a batch update, expect that it succeeds due to retrying despite the LOCK_FAILURE on the
+ // first attempt.
+ assertThat(doneBackgroundUpdate.get()).isFalse();
+
+ @SuppressWarnings("unused")
+ var unused =
+ retryHelper
+ .changeUpdate(
+ "batchUpdate",
+ updateFactory -> {
+ try (BatchUpdate bu = updateFactory.create(project, user.get(), TimeUtil.now())) {
+ bu.addOp(changeId, abandonOpFactory.create(null, "abandon"));
+ bu.execute(listener);
+ }
+ return null;
+ })
+ .call();
+
+ // Check that the concurrent update was done.
+ assertThat(doneBackgroundUpdate.get()).isTrue();
+
+ // Check that the BatchUpdate updated the change.
+ notes = changeNotesFactory.create(project, changeId);
+ assertThat(notes.getChange().getStatus()).isEqualTo(Change.Status.ABANDONED);
+ }
+
+ @Test
+ public void noRetryingOnOuterLevelIfRetryingWasAlreadyDoneOnInnerLevel() throws Exception {
+ Change.Id changeId = createChange();
+
+ ChangeNotes notes = changeNotesFactory.create(project, changeId);
+ assertThat(notes.getChange().getStatus()).isEqualTo(Change.Status.NEW);
+
+ AtomicBoolean backgroundFailure = new AtomicBoolean(false);
+
+ // Create a listener that updates the change meta ref concurrently on all attempts.
+ BatchUpdateListener listener =
+ new BatchUpdateListener() {
+ @Override
+ public BatchRefUpdate beforeUpdateRefs(BatchRefUpdate bru) {
+ try (RevWalk rw = new RevWalk(repo.getRepository())) {
+ String changeMetaRef = RefNames.changeMetaRef(changeId);
+ ObjectId metaId = repo.getRepository().exactRef(changeMetaRef).getObjectId();
+ RevCommit old = rw.parseCommit(metaId);
+ RevCommit commit =
+ repo.commit()
+ .parent(old)
+ .author(serverIdent)
+ .committer(serverIdent)
+ .setTopLevelTree(old.getTree())
+ .message("Concurrent Update\n\nPatch-Set: 1")
+ .create();
+ RefUpdate ru = repo.getRepository().updateRef(changeMetaRef);
+ ru.setExpectedOldObjectId(metaId);
+ ru.setNewObjectId(commit);
+ ru.update();
+ RefUpdateUtil.checkResult(ru);
+ } catch (Exception e) {
+ backgroundFailure.set(true);
+ }
+
+ return bru;
+ }
+ };
+
+ AtomicInteger innerRetryOnExceptionCounter = new AtomicInteger();
+ AtomicInteger outerRetryOnExceptionCounter = new AtomicInteger();
+ UpdateException exception =
+ assertThrows(
+ UpdateException.class,
+ () ->
+ // Outer level retrying. We expect that no retrying is happens here because retrying
+ // is already done on the inner level.
+ retryHelper
+ .action(
+ ActionType.CHANGE_UPDATE,
+ "batchUpdate",
+ () ->
+ // Inner level retrying. We expect that retrying happens here.
+ retryHelper
+ .changeUpdate(
+ "batchUpdate",
+ updateFactory -> {
+ try (BatchUpdate bu =
+ updateFactory.create(
+ project, user.get(), TimeUtil.now())) {
+ bu.addOp(
+ changeId, abandonOpFactory.create(null, "abandon"));
+ bu.execute(listener);
+ }
+ return null;
+ })
+ .listener(
+ new RetryListener() {
+ @Override
+ public <V> void onRetry(Attempt<V> attempt) {
+ if (attempt.hasException()) {
+ @SuppressWarnings("unused")
+ var unused =
+ innerRetryOnExceptionCounter.incrementAndGet();
+ }
+ }
+ })
+ .call())
+ // give it enough time to potentially retry multiple times when each retry also
+ // does retrying
+ .defaultTimeoutMultiplier(5)
+ .listener(
+ new RetryListener() {
+ @Override
+ public <V> void onRetry(Attempt<V> attempt) {
+ if (attempt.hasException()) {
+ @SuppressWarnings("unused")
+ var unused = outerRetryOnExceptionCounter.incrementAndGet();
+ }
+ }
+ })
+ .call());
+ assertThat(backgroundFailure.get()).isFalse();
+
+ // Check that retrying was done on the inner level.
+ assertThat(innerRetryOnExceptionCounter.get()).isGreaterThan(1);
+
+ // Check that there was no retrying on the outer level since retrying was already done on the
+ // inner level.
+ // We expect 1 because RetryListener#onRetry is invoked before the rejection predicate and stop
+ // strategies are applied (i.e. before RetryHelper decides whether retrying should be done).
+ assertThat(outerRetryOnExceptionCounter.get()).isEqualTo(1);
+
+ assertThat(exception).hasCauseThat().isInstanceOf(RetryException.class);
+ assertThat(exception.getCause()).hasCauseThat().isInstanceOf(UpdateException.class);
+ assertThat(exception.getCause().getCause())
+ .hasCauseThat()
+ .isInstanceOf(LockFailureException.class);
+
+ // Check that the change was not updated.
+ notes = changeNotesFactory.create(project, changeId);
+ assertThat(notes.getChange().getStatus()).isEqualTo(Change.Status.NEW);
+ }
+
private Change.Id createChange() throws Exception {
Change.Id id = Change.id(sequences.nextChangeId());
try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
diff --git a/javatests/com/google/gerrit/server/update/RepoViewTest.java b/javatests/com/google/gerrit/server/update/RepoViewTest.java
index b118c9fc5d..ca4cb9604b 100644
--- a/javatests/com/google/gerrit/server/update/RepoViewTest.java
+++ b/javatests/com/google/gerrit/server/update/RepoViewTest.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.update;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
import static org.eclipse.jgit.lib.Constants.R_HEADS;
diff --git a/javatests/com/google/gerrit/server/update/context/RefUpdateContextTest.java b/javatests/com/google/gerrit/server/update/context/RefUpdateContextTest.java
index 06466697a8..d9d52148c3 100644
--- a/javatests/com/google/gerrit/server/update/context/RefUpdateContextTest.java
+++ b/javatests/com/google/gerrit/server/update/context/RefUpdateContextTest.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.update.context;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.DIRECT_PUSH;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.GROUPS_UPDATE;
diff --git a/javatests/com/google/gerrit/testing/BUILD b/javatests/com/google/gerrit/testing/BUILD
index 9443b0d7f5..136938a7a4 100644
--- a/javatests/com/google/gerrit/testing/BUILD
+++ b/javatests/com/google/gerrit/testing/BUILD
@@ -7,6 +7,7 @@ junit_tests(
deps = [
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib:guava",
"//lib:jgit",
"//lib/truth",
],
diff --git a/javatests/com/google/gerrit/testing/IndexVersionsTest.java b/javatests/com/google/gerrit/testing/IndexVersionsTest.java
index 0362ddc271..3bd3e75ef6 100644
--- a/javatests/com/google/gerrit/testing/IndexVersionsTest.java
+++ b/javatests/com/google/gerrit/testing/IndexVersionsTest.java
@@ -21,6 +21,7 @@ import static com.google.gerrit.testing.IndexVersions.ALL;
import static com.google.gerrit.testing.IndexVersions.CURRENT;
import static com.google.gerrit.testing.IndexVersions.PREVIOUS;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
import java.util.ArrayList;
import java.util.List;
@@ -129,7 +130,7 @@ public class IndexVersionsTest {
+ SCHEMA_DEF.getSchemas().keySet());
}
- private static List<Integer> get(String value) {
+ private static ImmutableList<Integer> get(String value) {
return IndexVersions.get(ChangeSchemaDefinitions.INSTANCE, "test", value);
}
diff --git a/lib/errorprone/BUILD b/lib/errorprone/BUILD
index 456860aaee..f95a4305d5 100644
--- a/lib/errorprone/BUILD
+++ b/lib/errorprone/BUILD
@@ -3,6 +3,7 @@ load("@rules_java//java:defs.bzl", "java_library")
java_library(
name = "annotations",
data = ["//lib:LICENSE-Apache2.0"],
+ neverlink = 1,
visibility = ["//visibility:public"],
exports = ["@error-prone-annotations//jar"],
)
diff --git a/lib/lucene/BUILD b/lib/lucene/BUILD
index 93eeccb319..6b14a78932 100644
--- a/lib/lucene/BUILD
+++ b/lib/lucene/BUILD
@@ -2,51 +2,36 @@ load("@rules_java//java:defs.bzl", "java_binary", "java_import", "java_library")
package(default_visibility = ["//visibility:public"])
-# Merge jars so
-# META-INF/services/org.apache.lucene.codecs.Codec
-# contains the union of both Codec collections.
-java_binary(
- name = "lucene-core-and-backward-codecs-merged",
+java_library(
+ name = "lucene-analyzers-common",
data = ["//lib:LICENSE-Apache2.0"],
- main_class = "NotImportant",
- runtime_deps = [
- # in case of conflict, we want the implementation of backwards-codecs
- # first.
- "@backward-codecs//jar",
- "@lucene-core//jar",
- ],
-)
-
-java_import(
- name = "lucene-core-and-backward-codecs",
- jars = [
- ":lucene-core-and-backward-codecs-merged_deploy.jar",
- ],
+ exports = ["@lucene-analyzers-common//jar"],
+ runtime_deps = [":lucene-core"],
)
java_library(
- name = "lucene-analyzers-common",
+ name = "lucene-backward-codecs",
data = ["//lib:LICENSE-Apache2.0"],
- exports = ["@lucene-analyzers-common//jar"],
- runtime_deps = [":lucene-core-and-backward-codecs"],
+ exports = ["@lucene-backward-codecs//jar"],
)
java_library(
name = "lucene-core",
data = ["//lib:LICENSE-Apache2.0"],
exports = ["@lucene-core//jar"],
+ runtime_deps = [":lucene-backward-codecs"],
)
java_library(
name = "lucene-misc",
data = ["//lib:LICENSE-Apache2.0"],
exports = ["@lucene-misc//jar"],
- runtime_deps = [":lucene-core-and-backward-codecs"],
+ runtime_deps = [":lucene-core"],
)
java_library(
name = "lucene-queryparser",
data = ["//lib:LICENSE-Apache2.0"],
exports = ["@lucene-queryparser//jar"],
- runtime_deps = [":lucene-core-and-backward-codecs"],
+ runtime_deps = [":lucene-core"],
)
diff --git a/lib/nongoogle_test.sh b/lib/nongoogle_test.sh
index 78f485233b..6865340dcb 100755
--- a/lib/nongoogle_test.sh
+++ b/lib/nongoogle_test.sh
@@ -11,7 +11,6 @@ TMP=$(mktemp -d || mktemp -d -t /tmp/tmp.XXXXXX)
grep 'name = "[^"]*"' ${bzl} | sed 's|^[^"]*"||g;s|".*$||g' | sort > $TMP/names
cat << EOF > $TMP/want
-backward-codecs
cglib-3_2
commons-io
dropwizard-core
@@ -36,6 +35,7 @@ log-api
log-ext
log4j
lucene-analyzers-common
+lucene-backward-codecs
lucene-core
lucene-misc
lucene-queryparser
diff --git a/package.json b/package.json
index e671e863f0..bfbf0c414b 100644
--- a/package.json
+++ b/package.json
@@ -11,16 +11,16 @@
},
"devDependencies": {
"@koa/cors": "^3.4.3",
- "@types/page": "^1.11.6",
+ "@types/page": "^1.11.9",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@web/dev-server": "^0.1.38",
"@web/dev-server-esbuild": "^0.3.6",
- "eslint": "^8.49.0",
+ "eslint": "^8.56.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-html": "^7.1.0",
- "eslint-plugin-import": "^2.28.1",
+ "eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsdoc": "^44.2.7",
- "eslint-plugin-lit": "^1.9.1",
+ "eslint-plugin-lit": "^1.11.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-regex": "^1.10.0",
diff --git a/plugins/delete-project b/plugins/delete-project
-Subproject 93038507ff113042bf09ba96d788af43a5deff2
+Subproject ea78b4b817151f47f6e3aca7bf1e90f14518caa
diff --git a/plugins/hooks b/plugins/hooks
-Subproject 30073628612bce23826f4be71bfdd159da521cb
+Subproject f975f914312b258f84957d19f96014c3edd1264
diff --git a/plugins/package.json b/plugins/package.json
index 9e920868bf..dc63a8c247 100644
--- a/plugins/package.json
+++ b/plugins/package.json
@@ -3,39 +3,39 @@
"description": "Gerrit Code Review - frontend plugin dependencies, each plugin may depend on a subset of these",
"browser": true,
"dependencies": {
- "@codemirror/autocomplete": "^6.9.1",
- "@codemirror/commands": "^6.2.5",
+ "@codemirror/autocomplete": "^6.11.1",
+ "@codemirror/commands": "^6.3.3",
"@codemirror/lang-cpp": "^6.0.2",
"@codemirror/lang-css": "^6.2.1",
- "@codemirror/lang-html": "^6.4.6",
+ "@codemirror/lang-html": "^6.4.7",
"@codemirror/lang-java": "^6.0.1",
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/lang-json": "^6.0.1",
- "@codemirror/lang-less": "^6.0.0",
- "@codemirror/lang-markdown": "^6.2.1",
+ "@codemirror/lang-less": "^6.0.2",
+ "@codemirror/lang-markdown": "^6.2.3",
"@codemirror/lang-php": "^6.0.1",
"@codemirror/lang-python": "^6.1.3",
"@codemirror/lang-rust": "^6.0.1",
"@codemirror/lang-sass": "^6.0.2",
- "@codemirror/lang-sql": "^6.5.4",
+ "@codemirror/lang-sql": "^6.5.5",
"@codemirror/lang-xml": "^6.0.2",
"@codemirror/language": "^6.9.1",
"@codemirror/language-data": "^6.3.1",
"@codemirror/legacy-modes": "^6.3.3",
"@codemirror/lint": "^6.4.2",
- "@codemirror/search": "^6.5.4",
- "@codemirror/state": "^6.2.1",
- "@codemirror/view": "^6.20.2",
+ "@codemirror/search": "^6.5.5",
+ "@codemirror/state": "^6.4.0",
+ "@codemirror/view": "^6.23.0",
"@gerritcodereview/typescript-api": "3.8.0",
- "@open-wc/testing": "^3.2.0",
+ "@open-wc/testing": "^3.2.2",
"@polymer/decorators": "^3.0.0",
"@polymer/polymer": "^3.5.1",
"@web/dev-server-esbuild": "^0.3.6",
"@web/test-runner": "^0.15.3",
- "lit": "^3.0.0",
+ "lit": "^3.1.0",
"rxjs": "^6.6.7",
"sinon": "^13.0.2"
},
"license": "Apache-2.0",
"private": true
-} \ No newline at end of file
+}
diff --git a/plugins/plugin-manager b/plugins/plugin-manager
-Subproject ba74d4969462c2592bcf97868dd76c33041d47b
+Subproject cdd2d2d69666a70a16ac02bacf8e7fbbf4ca997
diff --git a/plugins/replication b/plugins/replication
-Subproject 8fd3c271ce0a21480e3d04da5ad2112efea3bed
+Subproject 56b8ffbab5bf619c0b6b5d44f0255fd41b9e1c8
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
-Subproject 9321303265fcab2ff7f764a444f8c2391574763
+Subproject 18c867b6a957b3ddeb7a9e9789819fc60bdcd99
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
-Subproject 084a37253dc94ac52cfaa1c9d516fcb8b0318b3
+Subproject 4bee62cbbc21979b841843dd5faaf79470a3596
diff --git a/plugins/webhooks b/plugins/webhooks
-Subproject 1dc0a718839f8872a59c189da7243ee77a4fe78
+Subproject 2e5ec3b3bcf5e7ba50edba9eca3c15c8057ad6c
diff --git a/plugins/yarn.lock b/plugins/yarn.lock
index 21ec5228ad..844c4f1f49 100644
--- a/plugins/yarn.lock
+++ b/plugins/yarn.lock
@@ -11,11 +11,11 @@
typical "^7.1.1"
"@babel/code-frame@^7.12.11":
- version "7.22.13"
- resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e"
- integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==
+ version "7.23.5"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244"
+ integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==
dependencies:
- "@babel/highlight" "^7.22.13"
+ "@babel/highlight" "^7.23.4"
chalk "^2.4.2"
"@babel/helper-validator-identifier@^7.22.20":
@@ -23,44 +23,44 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
-"@babel/highlight@^7.22.13":
- version "7.22.20"
- resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54"
- integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==
+"@babel/highlight@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b"
+ integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==
dependencies:
"@babel/helper-validator-identifier" "^7.22.20"
chalk "^2.4.2"
js-tokens "^4.0.0"
-"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.3.2", "@codemirror/autocomplete@^6.7.1", "@codemirror/autocomplete@^6.9.1":
- version "6.9.1"
- resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.9.1.tgz#e0989c6a33a37604b5d2c896dcca7562ae3d7c61"
- integrity sha512-yma56tqD7khIZK4gy4X5lX3/k5ArMiCGat7HEWRF/8L2kqOjVdp2qKZqpcJjwTIjSj6fqKAHqi7IjtH3QFE+Bw==
+"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.11.1", "@codemirror/autocomplete@^6.3.2", "@codemirror/autocomplete@^6.7.1":
+ version "6.11.1"
+ resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.11.1.tgz#c733900eee58ac2de817317b9fd1e91b857c4329"
+ integrity sha512-L5UInv8Ffd6BPw0P3EF7JLYAMeEbclY7+6Q11REt8vhih8RuLreKtPy/xk8wPxs4EQgYqzI7cdgpiYwWlbS/ow==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
-"@codemirror/commands@^6.2.5":
- version "6.2.5"
- resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.2.5.tgz#e889f93f9cc85b32f6b2844d85d08688f695a6b8"
- integrity sha512-dSi7ow2P2YgPBZflR9AJoaTHvqmeGIgkhignYMd5zK5y6DANTvxKxp6eMEpIDUJkRAaOY/TFZ4jP1ADIO/GLVA==
+"@codemirror/commands@^6.3.3":
+ version "6.3.3"
+ resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.3.3.tgz#03face5bf5f3de0fc4e09b177b3c91eda2ceb7e9"
+ integrity sha512-dO4hcF0fGT9tu1Pj1D2PvGvxjeGkbC6RGcZw6Qs74TH+Ed1gw98jmUgd2axWvIZEqTeTuFrg1lEB1KV6cK9h1A==
dependencies:
"@codemirror/language" "^6.0.0"
- "@codemirror/state" "^6.2.0"
+ "@codemirror/state" "^6.4.0"
"@codemirror/view" "^6.0.0"
- "@lezer/common" "^1.0.0"
+ "@lezer/common" "^1.1.0"
"@codemirror/lang-angular@^0.1.0":
- version "0.1.2"
- resolved "https://registry.yarnpkg.com/@codemirror/lang-angular/-/lang-angular-0.1.2.tgz#a3f565297842ad60caf2a0bf6f6137c13d19a666"
- integrity sha512-Nq7lmx9SU+JyoaRcs6SaJs7uAmW2W06HpgJVQYeZptVGNWDzDvzhjwVb/ZuG1rwTlOocY4Y9GwNOBuKCeJbKtw==
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/@codemirror/lang-angular/-/lang-angular-0.1.3.tgz#83035e7e9e1f0e2ba466e83d778407b519089a28"
+ integrity sha512-xgeWGJQQl1LyStvndWtruUvb4SnBZDAu/gvFH/ZU+c0W25tQR8e5hq7WTwiIY2dNxnf+49mRiGI/9yxIwB6f5w==
dependencies:
"@codemirror/lang-html" "^6.0.0"
"@codemirror/lang-javascript" "^6.1.2"
"@codemirror/language" "^6.0.0"
- "@lezer/common" "^1.0.0"
+ "@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.3.3"
@@ -83,10 +83,10 @@
"@lezer/common" "^1.0.2"
"@lezer/css" "^1.0.0"
-"@codemirror/lang-html@^6.0.0", "@codemirror/lang-html@^6.4.6":
- version "6.4.6"
- resolved "https://registry.yarnpkg.com/@codemirror/lang-html/-/lang-html-6.4.6.tgz#25c1c71591da80e75dceb67ceeab41e87bde29a5"
- integrity sha512-E4C8CVupBksXvgLSme/zv31x91g06eZHSph7NczVxZW+/K+3XgJGWNT//2WLzaKSBoxpAjaOi5ZnPU1SHhjh3A==
+"@codemirror/lang-html@^6.0.0", "@codemirror/lang-html@^6.4.7":
+ version "6.4.7"
+ resolved "https://registry.yarnpkg.com/@codemirror/lang-html/-/lang-html-6.4.7.tgz#e375e3c9ae898b5aca6e17b5055a3a76c7a8f5ff"
+ integrity sha512-y9hWSSO41XlcL4uYwWyk0lEgTHcelWWfRuqmvcAmxfCs0HNWZdriWo/EU43S63SxEZpc1Hd50Itw7ktfQvfkUg==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/lang-css" "^6.0.0"
@@ -127,20 +127,21 @@
"@codemirror/language" "^6.0.0"
"@lezer/json" "^1.0.0"
-"@codemirror/lang-less@^6.0.0":
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/@codemirror/lang-less/-/lang-less-6.0.1.tgz#fef10e8dbcd07055b815c3928233a05a8549181e"
- integrity sha512-ABcsKBjLbyPZwPR5gePpc8jEKCQrFF4pby2WlMVdmJOOr7OWwwyz8DZonPx/cKDE00hfoSLc8F7yAcn/d6+rTQ==
+"@codemirror/lang-less@^6.0.0", "@codemirror/lang-less@^6.0.2":
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/@codemirror/lang-less/-/lang-less-6.0.2.tgz#2e3d82a3ddb8710e6409689cd4a28c66558d0cb8"
+ integrity sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==
dependencies:
"@codemirror/lang-css" "^6.2.0"
"@codemirror/language" "^6.0.0"
+ "@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
-"@codemirror/lang-markdown@^6.0.0", "@codemirror/lang-markdown@^6.2.1":
- version "6.2.1"
- resolved "https://registry.yarnpkg.com/@codemirror/lang-markdown/-/lang-markdown-6.2.1.tgz#2d9e14a579e9a17c164902dcc0d771e86a6803d1"
- integrity sha512-Tpk1+CllQ/KU27AYixsxvtqqQS2xYfLEUiBEyyzO+Y9/0LfI2oB1qM2cMhy0D7oRnG0ZSAy9qcYniZc9VtlIxg==
+"@codemirror/lang-markdown@^6.0.0", "@codemirror/lang-markdown@^6.2.3":
+ version "6.2.3"
+ resolved "https://registry.yarnpkg.com/@codemirror/lang-markdown/-/lang-markdown-6.2.3.tgz#ce572230a872e8eef88bce40213f26e66a7e4497"
+ integrity sha512-wCewRLWpdefWi7uVkHIDiE8+45Fe4buvMDZkihqEom5uRUQrl76Zb13emjeK3W+8pcRgRfAmwelURBbxNEKCIg==
dependencies:
"@codemirror/autocomplete" "^6.7.1"
"@codemirror/lang-html" "^6.0.0"
@@ -189,35 +190,37 @@
"@lezer/common" "^1.0.2"
"@lezer/sass" "^1.0.0"
-"@codemirror/lang-sql@^6.0.0", "@codemirror/lang-sql@^6.5.4":
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/@codemirror/lang-sql/-/lang-sql-6.5.4.tgz#cb1f0140a22d9d502d93d7b91390c2e0becedce5"
- integrity sha512-5Gq7fYtT/5HbNyIG7a8vYaqOYQU3JbgtBe3+derkrFUXRVcjkf8WVgz++PIbMFAQsOFMDdDR+uiNM8ZRRuXH+w==
+"@codemirror/lang-sql@^6.0.0", "@codemirror/lang-sql@^6.5.5":
+ version "6.5.5"
+ resolved "https://registry.yarnpkg.com/@codemirror/lang-sql/-/lang-sql-6.5.5.tgz#85619f4ea6738c07c0241b19c62d8ef86678e672"
+ integrity sha512-DvOaP2RXLb2xlxJxxydTFfwyYw5YDqEFea6aAfgh9UH0kUD6J1KFZ0xPgPpw1eo/5s2w3L6uh5PVR7GM23GxkQ==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
+ "@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
"@codemirror/lang-vue@^0.1.1":
- version "0.1.2"
- resolved "https://registry.yarnpkg.com/@codemirror/lang-vue/-/lang-vue-0.1.2.tgz#50aec87b93ba8a6b0742a24cbab566b3989ee6ca"
- integrity sha512-D4YrefiRBAr+CfEIM4S3yvGSbYW+N69mttIfGMEf7diHpRbmygDxS+R/5xSqjgtkY6VO6qmUrre1GkRcWeZa9A==
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/@codemirror/lang-vue/-/lang-vue-0.1.3.tgz#bf79b9152cc18b4903d64c1f67e186ae045c8a97"
+ integrity sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==
dependencies:
"@codemirror/lang-html" "^6.0.0"
"@codemirror/lang-javascript" "^6.1.2"
"@codemirror/language" "^6.0.0"
- "@lezer/common" "^1.0.0"
+ "@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.3.1"
"@codemirror/lang-wast@^6.0.0":
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/@codemirror/lang-wast/-/lang-wast-6.0.1.tgz#c15bec84548a5e9b0a43fa69fb63631d087d6047"
- integrity sha512-sQLsqhRjl2MWG3rxZysX+2XAyed48KhLBHLgq9xcKxIJu3npH/G+BIXW5NM5mHeDUjG0jcGh9BcjP0NfMStuzA==
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/@codemirror/lang-wast/-/lang-wast-6.0.2.tgz#d2b14175e5e80d7878cbbb29e20ec90dc12d3a2b"
+ integrity sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==
dependencies:
"@codemirror/language" "^6.0.0"
+ "@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
@@ -258,12 +261,12 @@
"@codemirror/legacy-modes" "^6.1.0"
"@codemirror/language@^6.0.0", "@codemirror/language@^6.3.0", "@codemirror/language@^6.4.0", "@codemirror/language@^6.6.0", "@codemirror/language@^6.8.0", "@codemirror/language@^6.9.1":
- version "6.9.1"
- resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.9.1.tgz#97e2c3e44cf4ff152add865ed7ecec73868446a4"
- integrity sha512-lWRP3Y9IUdOms6DXuBpoWwjkR7yRmnS0hKYCbSfPz9v6Em1A1UCRujAkDiCrdYfs1Z0Eu4dGtwovNPStIfkgNA==
+ version "6.10.0"
+ resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.10.0.tgz#2d0e818716825ee2ed0dacd04595eaa61bae8f23"
+ integrity sha512-2vaNn9aPGCRFKWcHPFksctzJ8yS5p7YoaT+jHpc0UGKzNuAIx4qy6R5wiqbP+heEEdyaABA582mNqSHzSoYdmg==
dependencies:
"@codemirror/state" "^6.0.0"
- "@codemirror/view" "^6.0.0"
+ "@codemirror/view" "^6.23.0"
"@lezer/common" "^1.1.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
@@ -285,26 +288,26 @@
"@codemirror/view" "^6.0.0"
crelt "^1.0.5"
-"@codemirror/search@^6.5.4":
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.5.4.tgz#54005697bf581f7dccbbb4a0c34d3a7aa25a513a"
- integrity sha512-YoTrvjv9e8EbPs58opjZKyJ3ewFrVSUzQ/4WXlULQLSDDr1nGPJ67mMXFNNVYwdFhybzhrzrtqgHmtpJwIF+8g==
+"@codemirror/search@^6.5.5":
+ version "6.5.5"
+ resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.5.5.tgz#cf97e201da364da2285c2a250167af25bbd2a4a2"
+ integrity sha512-PIEN3Ke1buPod2EHbJsoQwlbpkz30qGZKcnmH1eihq9+bPQx8gelauUwLYaY4vBOuBAuEhmpDLii4rj/uO0yMA==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
crelt "^1.0.5"
-"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.4", "@codemirror/state@^6.2.0", "@codemirror/state@^6.2.1":
- version "6.2.1"
- resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.2.1.tgz#6dc8d8e5abb26b875e3164191872d69a5e85bd73"
- integrity sha512-RupHSZ8+OjNT38zU9fKH2sv+Dnlr8Eb8sl4NOnnqz95mCFTZUaiRP8Xv5MeeaG0px2b8Bnfe7YGwCV3nsBhbuw==
+"@codemirror/state@^6.0.0", "@codemirror/state@^6.4.0":
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.4.0.tgz#8bc3e096c84360b34525a84696a84f86b305363a"
+ integrity sha512-hm8XshYj5Fo30Bb922QX9hXB/bxOAVH+qaqHBzw5TKa72vOeslyGwd4X8M0c1dJ9JqxlaMceOQ8RsL9tC7gU0A==
-"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.20.2":
- version "6.20.2"
- resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.20.2.tgz#77c8e2801cb8c324740780a9f3ab19a15096a51a"
- integrity sha512-tZ9F0UZU2P3eTRtgljg3DaCOTn2FIjQU/ktTCjSz9/6he3GHDNxSCDAPidMtF+09r23o0h9H/5U7xibtUuEgdg==
+"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0":
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.23.0.tgz#8054a2043273abad7f1587d15accb0623e1960ed"
+ integrity sha512-/51px9N4uW8NpuWkyUX+iam5+PM6io2fm+QmRnzwqBy5v/pwGg9T0kILFtYeum8hjuvENtgsGNKluOfqIICmeQ==
dependencies:
- "@codemirror/state" "^6.1.4"
+ "@codemirror/state" "^6.4.0"
style-mod "^4.1.0"
w3c-keyname "^2.2.4"
@@ -441,152 +444,149 @@
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@jridgewell/trace-mapping@^0.3.12":
- version "0.3.19"
- resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811"
- integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==
+ version "0.3.20"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f"
+ integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==
dependencies:
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
-"@lezer/common@^1.0.0", "@lezer/common@^1.0.2", "@lezer/common@^1.1.0":
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.1.0.tgz#2e5bfe01d7a2ada6056d93c677bba4f1495e098a"
- integrity sha512-XPIN3cYDXsoJI/oDWoR2tD++juVrhgIago9xyKhZ7IhGlzdDM9QgC8D8saKNCz5pindGcznFr2HBSsEQSWnSjw==
+"@lezer/common@^1.0.0", "@lezer/common@^1.0.2", "@lezer/common@^1.1.0", "@lezer/common@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.0.tgz#f10493d12c4a196a02ff5fcf5695a516a4039aae"
+ integrity sha512-Wmvlm4q6tRpwiy20TnB3yyLTZim38Tkc50dPY8biQRwqE+ati/wD84rm3N15hikvdT4uSg9phs9ubjvcLmkpKg==
"@lezer/cpp@^1.0.0":
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/@lezer/cpp/-/cpp-1.1.1.tgz#ac0261f48dc3651bfea13fdaeff35f04c9011a7f"
- integrity sha512-eS1M3L3U2mDowoFVPG7tEp01SWu9/68Nx3HEBgLJVn3N9ku7g5S7WdFv0jzmcTipAyONYfZJ+7x4WRkfdB2Ung==
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@lezer/cpp/-/cpp-1.1.2.tgz#1db93b09e011e8a7a08c347c9d5b7749971253bf"
+ integrity sha512-macwKtyeUO0EW86r3xWQCzOV9/CF8imJLpJlPv3sDY57cPGeUZ8gXWOWNlJr52TVByMV3PayFQCA5SHEERDmVQ==
dependencies:
+ "@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
"@lezer/css@^1.0.0", "@lezer/css@^1.1.0":
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/@lezer/css/-/css-1.1.3.tgz#605495b00fd8a122088becf196a93744cbe817fc"
- integrity sha512-SjSM4pkQnQdJDVc80LYzEaMiNy9txsFbI7HsMgeVF28NdLaAdHNtQ+kB/QqDUzRBV/75NTXjJ/R5IdC8QQGxMg==
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/@lezer/css/-/css-1.1.6.tgz#b887d8f66d3d7b9b61a4c614a0ce923e05eee6dc"
+ integrity sha512-/HhbnfXchRc995VdDH9TBzd1B2CO/A4uhOhELqGjd7Bymgc+tGlb0W9Vp5GA1Otq8Ef4JCXpuKmr4hH3aFny6A==
dependencies:
+ "@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
"@lezer/highlight@^1.0.0", "@lezer/highlight@^1.1.3":
- version "1.1.6"
- resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.1.6.tgz#87e56468c0f43c2a8b3dc7f0b7c2804b34901556"
- integrity sha512-cmSJYa2us+r3SePpRCjN5ymCqCPv+zyXmDl0ciWtVaNiORT/MxM7ZgOMQZADD0o51qOaOg24qc/zBViOIwAjJg==
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.2.0.tgz#e5898c3644208b4b589084089dceeea2966f7780"
+ integrity sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==
dependencies:
"@lezer/common" "^1.0.0"
"@lezer/html@^1.3.0":
- version "1.3.6"
- resolved "https://registry.yarnpkg.com/@lezer/html/-/html-1.3.6.tgz#26a2a17da4e0f91835e36db9ccd025b2ed8d33f7"
- integrity sha512-Kk9HJARZTc0bAnMQUqbtuhFVsB4AnteR2BFUWfZV7L/x1H0aAKz6YabrfJ2gk/BEgjh9L3hg5O4y2IDZRBdzuQ==
+ version "1.3.8"
+ resolved "https://registry.yarnpkg.com/@lezer/html/-/html-1.3.8.tgz#e0c8b28f91607787ab6696a1dd802c0c38f679e4"
+ integrity sha512-EXseJ3pUzWxE6XQBQdqWHZqqlGQRSuNMBcLb6mZWS2J2v+QZhOObD+3ZIKIcm59ntTzyor4LqFTb72iJc3k23Q==
dependencies:
- "@lezer/common" "^1.0.0"
+ "@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
"@lezer/java@^1.0.0":
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/@lezer/java/-/java-1.0.4.tgz#f31f5af4bfc40475dc886f0e3e2d291889b87d25"
- integrity sha512-POc53LHf2AuNeRXjqZbXNu88GKj0KZTjjSx0L7tYeXlrEHF+3NAQx+dEwKVuCbkl0ZMtpRy2VsDYOV7KKV0oyg==
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@lezer/java/-/java-1.1.1.tgz#eed8813a5f3eb1a913aa8eaf40d5b20f40dee3d6"
+ integrity sha512-mt3dX13fRlpY7RlWELYRakanXgmwXsLRCrhstrn+c1sZd7jR2xle46/3heoxGd+oHxnuTnpoyXTyxcLJQs9+mQ==
dependencies:
+ "@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
"@lezer/javascript@^1.0.0":
- version "1.4.7"
- resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-1.4.7.tgz#4ebcce2db6043c07fbe827188c07cb001bc7fe37"
- integrity sha512-OVWlK0YEi7HM+9JRWtRkir8qvcg0/kVYg2TAMHlVtl6DU1C9yK1waEOLBMztZsV/axRJxsqfJKhzYz+bxZme5g==
+ version "1.4.11"
+ resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-1.4.11.tgz#4ca7681e29fda6f960e15d958ee4f4ceaf577223"
+ integrity sha512-B5Y9EJF4BWiMgj4ufxUo2hrORnmMBDrMtR+L7dwIO5pocuSAahG6QBwXR6PbKJOjRywJczU2r2LJPg79ER91TQ==
dependencies:
"@lezer/highlight" "^1.1.3"
"@lezer/lr" "^1.3.0"
"@lezer/json@^1.0.0":
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/@lezer/json/-/json-1.0.1.tgz#3bf5641f3d1408ec31a5f9b29e4e96c6e3a232e6"
- integrity sha512-nkVC27qiEZEjySbi6gQRuMwa2sDu2PtfjSgz0A4QF81QyRGm3kb2YRzLcOPcTEtmcwvrX/cej7mlhbwViA4WJw==
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@lezer/json/-/json-1.0.2.tgz#bdc849e174113e2d9a569a5e6fb1a27e2f703eaf"
+ integrity sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==
dependencies:
+ "@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
"@lezer/lr@^1.0.0", "@lezer/lr@^1.1.0", "@lezer/lr@^1.3.0", "@lezer/lr@^1.3.1", "@lezer/lr@^1.3.3":
- version "1.3.12"
- resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.3.12.tgz#ee65d79f5528d8f5c042cd8123325a48c411109b"
- integrity sha512-5nwY1JzCueUdRtlMBnlf1SUi69iGCq2ABq7WQFQMkn/kxPvoACAEnTp4P17CtXxYr7WCwtYPLL2AEvxKPuF1OQ==
+ version "1.3.14"
+ resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.3.14.tgz#59d4a3b25698bdac0ef182fa6eadab445fc4f29a"
+ integrity sha512-z5mY4LStlA3yL7aHT/rqgG614cfcvklS+8oFRFBYrs4YaWLJyKKM4+nN6KopToX0o9Hj6zmH6M5kinOYuy06ug==
dependencies:
"@lezer/common" "^1.0.0"
"@lezer/markdown@^1.0.0":
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/@lezer/markdown/-/markdown-1.1.0.tgz#5cee104ef353a3442ecee023ff1912826fac8658"
- integrity sha512-JYOI6Lkqbl83semCANkO3CKbKc0pONwinyagBufWBm+k4yhIcqfCF8B8fpEpvJLmIy7CAfwiq7dQ/PzUZA340g==
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@lezer/markdown/-/markdown-1.2.0.tgz#387cd5fba85479e3fa1d74586060dc5392c9ccb6"
+ integrity sha512-d7MwsfAukZJo1GpPrcPGa3MxaFFOqNp0gbqF+3F7pTeNDOgeJN1muXzx1XXDPt+Ac+/voCzsH7qXqnn+xReG/g==
dependencies:
"@lezer/common" "^1.0.0"
"@lezer/highlight" "^1.0.0"
"@lezer/php@^1.0.0":
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/@lezer/php/-/php-1.0.1.tgz#4496b58c980ca710c0433fd743d27e9964fd74ea"
- integrity sha512-aqdCQJOXJ66De22vzdwnuC502hIaG9EnPK2rSi+ebXyUd+j7GAX1mRjWZOVOmf3GST1YUfUCu6WXDiEgDGOVwA==
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@lezer/php/-/php-1.0.2.tgz#7c291631fc1e7f7efe99977522bc48bdc732658a"
+ integrity sha512-GN7BnqtGRpFyeoKSEqxvGvhJQiI4zkgmYnDk/JIyc7H7Ifc1tkPnUn/R2R8meH3h/aBf5rzjvU8ZQoyiNDtDrA==
dependencies:
+ "@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.1.0"
"@lezer/python@^1.1.4":
- version "1.1.8"
- resolved "https://registry.yarnpkg.com/@lezer/python/-/python-1.1.8.tgz#fe8d03d6cbc95a1d5625cffd30d78018ee816633"
- integrity sha512-1T/XsmeF57ijrjpC0Zmrf9YeO5mn2zC1XeSNrOnc0KB+6PgxJ5m7kWKt0CnwyS74oHQXbJxUUL+QDQJR26c1Gw==
+ version "1.1.10"
+ resolved "https://registry.yarnpkg.com/@lezer/python/-/python-1.1.10.tgz#580160705ef5b557d8829fd2bf8f09dc9a91a0fb"
+ integrity sha512-pvSjn+OWivmA/si/SFeGouHO50xoOZcPIFzf8dql0gRvcfCvLDpVIpnnGFFlB7wa0WDscDLo0NmH+4Tx80nBdQ==
dependencies:
+ "@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
"@lezer/rust@^1.0.0":
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/@lezer/rust/-/rust-1.0.1.tgz#ac2d7263fe22527e621bb5623929ba6d6c3a29ea"
- integrity sha512-j+ToFKM6Wpglv3OQ4ebHYdYIMT2dh0ziCCV0rTf47AWiHOVhR0WjaKrBq+yuvDQNEhr5sxPxVI7+naJIgpqcsQ==
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@lezer/rust/-/rust-1.0.2.tgz#cc9a75605d67182a0e799ac40b1965a61dcc6ef0"
+ integrity sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==
dependencies:
+ "@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
"@lezer/sass@^1.0.0":
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/@lezer/sass/-/sass-1.0.3.tgz#17e5d27e40979bc8b4aec8d05df0d01f745aedb8"
- integrity sha512-n4l2nVOB7gWiGU/Cg2IVxpt2Ic9Hgfgy/7gk+p/XJibAsPXs0lSbsfGwQgwsAw9B/euYo3oS6lEFr9WytoqcZg==
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@lezer/sass/-/sass-1.0.4.tgz#5d18c460a4a896145ac49bab8ea7998ac9d9b401"
+ integrity sha512-AqW4myvp73sbMk6y0+gJrMjN5xtqFZzqTftzO3YcO8gSL5d3pymIP3deQllAI8+s1ZoSzH6kD4hsoFLpkD9Kfg==
dependencies:
+ "@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
"@lezer/xml@^1.0.0":
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/@lezer/xml/-/xml-1.0.2.tgz#5c934602d1d3565fdaf04e93b534c8b94f4df2d1"
- integrity sha512-dlngsWceOtQBMuBPw5wtHpaxdPJ71aVntqjbpGkFtWsp4WtQmCnuTjQGocviymydN6M18fhj6UQX3oiEtSuY7w==
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@lezer/xml/-/xml-1.0.4.tgz#d565dd84af9ec0f620b0bb5f043b1233e63ffb0a"
+ integrity sha512-WmXKb5eX8+rRfZYSNRR5TPee/ZoDgBdVS/rj1VCJGDKa5gNldIctQYibCoFVyNhvZsyL/8nHbZJZPM4gnXN2Vw==
dependencies:
+ "@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
-"@lit-labs/ssr-dom-shim@^1.0.0", "@lit-labs/ssr-dom-shim@^1.1.0":
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz#64df34e2f12e68e78ac57e571d25ec07fa460ca9"
- integrity sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ==
-
-"@lit-labs/ssr-dom-shim@^1.1.2-pre.0":
+"@lit-labs/ssr-dom-shim@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz#d693d972974a354034454ec1317eb6afd0b00312"
integrity sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g==
-"@lit/reactive-element@^1.0.0", "@lit/reactive-element@^1.3.0", "@lit/reactive-element@^1.6.0":
- version "1.6.3"
- resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.6.3.tgz#25b4eece2592132845d303e091bad9b04cdcfe03"
- integrity sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==
- dependencies:
- "@lit-labs/ssr-dom-shim" "^1.0.0"
-
-"@lit/reactive-element@^2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.0.0.tgz#da14a256ac5533873b935840f306d572bac4a2ab"
- integrity sha512-wn+2+uDcs62ROBmVAwssO4x5xue/uKD3MGGZOXL2sMxReTRIT0JXKyMXeu7gh0aJ4IJNEIG/3aOnUaQvM7BMzQ==
+"@lit/reactive-element@^1.0.0 || ^2.0.0", "@lit/reactive-element@^2.0.0":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.0.2.tgz#779ae9d265407daaf7737cb892df5ec2a86e22a0"
+ integrity sha512-SVOwLAWUQg3Ji1egtOt1UiFe4zdDpnWHyc5qctSceJ5XIu0Uc76YmGpIjZgx9YJ0XtdW0Jm507sDvjOu+HnB8w==
dependencies:
- "@lit-labs/ssr-dom-shim" "^1.1.2-pre.0"
+ "@lit-labs/ssr-dom-shim" "^1.1.2"
"@mdn/browser-compat-data@^4.0.0":
version "4.2.1"
@@ -614,59 +614,44 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
-"@open-wc/chai-dom-equals@^0.12.36":
- version "0.12.36"
- resolved "https://registry.yarnpkg.com/@open-wc/chai-dom-equals/-/chai-dom-equals-0.12.36.tgz#ed0eb56b9e98c4d7f7280facce6215654aae9f4c"
- integrity sha512-Gt1fa37h4rtWPQGETSU4n1L678NmMi9KwHM1sH+JCGcz45rs8DBPx7MUVeGZ+HxRlbEI5t9LU2RGGv6xT2OlyA==
- dependencies:
- "@open-wc/semantic-dom-diff" "^0.13.16"
- "@types/chai" "^4.1.7"
-
"@open-wc/dedupe-mixin@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@open-wc/dedupe-mixin/-/dedupe-mixin-1.4.0.tgz#b3c58f8699b197bb5e923d624c720e67c9f324d6"
integrity sha512-Sj7gKl1TLcDbF7B6KUhtvr+1UCxdhMbNY5KxdU5IfMFWqL8oy1ZeAcCANjoB1TL0AJTcPmcCFsCbHf8X2jGDUA==
-"@open-wc/scoped-elements@^2.2.0":
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/@open-wc/scoped-elements/-/scoped-elements-2.2.0.tgz#4d65d7ba796c2bb76ef7934068532ca1795ea7b6"
- integrity sha512-Qe+vWsuVHFzUkdChwlmJGuQf9cA3I+QOsSHULV/6qf6wsqLM2/32svNRH+rbBIMwiPEwzZprZlkvkqQRucYnVA==
+"@open-wc/scoped-elements@^2.2.4":
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/@open-wc/scoped-elements/-/scoped-elements-2.2.4.tgz#081559b62d885ac0ec043546f17f1f680294d500"
+ integrity sha512-12X4F4QGPWcvPbxAiJ4v8wQFCOu+laZHRGfTrkoj+3JzACCtuxHG49YbuqVzQ135QPKCuhP9wA0kpGGEfUegyg==
dependencies:
- "@lit/reactive-element" "^1.0.0"
+ "@lit/reactive-element" "^1.0.0 || ^2.0.0"
"@open-wc/dedupe-mixin" "^1.4.0"
-"@open-wc/semantic-dom-diff@^0.13.16":
- version "0.13.21"
- resolved "https://registry.yarnpkg.com/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.13.21.tgz#718b9ec5f9a98935fc775e577ad094ae8d8b7dea"
- integrity sha512-BONpjHcGX2zFa9mfnwBCLEmlDsOHzT+j6Qt1yfK3MzFXFtAykfzFjAgaxPetu0YbBlCfXuMlfxI4vlRGCGMvFg==
-
"@open-wc/semantic-dom-diff@^0.20.0":
- version "0.20.0"
- resolved "https://registry.yarnpkg.com/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.20.0.tgz#3766aa88f67df624db0494adf82c8035216a2493"
- integrity sha512-qGHl3nkXluXsjpLY9bSZka/cnlrybPtJMs6RjmV/OP4ID7Gcz1uNWQks05pAhptDB1R47G6PQjdwxG8dXl1zGA==
+ version "0.20.1"
+ resolved "https://registry.yarnpkg.com/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.20.1.tgz#b1bb78be455bd99fb034d9baadbb959d7d124030"
+ integrity sha512-mPF/RPT2TU7Dw41LEDdaeP6eyTOWBD4z0+AHP4/d0SbgcfJZVRymlIB6DQmtz0fd2CImIS9kszaMmwMt92HBPA==
dependencies:
"@types/chai" "^4.3.1"
- "@web/test-runner-commands" "^0.7.0"
+ "@web/test-runner-commands" "^0.9.0"
-"@open-wc/testing-helpers@^2.3.0":
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/@open-wc/testing-helpers/-/testing-helpers-2.3.0.tgz#6ee88baaf316a6217c43e7ba536cb187d15cb6f4"
- integrity sha512-wkDipkia/OMWq5Z1KkAgvqNLfIOCiPGrrtfoCKuQje8u7F0Bz9Un44EwBtWcCdYtLc40quWP7XFpFsW8poIfUA==
+"@open-wc/testing-helpers@^2.3.1":
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/@open-wc/testing-helpers/-/testing-helpers-2.3.2.tgz#c2bfa82cedd833608effa2d2367fe9524ddf4434"
+ integrity sha512-uZMGC/C1m5EiwQsff6KMmCW25TYMQlJt4ilAWIjnelWGFg9HPUiLnlFvAas3ESUP+4OXLO8Oft7p4mHvbYvAEQ==
dependencies:
- "@open-wc/scoped-elements" "^2.2.0"
- lit "^2.0.0"
- lit-html "^2.0.0"
+ "@open-wc/scoped-elements" "^2.2.4"
+ lit "^2.0.0 || ^3.0.0"
+ lit-html "^2.0.0 || ^3.0.0"
-"@open-wc/testing@^3.2.0":
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/@open-wc/testing/-/testing-3.2.0.tgz#884ca348861a116829ce5657fccff11a1a9a07bd"
- integrity sha512-9geTbFq8InbcfniPtS8KCfb5sbQ9WE6QMo1Tli8XMnfllnkZok7Az4kTRAskGQeMeQN/I2I//jE5xY/60qhrHg==
+"@open-wc/testing@^3.2.2":
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/@open-wc/testing/-/testing-3.2.2.tgz#c952f4b20af0d201cc8cc436c2c3cdd338bf8177"
+ integrity sha512-byN4dJTd6ZyI9mWmI4lVj30uiu+rYvQr93g64Pd7UFBdAUgb02DHLj6fkJ1gjxA6LC/MeFd7K7mOZ4+vKrMptw==
dependencies:
"@esm-bundle/chai" "^4.3.4-fix.0"
- "@open-wc/chai-dom-equals" "^0.12.36"
"@open-wc/semantic-dom-diff" "^0.20.0"
- "@open-wc/testing-helpers" "^2.3.0"
- "@types/chai" "^4.2.11"
+ "@open-wc/testing-helpers" "^2.3.1"
"@types/chai-dom" "^1.11.0"
"@types/sinon-chai" "^3.2.3"
chai-a11y-axe "^1.5.0"
@@ -770,71 +755,71 @@
integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==
"@types/accepts@*":
- version "1.3.5"
- resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575"
- integrity sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==
+ version "1.3.7"
+ resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.7.tgz#3b98b1889d2b2386604c2bbbe62e4fb51e95b265"
+ integrity sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==
dependencies:
"@types/node" "*"
"@types/babel__code-frame@^7.0.2":
- version "7.0.4"
- resolved "https://registry.yarnpkg.com/@types/babel__code-frame/-/babel__code-frame-7.0.4.tgz#0d14543f70ca91f4d2b0513a60f1eb31432c42e1"
- integrity sha512-WBxINLlATjvmpCgBbb9tOPrKtcPfu4A/Yz2iRzmdaodfvjAS/Z0WZJClV9/EXvoC9viI3lgUs7B9Uo7G/RmMGg==
+ version "7.0.6"
+ resolved "https://registry.yarnpkg.com/@types/babel__code-frame/-/babel__code-frame-7.0.6.tgz#20a899c0d29fba1ddf5c2156a10a2bda75ee6f29"
+ integrity sha512-Anitqkl3+KrzcW2k77lRlg/GfLZLWXBuNgbEcIOU6M92yw42vsd3xV/Z/yAHEj8m+KUjL6bWOVOFqX8PFPJ4LA==
"@types/body-parser@*":
- version "1.19.3"
- resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.3.tgz#fb558014374f7d9e56c8f34bab2042a3a07d25cd"
- integrity sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==
+ version "1.19.5"
+ resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4"
+ integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==
dependencies:
"@types/connect" "*"
"@types/node" "*"
"@types/chai-dom@^1.11.0":
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/@types/chai-dom/-/chai-dom-1.11.1.tgz#5f91fb34a612ccef177c70100c7c1b98a684d696"
- integrity sha512-q+fs4jdKZFDhXOWBehY0jDGCp8nxVe11Ia8MxqlIsJC3Y2JU149PSBYF2li2F3uxJFSAl2Rf8XeLWonHglpcGw==
+ version "1.11.3"
+ resolved "https://registry.yarnpkg.com/@types/chai-dom/-/chai-dom-1.11.3.tgz#1659ace2698cdcd9ed8b2c007876f53e37d9cc89"
+ integrity sha512-EUEZI7uID4ewzxnU7DJXtyvykhQuwe+etJ1wwOiJyQRTH/ifMWKX+ghiXkxCUvNJ6IQDodf0JXhuP6zZcy2qXQ==
dependencies:
"@types/chai" "*"
-"@types/chai@*", "@types/chai@^4.1.7", "@types/chai@^4.2.11", "@types/chai@^4.2.12", "@types/chai@^4.3.1":
- version "4.3.6"
- resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.6.tgz#7b489e8baf393d5dd1266fb203ddd4ea941259e6"
- integrity sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==
+"@types/chai@*", "@types/chai@^4.2.12", "@types/chai@^4.3.1":
+ version "4.3.11"
+ resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.11.tgz#e95050bf79a932cb7305dd130254ccdf9bde671c"
+ integrity sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==
"@types/co-body@^6.1.0":
- version "6.1.1"
- resolved "https://registry.yarnpkg.com/@types/co-body/-/co-body-6.1.1.tgz#28d253c95cfbe30c8e8c5d69d4c0dbbcffc101c2"
- integrity sha512-I9A1k7o4m8m6YPYJIGb1JyNTLqRWtSPg1JOZPWlE19w8Su2VRgRVp/SkKftQSwoxWHGUxGbON4jltONMumC8bQ==
+ version "6.1.3"
+ resolved "https://registry.yarnpkg.com/@types/co-body/-/co-body-6.1.3.tgz#201796c6389066b400cfcb4e1ec5c3db798265a2"
+ integrity sha512-UhuhrQ5hclX6UJctv5m4Rfp52AfG9o9+d9/HwjxhVB5NjXxr5t9oKgJxN8xRHgr35oo8meUEHUPFWiKg6y71aA==
dependencies:
"@types/node" "*"
"@types/qs" "*"
"@types/command-line-args@^5.0.0":
- version "5.2.1"
- resolved "https://registry.yarnpkg.com/@types/command-line-args/-/command-line-args-5.2.1.tgz#233bd1ba687e84ecbec0388e09f9ec9ebf63c55b"
- integrity sha512-U2OcmS2tj36Yceu+mRuPyUV0ILfau/h5onStcSCzqTENsq0sBiAp2TmaXu1k8fY4McLcPKSYl9FRzn2hx5bI+w==
+ version "5.2.3"
+ resolved "https://registry.yarnpkg.com/@types/command-line-args/-/command-line-args-5.2.3.tgz#553ce2fd5acf160b448d307649b38ffc60d39639"
+ integrity sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==
"@types/connect@*":
- version "3.4.36"
- resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.36.tgz#e511558c15a39cb29bd5357eebb57bd1459cd1ab"
- integrity sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==
+ version "3.4.38"
+ resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858"
+ integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==
dependencies:
"@types/node" "*"
"@types/content-disposition@*":
- version "0.5.6"
- resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.6.tgz#0f5fa03609f308a7a1a57e0b0afe4b95f1d19740"
- integrity sha512-GmShTb4qA9+HMPPaV2+Up8tJafgi38geFi7vL4qAM7k8BwjoelgHZqEUKJZLvughUw22h6vD/wvwN4IUCaWpDA==
+ version "0.5.8"
+ resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.8.tgz#6742a5971f490dc41e59d277eee71361fea0b537"
+ integrity sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==
"@types/convert-source-map@^2.0.0":
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/@types/convert-source-map/-/convert-source-map-2.0.1.tgz#e72e8a3de9d6fe3d8e43d5c101c346de2ff6abdf"
- integrity sha512-tm5Eb3AwhibN6ULRaad5TbNO83WoXVZLh2YRGAFH+qWkUz48l9Hu1jc+wJswB7T+ACWAG0cFnTeeQGpwedvlNw==
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@types/convert-source-map/-/convert-source-map-2.0.3.tgz#e586c22ca4af2d670d47d32d7fe365d5c5558695"
+ integrity sha512-ag0BfJLZf6CQz8VIuRIEYQ5Ggwk/82uvTQf27RcpyDNbY0Vw49LIPqAxk5tqYfrCs9xDaIMvl4aj7ZopnYL8bA==
"@types/cookies@*":
- version "0.7.8"
- resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.8.tgz#16fccd6d58513a9833c527701a90cc96d216bc18"
- integrity sha512-y6KhF1GtsLERUpqOV+qZJrjUGzc0GE6UTa0b5Z/LZ7Nm2mKSdCXmS6Kdnl7fctPNnMSouHjxqEWI12/YqQfk5w==
+ version "0.7.10"
+ resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.10.tgz#c4881dca4dd913420c488508d192496c46eb4fd0"
+ integrity sha512-hmUCjAk2fwZVPPkkPBcI7jGLIR5mg4OVoNMBwU6aVsMm/iNPY7z9/R+x2fSwLt/ZXoGua6C5Zy2k5xOo9jUyhQ==
dependencies:
"@types/connect" "*"
"@types/express" "*"
@@ -842,9 +827,9 @@
"@types/node" "*"
"@types/debounce@^1.2.0":
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/@types/debounce/-/debounce-1.2.2.tgz#8a9fd94003d874b56204526e6686b8a57dc4b278"
- integrity sha512-ow0L7we5RXNQocEO9LNBRJCk/ecBc8M0aTg0DLrlg1nsnKAcjvFmYFUbsxujlrbngRslmKIA4mKoOxIJdUElhw==
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/@types/debounce/-/debounce-1.2.4.tgz#cb7e85d9ad5ababfac2f27183e8ac8b576b2abb3"
+ integrity sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==
"@types/estree@0.0.39":
version "0.0.39"
@@ -852,9 +837,9 @@
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
"@types/express-serve-static-core@^4.17.33":
- version "4.17.37"
- resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.37.tgz#7e4b7b59da9142138a2aaa7621f5abedce8c7320"
- integrity sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==
+ version "4.17.41"
+ resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz#5077defa630c2e8d28aa9ffc2c01c157c305bef6"
+ integrity sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==
dependencies:
"@types/node" "*"
"@types/qs" "*"
@@ -862,9 +847,9 @@
"@types/send" "*"
"@types/express@*":
- version "4.17.18"
- resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.18.tgz#efabf5c4495c1880df1bdffee604b143b29c4a95"
- integrity sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d"
+ integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==
dependencies:
"@types/body-parser" "*"
"@types/express-serve-static-core" "^4.17.33"
@@ -872,50 +857,50 @@
"@types/serve-static" "*"
"@types/http-assert@*":
- version "1.5.3"
- resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.3.tgz#ef8e3d1a8d46c387f04ab0f2e8ab8cb0c5078661"
- integrity sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA==
+ version "1.5.5"
+ resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.5.tgz#dfb1063eb7c240ee3d3fe213dac5671cfb6a8dbf"
+ integrity sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==
"@types/http-errors@*":
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.2.tgz#a86e00bbde8950364f8e7846687259ffcd96e8c2"
- integrity sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f"
+ integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.3":
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
- integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7"
+ integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==
"@types/istanbul-lib-report@*":
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686"
- integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf"
+ integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==
dependencies:
"@types/istanbul-lib-coverage" "*"
"@types/istanbul-reports@^3.0.0":
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff"
- integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54"
+ integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==
dependencies:
"@types/istanbul-lib-report" "*"
"@types/keygrip@*":
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.3.tgz#2286b16ef71d8dea74dab00902ef419a54341bfe"
- integrity sha512-tfzBBb7OV2PbUfKbG6zRE5UbmtdLVCKT/XT364Z9ny6pXNbd9GnIB6aFYpq2A5lZ6mq9bhXgK6h5MFGNwhMmuQ==
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.6.tgz#1749535181a2a9b02ac04a797550a8787345b740"
+ integrity sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==
"@types/koa-compose@*":
- version "3.2.6"
- resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.6.tgz#17a077786d0ac5eee04c37a7d6c207b3252f6de9"
- integrity sha512-PHiciWxH3NRyAaxUdEDE1NIZNfvhgtPlsdkjRPazHC6weqt90Jr0uLhIQs+SDwC8HQ/jnA7UQP6xOqGFB7ugWw==
+ version "3.2.8"
+ resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.8.tgz#dec48de1f6b3d87f87320097686a915f1e954b57"
+ integrity sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==
dependencies:
"@types/koa" "*"
"@types/koa@*", "@types/koa@^2.11.6":
- version "2.13.9"
- resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.9.tgz#8d989ac17d7f033475fbe34c4f906c9287c2041a"
- integrity sha512-tPX3cN1dGrMn+sjCDEiQqXH2AqlPoPd594S/8zxwUm/ZbPsQXKqHPUypr2gjCPhHUc+nDJLduhh5lXI/1olnGQ==
+ version "2.13.12"
+ resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.12.tgz#70d87a9061a81909e0ee11ca50168416e8d3e795"
+ integrity sha512-vAo1KuDSYWFDB4Cs80CHvfmzSQWeUb909aQib0C0aFx4sw0K9UZFz2m5jaEP+b3X1+yr904iQiruS0hXi31jbw==
dependencies:
"@types/accepts" "*"
"@types/content-disposition" "*"
@@ -927,14 +912,14 @@
"@types/node" "*"
"@types/mime@*":
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"
- integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.4.tgz#2198ac274de6017b44d941e00261d5bc6a0e0a45"
+ integrity sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==
"@types/mime@^1":
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
- integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690"
+ integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==
"@types/mocha@^8.2.0":
version "8.2.3"
@@ -942,9 +927,11 @@
integrity sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw==
"@types/node@*":
- version "20.6.5"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-20.6.5.tgz#4c6a79adf59a8e8193ac87a0e522605b16587258"
- integrity sha512-2qGq5LAOTh9izcc0+F+dToFigBWiK1phKPt7rNhOqJSr35y8rlIBjDwGtFSgAI6MGIhjwOVNSQZVdJsZJ2uR1w==
+ version "20.10.6"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.6.tgz#a3ec84c22965802bf763da55b2394424f22bfbb5"
+ integrity sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==
+ dependencies:
+ undici-types "~5.26.4"
"@types/parse5@^6.0.1":
version "6.0.3"
@@ -952,14 +939,14 @@
integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==
"@types/qs@*":
- version "6.9.8"
- resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.8.tgz#f2a7de3c107b89b441e071d5472e6b726b4adf45"
- integrity sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==
+ version "6.9.11"
+ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.11.tgz#208d8a30bc507bd82e03ada29e4732ea46a6bbda"
+ integrity sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==
"@types/range-parser@*":
- version "1.2.4"
- resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
- integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
+ integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
"@types/resolve@1.17.1":
version "1.17.1"
@@ -969,46 +956,46 @@
"@types/node" "*"
"@types/send@*":
- version "0.17.1"
- resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.1.tgz#ed4932b8a2a805f1fe362a70f4e62d0ac994e301"
- integrity sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==
+ version "0.17.4"
+ resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a"
+ integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==
dependencies:
"@types/mime" "^1"
"@types/node" "*"
"@types/serve-static@*":
- version "1.15.2"
- resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.2.tgz#3e5419ecd1e40e7405d34093f10befb43f63381a"
- integrity sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==
+ version "1.15.5"
+ resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.5.tgz#15e67500ec40789a1e8c9defc2d32a896f05b033"
+ integrity sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==
dependencies:
"@types/http-errors" "*"
"@types/mime" "*"
"@types/node" "*"
"@types/sinon-chai@^3.2.3":
- version "3.2.9"
- resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.9.tgz#71feb938574bbadcb176c68e5ff1a6014c5e69d4"
- integrity sha512-/19t63pFYU0ikrdbXKBWj9PCdnKyTd0Qkz0X91Ta081cYsq90OxYdcWwK/dwEoDa6dtXgj2HJfmzgq+QZTHdmQ==
+ version "3.2.12"
+ resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.12.tgz#c7cb06bee44a534ec84f3a5534c3a3a46fd779b6"
+ integrity sha512-9y0Gflk3b0+NhQZ/oxGtaAJDvRywCa5sIyaVnounqLvmf93yBF4EgIRspePtkMs3Tr844nCclYMlcCNmLCvjuQ==
dependencies:
"@types/chai" "*"
"@types/sinon" "*"
"@types/sinon@*":
- version "10.0.16"
- resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.16.tgz#4bf10313bd9aa8eef1e50ec9f4decd3dd455b4d3"
- integrity sha512-j2Du5SYpXZjJVJtXBokASpPRj+e2z+VUhCPHmM6WMfe3dpHu6iVKJMU6AiBcMp/XTAYnEj6Wc1trJUWwZ0QaAQ==
+ version "17.0.2"
+ resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-17.0.2.tgz#9a769f67e62b45b7233f1fe01cb1f231d2393e1c"
+ integrity sha512-Zt6heIGsdqERkxctIpvN5Pv3edgBrhoeb3yHyxffd4InN0AX2SVNKSrhdDZKGQICVOxWP/q4DyhpfPNMSrpIiA==
dependencies:
"@types/sinonjs__fake-timers" "*"
"@types/sinonjs__fake-timers@*":
- version "8.1.2"
- resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz#bf2e02a3dbd4aecaf95942ecd99b7402e03fad5e"
- integrity sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==
+ version "8.1.5"
+ resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz#5fd3592ff10c1e9695d377020c033116cc2889f2"
+ integrity sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==
"@types/trusted-types@^2.0.2":
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.4.tgz#2b38784cd16957d3782e8e2b31c03bc1d13b4d65"
- integrity sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==
+ version "2.0.7"
+ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
+ integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
"@types/ws@^7.4.0":
version "7.4.7"
@@ -1018,9 +1005,9 @@
"@types/node" "*"
"@types/yauzl@^2.9.1":
- version "2.10.1"
- resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.1.tgz#4e8f299f0934d60f36c74f59cb5a8483fd786691"
- integrity sha512-CHzgNU3qYBnp/O4S3yv2tXPlvMTq0YWSTVg2/JYLqWZGHwwgJGAwd00poay/11asPq8wLFwHzubyInqHIFmmiw==
+ version "2.10.3"
+ resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999"
+ integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==
dependencies:
"@types/node" "*"
@@ -1031,10 +1018,10 @@
dependencies:
errorstacks "^2.2.0"
-"@web/browser-logs@^0.3.2":
- version "0.3.3"
- resolved "https://registry.yarnpkg.com/@web/browser-logs/-/browser-logs-0.3.3.tgz#121e5b662db2707c4b8cd1628d86903f059f5031"
- integrity sha512-wt8arj0x7ghXbnipgCvLR+xQ90cFg16ae23cFbInCrJvAxvyI22bAtT24W4XOXMPXwWLBVUJwBgBcXo3oKIvDw==
+"@web/browser-logs@^0.4.0":
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/@web/browser-logs/-/browser-logs-0.4.0.tgz#8c4adddac46be02dff1a605312132053b3737d0a"
+ integrity sha512-/EBiDAUCJ2DzZhaFxTPRIznEPeafdLbXShIL6aTu7x73x7ZoxSDv7DGuTsh2rWNMUa4+AKli4UORrpyv6QBOiA==
dependencies:
errorstacks "^2.2.0"
@@ -1069,14 +1056,14 @@
picomatch "^2.2.2"
ws "^7.4.2"
-"@web/dev-server-core@^0.5.1":
- version "0.5.2"
- resolved "https://registry.yarnpkg.com/@web/dev-server-core/-/dev-server-core-0.5.2.tgz#27fe5448e587a87272b556b44ce84c6453655cdb"
- integrity sha512-7YjWmwzM+K5fPvBCXldUIMTK4EnEufi1aWQWinQE81oW1CqzEwmyUNCtnWV9fcPA4kJC4qrpcjWNGF4YDWxuSg==
+"@web/dev-server-core@^0.7.0":
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/@web/dev-server-core/-/dev-server-core-0.7.0.tgz#ffe71dd272ecb73a2b0c1ee23f3fad812780b998"
+ integrity sha512-1FJe6cJ3r0x0ZmxY/FnXVduQD4lKX7QgYhyS6N+VmIpV+tBU4sGRbcrmeoYeY+nlnPa6p2oNuonk3X5ln/W95g==
dependencies:
"@types/koa" "^2.11.6"
"@types/ws" "^7.4.0"
- "@web/parse5-utils" "^2.0.0"
+ "@web/parse5-utils" "^2.1.0"
chokidar "^3.4.3"
clone "^2.1.2"
es-module-lexer "^1.0.0"
@@ -1144,10 +1131,10 @@
"@types/parse5" "^6.0.1"
parse5 "^6.0.1"
-"@web/parse5-utils@^2.0.0":
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/@web/parse5-utils/-/parse5-utils-2.0.1.tgz#11b91417165a838954dcf228383cfd8e1bdaf914"
- integrity sha512-FQI72BU5CXhpp7gLRskOQGGCcwvagLZnMnDwAfjrxo3pm1KOQzr8Vl+438IGpHV62xvjNdF1pjXwXcf7eekWGw==
+"@web/parse5-utils@^2.1.0":
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/@web/parse5-utils/-/parse5-utils-2.1.0.tgz#3d33aca62c66773492f2fba89d23a45f8b57ba4a"
+ integrity sha512-GzfK5disEJ6wEjoPwx8AVNwUe9gYIiwc+x//QYxYDAFKUp4Xb1OJAGLc2l2gVrSQmtPGLKrTRcW90Hv4pEq1qA==
dependencies:
"@types/parse5" "^6.0.1"
parse5 "^6.0.1"
@@ -1170,12 +1157,12 @@
"@web/test-runner-core" "^0.10.29"
mkdirp "^1.0.4"
-"@web/test-runner-commands@^0.7.0":
- version "0.7.0"
- resolved "https://registry.yarnpkg.com/@web/test-runner-commands/-/test-runner-commands-0.7.0.tgz#c9693e4e8b05ef06a2102e03ac924bcbf7985312"
- integrity sha512-3aXeGrkynOdJ5jgZu5ZslcWmWuPVY9/HNdWDUqPyNePG08PKmLV9Ij342ODDL6OVsxF5dvYn1312PhDqu5AQNw==
+"@web/test-runner-commands@^0.9.0":
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/@web/test-runner-commands/-/test-runner-commands-0.9.0.tgz#ed15a021249948204bb27559eb437ff6ceeee067"
+ integrity sha512-zeLI6QdH0jzzJMDV5O42Pd8WLJtYqovgdt0JdytgHc0d1EpzXDsc7NTCJSImboc2NcayIsWAvvGGeRF69SMMYg==
dependencies:
- "@web/test-runner-core" "^0.11.0"
+ "@web/test-runner-core" "^0.13.0"
mkdirp "^1.0.4"
"@web/test-runner-core@^0.10.20", "@web/test-runner-core@^0.10.29":
@@ -1210,10 +1197,10 @@
picomatch "^2.2.2"
source-map "^0.7.3"
-"@web/test-runner-core@^0.11.0":
- version "0.11.4"
- resolved "https://registry.yarnpkg.com/@web/test-runner-core/-/test-runner-core-0.11.4.tgz#590994c3fc69337e2c5411bf11c293dd061cc07a"
- integrity sha512-E7BsKAP8FAAEsfj4viASjmuaYfOw4UlKP9IFqo4W20eVyd1nbUWU3Amq4Jksh7W/j811qh3VaFNjDfCwklQXMg==
+"@web/test-runner-core@^0.13.0":
+ version "0.13.0"
+ resolved "https://registry.yarnpkg.com/@web/test-runner-core/-/test-runner-core-0.13.0.tgz#a3799461002fcb969b0baa100d88be6c1ff504f4"
+ integrity sha512-mUrETPg9n4dHWEk+D46BU3xVhQf+ljT4cG7FSpmF7AIOsXWgWHoaXp6ReeVcEmM5fmznXec2O/apTb9hpGrP3w==
dependencies:
"@babel/code-frame" "^7.12.11"
"@types/babel__code-frame" "^7.0.2"
@@ -1222,8 +1209,8 @@
"@types/debounce" "^1.2.0"
"@types/istanbul-lib-coverage" "^2.0.3"
"@types/istanbul-reports" "^3.0.0"
- "@web/browser-logs" "^0.3.2"
- "@web/dev-server-core" "^0.5.1"
+ "@web/browser-logs" "^0.4.0"
+ "@web/dev-server-core" "^0.7.0"
chokidar "^3.4.3"
cli-cursor "^3.1.0"
co-body "^6.1.0"
@@ -1364,9 +1351,9 @@ async@^2.6.4:
lodash "^4.17.14"
axe-core@^4.3.3:
- version "4.8.2"
- resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.8.2.tgz#2f6f3cde40935825cf4465e3c1c9e77b240ff6ae"
- integrity sha512-/dlp0fxyM3R8YW7MFzaHWXrf4zzbr0vaYb23VBFCl83R7nWNPg/yaQw2Dc8jzCMmDVLhSdzH8MjrsuIUuvX+6g==
+ version "4.8.3"
+ resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.8.3.tgz#205df863dd9917d5979e9435dab4d47692759051"
+ integrity sha512-d5ZQHPSPkF9Tw+yfyDcRoUOc4g/8UloJJe5J8m4L5+c7AtDdjDLRxew/knnI4CxvtdxEUVgWz4x3OIQUIFiMfw==
base64-js@^1.3.1:
version "1.5.1"
@@ -1426,12 +1413,13 @@ cache-content-type@^1.0.0:
ylru "^1.2.0"
call-bind@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
- integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513"
+ integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==
dependencies:
- function-bind "^1.1.1"
- get-intrinsic "^1.0.2"
+ function-bind "^1.1.2"
+ get-intrinsic "^1.2.1"
+ set-function-length "^1.1.1"
camelcase@^6.2.0:
version "6.3.0"
@@ -1598,20 +1586,15 @@ content-type@^1.0.4:
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
-convert-source-map@^1.6.0:
- version "1.9.0"
- resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
- integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
-
convert-source-map@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
-cookies@~0.8.0:
- version "0.8.0"
- resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90"
- integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==
+cookies@~0.9.0:
+ version "0.9.1"
+ resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.9.1.tgz#3ffed6f60bb4fb5f146feeedba50acc418af67e3"
+ integrity sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==
dependencies:
depd "~2.0.0"
keygrip "~1.1.0"
@@ -1664,6 +1647,15 @@ deepmerge@^4.2.2:
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
+define-data-property@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3"
+ integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==
+ dependencies:
+ get-intrinsic "^1.2.1"
+ gopd "^1.0.1"
+ has-property-descriptors "^1.0.0"
+
define-lazy-prop@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
@@ -1734,14 +1726,14 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1:
once "^1.4.0"
errorstacks@^2.2.0:
- version "2.4.0"
- resolved "https://registry.yarnpkg.com/errorstacks/-/errorstacks-2.4.0.tgz#2155674dd9e741aacda3f3b8b967d9c40a4a0baf"
- integrity sha512-5ecWhU5gt0a5G05nmQcgCxP5HperSMxLDzvWlT5U+ZSKkuDK0rJ3dbCQny6/vSCIXjwrhwSecXBbw1alr295hQ==
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/errorstacks/-/errorstacks-2.4.1.tgz#05adf6de1f5b04a66f2c12cc0593e1be2b18cd0f"
+ integrity sha512-jE4i0SMYevwu/xxAuzhly/KTwtj0xDhbzB6m1xPImxTkw8wcCbgarOQPfCVMi5JKVyW7in29pNJCCJrry3Ynnw==
es-module-lexer@^1.0.0:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.1.tgz#c1b0dd5ada807a3b3155315911f364dc4e909db1"
- integrity sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.4.1.tgz#41ea21b43908fe6a287ffcbe4300f790555331f5"
+ integrity sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==
"esbuild@^0.16 || ^0.17":
version "0.17.19"
@@ -1813,9 +1805,9 @@ extract-zip@2.0.1:
"@types/yauzl" "^2.9.1"
fast-glob@^3.2.9:
- version "3.3.1"
- resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4"
- integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
+ integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
@@ -1824,9 +1816,9 @@ fast-glob@^3.2.9:
micromatch "^4.0.4"
fastq@^1.6.0:
- version "1.15.0"
- resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
- integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==
+ version "1.16.0"
+ resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.16.0.tgz#83b9a9375692db77a822df081edb6a9cf6839320"
+ integrity sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==
dependencies:
reusify "^1.0.4"
@@ -1866,25 +1858,25 @@ fsevents@~2.3.2:
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
-function-bind@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
- integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+function-bind@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+ integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
get-caller-file@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
-get-intrinsic@^1.0.2:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82"
- integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==
+get-intrinsic@^1.0.2, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b"
+ integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==
dependencies:
- function-bind "^1.1.1"
- has "^1.0.3"
+ function-bind "^1.1.2"
has-proto "^1.0.1"
has-symbols "^1.0.3"
+ hasown "^2.0.0"
get-stream@^5.1.0:
version "5.2.0"
@@ -1917,6 +1909,13 @@ globby@^11.0.1:
merge2 "^1.4.1"
slash "^3.0.0"
+gopd@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
+ integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
+ dependencies:
+ get-intrinsic "^1.1.3"
+
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@@ -1927,6 +1926,13 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+has-property-descriptors@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340"
+ integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==
+ dependencies:
+ get-intrinsic "^1.2.2"
+
has-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0"
@@ -1944,12 +1950,12 @@ has-tostringtag@^1.0.0:
dependencies:
has-symbols "^1.0.2"
-has@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
- integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+hasown@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c"
+ integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==
dependencies:
- function-bind "^1.1.1"
+ function-bind "^1.1.2"
html-escaper@^2.0.0:
version "2.0.2"
@@ -2017,14 +2023,14 @@ ieee754@^1.1.13:
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
ignore@^5.2.0:
- version "5.2.4"
- resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
- integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78"
+ integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==
inflation@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/inflation/-/inflation-2.0.0.tgz#8b417e47c28f925a45133d914ca1fd389107f30f"
- integrity sha512-m3xv4hJYR2oXw4o4Y5l6P5P16WYmazYof+el6Al3f+YlggGj6qT9kImBAnzDelRALnP5d3h4jGBPKzYCizjZZw==
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/inflation/-/inflation-2.1.0.tgz#9214db11a47e6f756d111c4f9df96971c60f886c"
+ integrity sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==
inherits@2.0.3:
version "2.0.3"
@@ -2056,11 +2062,11 @@ is-builtin-module@^3.1.0:
builtin-modules "^3.3.0"
is-core-module@^2.13.0:
- version "2.13.0"
- resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db"
- integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==
+ version "2.13.1"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384"
+ integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==
dependencies:
- has "^1.0.3"
+ hasown "^2.0.0"
is-docker@^2.0.0, is-docker@^2.1.1:
version "2.2.1"
@@ -2124,9 +2130,9 @@ isbinaryfile@^5.0.0:
integrity sha512-UDdnyGvMajJUWCkib7Cei/dvyJrrvo4FIrsvSFWdPpXSUorzXrDJ0S+X5Q4ZlasfPjca4yqCNNsjbCeiy8FFeg==
istanbul-lib-coverage@^3.0.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3"
- integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756"
+ integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==
istanbul-lib-report@^3.0.0, istanbul-lib-report@^3.0.1:
version "3.0.1"
@@ -2200,15 +2206,15 @@ koa-static@^5.0.0:
koa-send "^5.0.0"
koa@^2.13.0:
- version "2.14.2"
- resolved "https://registry.yarnpkg.com/koa/-/koa-2.14.2.tgz#a57f925c03931c2b4d94b19d2ebf76d3244863fc"
- integrity sha512-VFI2bpJaodz6P7x2uyLiX6RLYpZmOJqNmoCst/Yyd7hQlszyPwG/I9CQJ63nOtKSxpt5M7NH67V6nJL2BwCl7g==
+ version "2.15.0"
+ resolved "https://registry.yarnpkg.com/koa/-/koa-2.15.0.tgz#d24ae1b0ff378bf12eb3df584ab4204e4c12ac2b"
+ integrity sha512-KEL/vU1knsoUvfP4MC4/GthpQrY/p6dzwaaGI6Rt4NQuFqkw3qrvsdYF5pz3wOfi7IGTvMPHC9aZIcUKYFNxsw==
dependencies:
accepts "^1.3.5"
cache-content-type "^1.0.0"
content-disposition "~0.5.2"
content-type "^1.0.4"
- cookies "~0.8.0"
+ cookies "~0.9.0"
debug "^4.3.2"
delegates "^1.0.0"
depd "^2.0.0"
@@ -2236,55 +2242,30 @@ lighthouse-logger@^1.0.0:
debug "^2.6.9"
marky "^1.2.2"
-lit-element@^3.3.0:
- version "3.3.3"
- resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.3.3.tgz#10bc19702b96ef5416cf7a70177255bfb17b3209"
- integrity sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==
- dependencies:
- "@lit-labs/ssr-dom-shim" "^1.1.0"
- "@lit/reactive-element" "^1.3.0"
- lit-html "^2.8.0"
-
lit-element@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-4.0.0.tgz#8343891bc9159a5fcb7f534914b37f2c0161e036"
- integrity sha512-N6+f7XgusURHl69DUZU6sTBGlIN+9Ixfs3ykkNDfgfTkDYGGOWwHAYBhDqVswnFGyWgQYR2KiSpu4J76Kccs/A==
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-4.0.2.tgz#1a519896d5ab7c7be7a8729f400499e38779c093"
+ integrity sha512-/W6WQZUa5VEXwC7H9tbtDMdSs9aWil3Ou8hU6z2cOKWbsm/tXPAcsoaHVEtrDo0zcOIE5GF6QgU55tlGL2Nihg==
dependencies:
- "@lit-labs/ssr-dom-shim" "^1.1.2-pre.0"
+ "@lit-labs/ssr-dom-shim" "^1.1.2"
"@lit/reactive-element" "^2.0.0"
- lit-html "^3.0.0"
+ lit-html "^3.1.0"
-lit-html@^2.0.0, lit-html@^2.8.0:
- version "2.8.0"
- resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.8.0.tgz#96456a4bb4ee717b9a7d2f94562a16509d39bffa"
- integrity sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==
- dependencies:
- "@types/trusted-types" "^2.0.2"
-
-lit-html@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-3.0.0.tgz#77d6776ee488642c74c5575315ef81aa09d24ea9"
- integrity sha512-DNJIE8dNY0dQF2Gs0sdMNUppMQT2/CvV4OVnSdg7BXAsGqkVwsE5bqQ04POfkYH5dBIuGnJYdFz5fYYyNnOxiA==
+"lit-html@^2.0.0 || ^3.0.0", lit-html@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-3.1.0.tgz#a7b93dd682073f2e2029656f4e9cd91e8034c196"
+ integrity sha512-FwAjq3iNsaO6SOZXEIpeROlJLUlrbyMkn4iuv4f4u1H40Jw8wkeR/OUXZUHUoiYabGk8Y4Y0F/rgq+R4MrOLmA==
dependencies:
"@types/trusted-types" "^2.0.2"
-lit@^2.0.0:
- version "2.8.0"
- resolved "https://registry.yarnpkg.com/lit/-/lit-2.8.0.tgz#4d838ae03059bf9cafa06e5c61d8acc0081e974e"
- integrity sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==
- dependencies:
- "@lit/reactive-element" "^1.6.0"
- lit-element "^3.3.0"
- lit-html "^2.8.0"
-
-lit@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/lit/-/lit-3.0.0.tgz#204bd65935892a73670471e893ee8ca55d2f9a3b"
- integrity sha512-nQ0teRzU1Kdj++VdmttS2WvIen8M79wChJ6guRKIIym2M3Ansg3Adj9O6yuQh2IpjxiUXlNuS81WKlQ4iL3BmA==
+"lit@^2.0.0 || ^3.0.0", lit@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/lit/-/lit-3.1.0.tgz#76429b85dc1f5169fed499a0f7e89e2e619010c9"
+ integrity sha512-rzo/hmUqX8zmOdamDAeydfjsGXbbdtAFqMhmocnh2j9aDYqbu0fjXygjCa0T99Od9VQ/2itwaGrjZz/ZELVl7w==
dependencies:
"@lit/reactive-element" "^2.0.0"
lit-element "^4.0.0"
- lit-html "^3.0.0"
+ lit-html "^3.1.0"
lodash.assignwith@^4.2.0:
version "4.2.0"
@@ -2423,9 +2404,9 @@ nanocolors@^0.2.1:
integrity sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==
nanoid@^3.1.25:
- version "3.3.6"
- resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
- integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
+ version "3.3.7"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
+ integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
negotiator@0.6.3:
version "0.6.3"
@@ -2433,9 +2414,9 @@ negotiator@0.6.3:
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
nise@^5.1.1:
- version "5.1.4"
- resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.4.tgz#491ce7e7307d4ec546f5a659b2efe94a18b4bbc0"
- integrity sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==
+ version "5.1.5"
+ resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.5.tgz#f2aef9536280b6c18940e32ba1fbdc770b8964ee"
+ integrity sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==
dependencies:
"@sinonjs/commons" "^2.0.0"
"@sinonjs/fake-timers" "^10.0.2"
@@ -2456,9 +2437,9 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
object-inspect@^1.9.0:
- version "1.12.3"
- resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
- integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2"
+ integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==
on-finished@^2.3.0:
version "2.4.1"
@@ -2565,9 +2546,9 @@ pump@^3.0.0:
once "^1.3.1"
punycode@^2.1.1:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
- integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
+ integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
puppeteer-core@^19.8.1:
version "19.11.1"
@@ -2638,9 +2619,9 @@ resolve-path@^1.4.0:
path-is-absolute "1.0.1"
resolve@^1.19.0:
- version "1.22.6"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.6.tgz#dd209739eca3aef739c626fea1b4f3c506195362"
- integrity sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==
+ version "1.22.8"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
+ integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
dependencies:
is-core-module "^2.13.0"
path-parse "^1.0.7"
@@ -2697,6 +2678,16 @@ semver@^7.3.4, semver@^7.5.3:
dependencies:
lru-cache "^6.0.0"
+set-function-length@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed"
+ integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==
+ dependencies:
+ define-data-property "^1.1.1"
+ get-intrinsic "^1.2.1"
+ gopd "^1.0.1"
+ has-property-descriptors "^1.0.0"
+
setprototypeof@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
@@ -2916,9 +2907,9 @@ typical@^7.1.1:
integrity sha512-T+tKVNs6Wu7IWiAce5BgMd7OZfNYUndHwc5MknN+UHOudi7sGZzuHdCadllRuqJ3fPtgFtIH9+lt9qRv6lmpfA==
ua-parser-js@^1.0.33:
- version "1.0.36"
- resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.36.tgz#a9ab6b9bd3a8efb90bb0816674b412717b7c428c"
- integrity sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw==
+ version "1.0.37"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.37.tgz#b5dc7b163a5c1f0c510b08446aed4da92c46373f"
+ integrity sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==
unbzip2-stream@1.4.3:
version "1.4.3"
@@ -2928,6 +2919,11 @@ unbzip2-stream@1.4.3:
buffer "^5.2.1"
through "^2.3.8"
+undici-types@~5.26.4:
+ version "5.26.5"
+ resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
+ integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
+
unpipe@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
@@ -2939,13 +2935,13 @@ util-deprecate@^1.0.1:
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
v8-to-istanbul@^9.0.1:
- version "9.1.0"
- resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz#1b83ed4e397f58c85c266a570fc2558b5feb9265"
- integrity sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==
+ version "9.2.0"
+ resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz#2ed7644a245cddd83d4e087b9b33b3e62dfd10ad"
+ integrity sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==
dependencies:
"@jridgewell/trace-mapping" "^0.3.12"
"@types/istanbul-lib-coverage" "^2.0.1"
- convert-source-map "^1.6.0"
+ convert-source-map "^2.0.0"
vary@^1.1.2:
version "1.1.2"
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index b8c5666e26..a9d10bef5d 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -29,8 +29,7 @@ probably easiest since you will have npm as part of Nodejs.
## Installing [Node.js](https://nodejs.org/en/download/) and npm packages
-The minimum nodejs version supported is 10.x+. We recommend at least the latest
-LTS (v16 as of October 2022).
+At the time of writing (November 2023) you should use version 18 of nodejs.
```sh
# Debian experimental
@@ -38,7 +37,7 @@ sudo apt-get install nodejs
sudo apt-get install npm
# OS X with Homebrew
-brew install node@16
+brew install node@18
brew install npm
```
@@ -57,17 +56,8 @@ For first time users to get the local server up, `bazel build gerrit` should be
# Install yarn package manager
npm install -g yarn
-# Install packages from root-level packages.json
-bazel fetch @npm//:node_modules
-
-# Install packages from polygerrit-ui/app/packages.json
-bazel fetch @ui_npm//:node_modules
-
-# Install packages from polygerrit-ui/packages.json
-bazel fetch @ui_dev_npm//:node_modules
-
-# Install packages from tools/node_tools/packages.json
-bazel fetch @tools_npm//:node_modules
+# Install packages from all packages.json files
+yarn setup
```
More information for installing and using nodejs rules can be found here https://bazelbuild.github.io/rules_nodejs/install.html
diff --git a/polygerrit-ui/app/api/checks.ts b/polygerrit-ui/app/api/checks.ts
index f66c3739e6..914b099ca1 100644
--- a/polygerrit-ui/app/api/checks.ts
+++ b/polygerrit-ui/app/api/checks.ts
@@ -85,7 +85,8 @@ export declare interface FetchResponse {
actions?: Action[];
/**
- * Shown prominently in the change summary below the run chips.
+ * Shown prominently in the change summary below the run chips. Interpreted
+ * as markdown.
*/
summaryMessage?: string;
diff --git a/polygerrit-ui/app/api/plugin.ts b/polygerrit-ui/app/api/plugin.ts
index 684429a666..8630f75887 100644
--- a/polygerrit-ui/app/api/plugin.ts
+++ b/polygerrit-ui/app/api/plugin.ts
@@ -34,6 +34,7 @@ export enum EventType {
POST_REVERT = 'postrevert',
ADMIN_MENU_LINKS = 'admin-menu-links',
SHOW_DIFF = 'showdiff',
+ REPLY_SENT = 'replysent',
}
export declare interface PluginApi {
diff --git a/polygerrit-ui/app/api/rest-api.ts b/polygerrit-ui/app/api/rest-api.ts
index 045aee5967..997b8fe8df 100644
--- a/polygerrit-ui/app/api/rest-api.ts
+++ b/polygerrit-ui/app/api/rest-api.ts
@@ -539,6 +539,7 @@ export interface CommentInfo {
commit_id?: string;
context_lines?: ContextLine[];
source_content_type?: string;
+ fix_suggestions?: FixSuggestionInfo[];
}
/**
@@ -658,6 +659,7 @@ export declare interface DownloadInfo {
*/
export declare interface DownloadSchemeInfo {
url: string;
+ description?: string;
is_auth_required: boolean;
is_auth_supported: boolean;
commands: string;
@@ -1291,3 +1293,30 @@ export function isBase64FileContent(
// The URL encoded UUID of the comment
export type UrlEncodedCommentId = BrandType<string, '_urlEncodedCommentId'>;
+
+/**
+ * The FixSuggestionInfo entity represents a suggested fix
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#fix-suggestion-info
+ */
+export interface FixSuggestionInfoInput {
+ description: string;
+ replacements: FixReplacementInfo[];
+}
+
+/**
+ * The FixReplacementInfo entity describes how the content of a file should be replaced by another content
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#fix-replacement-info
+ */
+export interface FixReplacementInfo {
+ path: string;
+ range: CommentRange;
+ replacement: string;
+}
+// The UUID of the suggested fix.
+export type FixId = BrandType<string, '_fixId'>;
+
+export interface FixSuggestionInfo extends FixSuggestionInfoInput {
+ fix_id: FixId;
+ description: string;
+ replacements: FixReplacementInfo[];
+}
diff --git a/polygerrit-ui/app/api/suggestions.ts b/polygerrit-ui/app/api/suggestions.ts
index 5dedad4f1f..b4158be8fe 100644
--- a/polygerrit-ui/app/api/suggestions.ts
+++ b/polygerrit-ui/app/api/suggestions.ts
@@ -4,7 +4,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import {CommentRange, NumericChangeId, RevisionPatchSetNum} from './rest-api';
+import {
+ ChangeInfo,
+ CommentRange,
+ RevisionPatchSetNum,
+ FixSuggestionInfo,
+} from './rest-api';
export declare interface SuggestionsPluginApi {
/**
@@ -15,7 +20,7 @@ export declare interface SuggestionsPluginApi {
export declare interface SuggestCodeRequest {
prompt: string;
- changeNumber: NumericChangeId;
+ changeInfo: ChangeInfo;
patchsetNumber: RevisionPatchSetNum;
filePath: string;
range?: CommentRange;
@@ -24,10 +29,30 @@ export declare interface SuggestCodeRequest {
export declare interface SuggestionsProvider {
/**
- * Gerrit calls this method when ...
+ * Gerrit calls these methods when ...
* - ... user types a comment draft
*/
- suggestCode(commentData: SuggestCodeRequest): Promise<SuggestCodeResponse>;
+ suggestCode?(commentData: SuggestCodeRequest): Promise<SuggestCodeResponse>;
+ suggestFix?(commentData: SuggestCodeRequest): Promise<SuggestedFixResponse>;
+ /**
+ * Gets the title to display on the fix suggestion preview.
+ *
+ * @param fix_suggestions A list of suggested fixes.
+ * @return The title string or empty to use the default title.
+ */
+ getFixSuggestionTitle?(fix_suggestions?: FixSuggestionInfo[]): string;
+ /**
+ * Gets a link to documentation for icon help next to title
+ *
+ * @param fix_suggestions A list of suggested fixes.
+ * @return The documentation URL string or empty to use the default link to
+ * gerrit documentation about fix suggestions.
+ */
+ getDocumentationLink?(fix_suggestions?: FixSuggestionInfo[]): string;
+ /**
+ * List of supported file extensions. If undefined, all file extensions supported.
+ */
+ supportedFileExtensions?: string[];
}
export declare interface SuggestCodeResponse {
@@ -35,6 +60,11 @@ export declare interface SuggestCodeResponse {
suggestions: Suggestion[];
}
+export declare interface SuggestedFixResponse {
+ responseCode: ResponseCode;
+ fix_suggestions: FixSuggestionInfo[];
+}
+
export declare interface Suggestion {
replacement: string;
newRange?: CommentRange;
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
index 24fb5c0d24..0fa58f4e53 100644
--- a/polygerrit-ui/app/constants/constants.ts
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -263,6 +263,7 @@ export function createDefaultPreferences(): PreferencesInfo {
email_strategy: EmailStrategy.ATTENTION_SET_ONLY,
default_base_for_merges: DefaultBase.AUTO_MERGE,
allow_browser_notifications: false,
+ allow_suggest_code_while_commenting: true,
diff_page_sidebar: 'NONE',
};
}
diff --git a/polygerrit-ui/app/constants/reporting.ts b/polygerrit-ui/app/constants/reporting.ts
index f57afca197..37a17bab82 100644
--- a/polygerrit-ui/app/constants/reporting.ts
+++ b/polygerrit-ui/app/constants/reporting.ts
@@ -146,4 +146,14 @@ export enum Interaction {
GENERATE_SUGGESTION_ENABLED = 'generate_suggestion_enabled',
// User disabled generating suggestions
GENERATE_SUGGESTION_DISABLED = 'generate_suggestion_disabled',
+ GENERATE_SUGGESTION_EDITED = 'generate_suggestion_edited',
+ START_REVIEW = 'start-review',
+ CODE_REVIEW_APPROVAL = 'code-review-approval',
+ FILE_LIST_DIFF_COLLAPSED = 'file-list-diff-collapsed',
+ FILE_LIST_DIFF_EXPANDED = 'file-list-diff-expanded',
+ FILE_LIST_ALL_DIFFS_COLLAPSED = 'file-list-all-diffs-collapsed',
+ FILE_LIST_ALL_DIFFS_EXPANDED = 'file-list-all-diffs-expanded',
+ // The very first reporting event with `ChangeId` set when visiting a change
+ // related page. Can be used as a starting point for user journeys.
+ CHANGE_ID_CHANGED = 'change-id-changed',
}
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
index 8393d81192..077ac94cbc 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
@@ -103,10 +103,27 @@ export class GrCreateRepoDialog extends LitElement {
:host {
display: inline-block;
}
+ div.title-flex,
+ div.value-flex {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
input {
width: 20em;
+ box-sizing: border-box;
+ }
+ div.gr-form-styles section {
+ margin: var(--spacing-m) 0;
+ }
+ div.gr-form-styles span.title {
+ width: 13em;
+ }
+ section .title gr-icon {
+ vertical-align: top;
}
- gr-autocomplete {
+ section .value gr-autocomplete {
+ display: block;
width: 20em;
}
`,
@@ -118,7 +135,9 @@ export class GrCreateRepoDialog extends LitElement {
<div class="gr-form-styles">
<div id="form">
<section>
- <span class="title">Repository name</span>
+ <div class="title-flex">
+ <span class="title">Repository Name</span>
+ </div>
<iron-input
.bindValue=${convertToString(this.repoConfig.name)}
@bind-value-changed=${this.handleNameBindValueChanged}
@@ -126,8 +145,10 @@ export class GrCreateRepoDialog extends LitElement {
<input id="repoNameInput" autocomplete="on" />
</iron-input>
</section>
- <section>
- <span class="title">Default Branch</span>
+ <section ?hidden=${!!this.repoConfig.permissions_only}>
+ <div class="title-flex">
+ <span class="title">Default Branch</span>
+ </div>
<span class="value">
<gr-autocomplete
id="defaultBranchNameInput"
@@ -139,7 +160,16 @@ export class GrCreateRepoDialog extends LitElement {
</span>
</section>
<section>
- <span class="title">Rights inherit from</span>
+ <div class="title-flex">
+ <span class="title">
+ <gr-tooltip-content
+ has-tooltip
+ title="For inheriting access rights and repository configuration"
+ >
+ Parent Repository <gr-icon icon="info"></gr-icon>
+ </gr-tooltip-content>
+ </span>
+ </div>
<span class="value">
<gr-autocomplete
id="rightsInheritFromInput"
@@ -152,13 +182,23 @@ export class GrCreateRepoDialog extends LitElement {
</span>
</section>
<section>
- <span class="title">Owner</span>
+ <div class="title-flex">
+ <span class="title">
+ <gr-tooltip-content
+ has-tooltip
+ title="When the project is created, the 'Owner' access right is automatically assigned to this group."
+ >
+ Owner Group <gr-icon icon="info"></gr-icon>
+ </gr-tooltip-content>
+ </span>
+ </div>
<span class="value">
<gr-autocomplete
id="ownerInput"
.text=${convertToString(this.repoOwner)}
.value=${convertToString(this.repoOwnerId)}
.query=${this.queryGroups}
+ .placeholder=${'Optional'}
@text-changed=${this.handleOwnerTextChanged}
@value-changed=${this.handleOwnerValueChanged}
>
@@ -166,38 +206,61 @@ export class GrCreateRepoDialog extends LitElement {
</span>
</section>
<section>
- <span class="title">Create initial empty commit</span>
- <span class="value">
- <gr-select
- id="initialCommit"
- .bindValue=${this.repoConfig.create_empty_commit}
- @bind-value-changed=${this
- .handleCreateEmptyCommitBindValueChanged}
- >
- <select>
- <option value="false">False</option>
- <option value="true">True</option>
- </select>
- </gr-select>
- </span>
+ <div class="title-flex">
+ <span class="title">
+ <gr-tooltip-content
+ has-tooltip
+ title="Choose 'false', if you want to import an existing repo, 'true' otherwise."
+ >
+ Create Empty Commit <gr-icon icon="info"></gr-icon>
+ </gr-tooltip-content>
+ </span>
+ </div>
+ <div class="value-flex">
+ <span class="value">
+ <gr-select
+ id="initialCommit"
+ .bindValue=${this.repoConfig.create_empty_commit}
+ @bind-value-changed=${this
+ .handleCreateEmptyCommitBindValueChanged}
+ >
+ <select>
+ <option value="false">False</option>
+ <option value="true">True</option>
+ </select>
+ </gr-select>
+ </span>
+ </div>
</section>
<section>
- <span class="title"
- >Only serve as parent for other repositories</span
- >
- <span class="value">
- <gr-select
- id="parentRepo"
- .bindValue=${this.repoConfig.permissions_only}
- @bind-value-changed=${this
- .handlePermissionsOnlyBindValueChanged}
- >
- <select>
- <option value="false">False</option>
- <option value="true">True</option>
- </select>
- </gr-select>
- </span>
+ <div class="title-flex">
+ <span class="title">
+ <gr-tooltip-content
+ has-tooltip
+ title="Only serve as a parent repository for other repositories
+to inheright access rights and configs.
+If 'true', then you cannot push code to this repo.
+It will only have a 'refs/meta/config' branch."
+ >
+ Parent Repo Only <gr-icon icon="info"></gr-icon>
+ </gr-tooltip-content>
+ </span>
+ </div>
+ <div class="value-flex">
+ <span class="value">
+ <gr-select
+ id="parentRepo"
+ .bindValue=${this.repoConfig.permissions_only}
+ @bind-value-changed=${this
+ .handlePermissionsOnlyBindValueChanged}
+ >
+ <select>
+ <option value="false">False</option>
+ <option value="true">True</option>
+ </select>
+ </gr-select>
+ </span>
+ </div>
</section>
</div>
</div>
@@ -252,8 +315,10 @@ export class GrCreateRepoDialog extends LitElement {
}
private handleRightsTextChanged(e: ValueChangedEvent) {
- this.repoConfig.parent = e.detail.value as RepoName;
- this.requestUpdate();
+ this.repoConfig = {
+ ...this.repoConfig,
+ parent: e.detail.value as RepoName,
+ };
}
private handleOwnerTextChanged(e: ValueChangedEvent) {
@@ -279,14 +344,18 @@ export class GrCreateRepoDialog extends LitElement {
}
private handleCreateEmptyCommitBindValueChanged(
- e: ValueChangedEvent<boolean>
+ e: ValueChangedEvent<string>
) {
- this.repoConfig.create_empty_commit = e.detail.value;
- this.requestUpdate();
+ this.repoConfig = {
+ ...this.repoConfig,
+ create_empty_commit: e.detail.value === 'true',
+ };
}
- private handlePermissionsOnlyBindValueChanged(e: ValueChangedEvent<boolean>) {
- this.repoConfig.permissions_only = e.detail.value;
- this.requestUpdate();
+ private handlePermissionsOnlyBindValueChanged(e: ValueChangedEvent<string>) {
+ this.repoConfig = {
+ ...this.repoConfig,
+ permissions_only: e.detail.value === 'true',
+ };
}
}
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.ts
index bc851a33cb..289cd03a58 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.ts
@@ -32,52 +32,101 @@ suite('gr-create-repo-dialog tests', () => {
<div class="gr-form-styles">
<div id="form">
<section>
- <span class="title"> Repository name </span>
+ <div class="title-flex">
+ <span class="title"> Repository Name </span>
+ </div>
<iron-input>
<input autocomplete="on" id="repoNameInput" />
</iron-input>
</section>
<section>
- <span class="title"> Default Branch </span>
+ <div class="title-flex">
+ <span class="title"> Default Branch </span>
+ </div>
<span class="value">
<gr-autocomplete id="defaultBranchNameInput"> </gr-autocomplete>
</span>
</section>
<section>
- <span class="title"> Rights inherit from </span>
+ <div class="title-flex">
+ <span class="title">
+ <gr-tooltip-content
+ has-tooltip=""
+ title="For inheriting access rights and repository configuration"
+ >
+ Parent Repository
+ <gr-icon icon="info"> </gr-icon>
+ </gr-tooltip-content>
+ </span>
+ </div>
<span class="value">
<gr-autocomplete id="rightsInheritFromInput"> </gr-autocomplete>
</span>
</section>
<section>
- <span class="title"> Owner </span>
+ <div class="title-flex">
+ <span class="title">
+ <gr-tooltip-content
+ has-tooltip=""
+ title="When the project is created, the 'Owner' access right is automatically assigned to this group."
+ >
+ Owner Group
+ <gr-icon icon="info"> </gr-icon>
+ </gr-tooltip-content>
+ </span>
+ </div>
<span class="value">
<gr-autocomplete id="ownerInput"> </gr-autocomplete>
</span>
</section>
<section>
- <span class="title"> Create initial empty commit </span>
- <span class="value">
- <gr-select id="initialCommit">
- <select>
- <option value="false">False</option>
- <option value="true">True</option>
- </select>
- </gr-select>
- </span>
+ <div class="title-flex">
+ <span class="title">
+ <gr-tooltip-content
+ has-tooltip=""
+ title="Choose 'false', if you want to import an existing repo, 'true' otherwise."
+ >
+ Create Empty Commit
+ <gr-icon icon="info"> </gr-icon>
+ </gr-tooltip-content>
+ </span>
+ </div>
+ <div class="value-flex">
+ <span class="value">
+ <gr-select id="initialCommit">
+ <select>
+ <option value="false">False</option>
+ <option value="true">True</option>
+ </select>
+ </gr-select>
+ </span>
+ </div>
</section>
<section>
- <span class="title">
- Only serve as parent for other repositories
- </span>
- <span class="value">
- <gr-select id="parentRepo">
- <select>
- <option value="false">False</option>
- <option value="true">True</option>
- </select>
- </gr-select>
- </span>
+ <div class="title-flex">
+ <span class="title">
+ <gr-tooltip-content
+ has-tooltip=""
+ title="Only serve as a parent repository for other repositories
+to inheright access rights and configs.
+If 'true', then you cannot push code to this repo.
+It will only have a 'refs/meta/config' branch."
+ >
+ Parent Repo Only
+ <gr-icon icon="info"> </gr-icon>
+ </gr-tooltip-content>
+ </span>
+ </div>
+ <div class="value-flex">
+ <span class="value">
+ <gr-select id="parentRepo">
+ <select>
+ <option value="false">False</option>
+ <option value="true">True</option>
+ </select>
+ </gr-select>
+ </span>
+ </div>
</section>
</div>
</div>
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts
index 7d6d17d869..0af614d3d9 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts
@@ -280,7 +280,7 @@ export class GrGroupMembers extends LitElement {
filterActive = false
) {
return this.restApiService
- .getSuggestedAccounts(
+ .queryAccounts(
input,
SUGGESTIONS_LIMIT,
canSee,
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts
index a3c7bbdd73..c16a108a06 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts
@@ -93,7 +93,7 @@ suite('gr-group-members tests', () => {
},
];
- stubRestApi('getSuggestedAccounts').callsFake(input => {
+ stubRestApi('queryAccounts').callsFake(input => {
if (input.startsWith('test')) {
return Promise.resolve([
{
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
index 2809d6ee95..615528ca96 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
@@ -695,7 +695,10 @@ export class GrRepoAccess extends LitElement {
return this.restApiService
.setRepoAccessRightsForReview(this.repo, obj)
.then(change => {
- this.getNavigation().setUrl(createChangeUrl({change}));
+ // Don't navigate on server error.
+ if (change) {
+ this.getNavigation().setUrl(createChangeUrl({change}));
+ }
})
.finally(() => {
this.modified = false;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
index 711794457e..9027753441 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
@@ -19,6 +19,7 @@ import {
ConfigInput,
MaxObjectSizeLimitInfo,
PluginParameterToConfigParameterInfoMap,
+ DownloadSchemeInfo,
} from '../../../types/common';
import {
InheritedBooleanInfoConfiguredValue,
@@ -224,13 +225,10 @@ export class GrRepo extends LitElement {
<fieldset>
<gr-download-commands
id="downloadCommands"
- .commands=${this.computeCommands(
- this.repo,
- this.schemesObj,
- this.selectedScheme
- )}
+ .commands=${this.computeCommands()}
.schemes=${this.schemes}
.selectedScheme=${this.selectedScheme}
+ .description=${this.computeDescription()}
@selected-scheme-changed=${(e: BindValueChangeEvent) => {
if (this.loading) return;
this.selectedScheme = e.detail.value;
@@ -1073,23 +1071,33 @@ export class GrRepo extends LitElement {
}
}
- private computeCommands(
- repo?: RepoName,
- schemesObj?: SchemesInfoMap,
- selectedScheme?: string
- ) {
- if (!schemesObj || !repo || !selectedScheme) return [];
- if (!hasOwnProperty(schemesObj, selectedScheme)) return [];
- const commandObj = schemesObj[selectedScheme].clone_commands;
+ private getSchemeInfo(): DownloadSchemeInfo | undefined {
+ if (!this.schemesObj || !this.repo || !this.selectedScheme) {
+ return undefined;
+ }
+ if (!hasOwnProperty(this.schemesObj, this.selectedScheme)) return undefined;
+ return this.schemesObj[this.selectedScheme];
+ }
+
+ private computeDescription() {
+ const schemeInfo = this.getSchemeInfo();
+ return schemeInfo?.description;
+ }
+
+ private computeCommands() {
+ const schemeInfo = this.getSchemeInfo();
+ if (!this.repo || !schemeInfo) return undefined;
+
+ const commandObj = schemeInfo.clone_commands ?? {};
const commands = [];
for (const [title, command] of Object.entries(commandObj)) {
commands.push({
title,
command: command
- .replace(/\${project}/gi, encodeURI(repo))
+ .replace(/\${project}/gi, encodeURI(this.repo))
.replace(
/\${project-base-name}/gi,
- encodeURI(repo.substring(repo.lastIndexOf('/') + 1))
+ encodeURI(this.repo.substring(this.repo.lastIndexOf('/') + 1))
),
});
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow_test.ts
index df7a6ceb6e..15f022b05b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow_test.ts
@@ -215,7 +215,7 @@ suite('gr-change-list-bulk-abandon-flow tests', () => {
`Status: ${ProgressStatus.RUNNING}`
);
- executeChangeAction.resolve({...new Response(), status: 200});
+ executeChangeAction.resolve(new Response());
await waitUntil(
() =>
element.progress.get(1 as NumericChangeId) === ProgressStatus.SUCCESSFUL
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts
index af997a9487..94ca74a2a6 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts
@@ -33,6 +33,7 @@ import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
import {GrButton} from '../../shared/gr-button/gr-button';
import './gr-change-list-hashtag-flow';
import {GrChangeListHashtagFlow} from './gr-change-list-hashtag-flow';
+import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
suite('gr-change-list-hashtag-flow tests', () => {
let element: GrChangeListHashtagFlow;
@@ -201,7 +202,7 @@ suite('gr-change-list-hashtag-flow tests', () => {
const promise = mockPromise<Hashtag[]>();
setChangeHashtagPromises.push(promise);
setChangeHashtagStub
- .withArgs(changes[i]._number, sinon.match.any)
+ .withArgs(changes[i]._number, sinon.match.any, sinon.match.any)
.returns(promise);
}
model = new BulkActionsModel(getAppContext().restApiService);
@@ -322,14 +323,17 @@ suite('gr-change-list-hashtag-flow tests', () => {
assert.deepEqual(setChangeHashtagStub.firstCall.args, [
changes[0]._number,
{add: ['hashtag1']},
+ throwingErrorCallback,
]);
assert.deepEqual(setChangeHashtagStub.secondCall.args, [
changes[1]._number,
{add: ['hashtag1']},
+ throwingErrorCallback,
]);
assert.deepEqual(setChangeHashtagStub.thirdCall.args, [
changes[2]._number,
{add: ['hashtag1']},
+ throwingErrorCallback,
]);
await waitUntilCalled(alertStub, 'alertStub');
assert.deepEqual(alertStub.lastCall.args[0].detail, {
@@ -346,34 +350,6 @@ suite('gr-change-list-hashtag-flow tests', () => {
);
});
- test('shows error when add hashtag fails', async () => {
- // selects "hashtag1"
- queryAll<HTMLButtonElement>(element, 'button.chip')[0].click();
- await element.updateComplete;
-
- queryAndAssert<GrButton>(element, '#add-hashtag-button').click();
- await element.updateComplete;
-
- assert.equal(
- queryAndAssert(element, '.loadingText').textContent,
- 'Adding hashtag...'
- );
-
- await rejectPromises();
- await element.updateComplete;
- await waitUntil(() => query(element, '.error') !== undefined);
-
- assert.equal(
- queryAndAssert(element, '.error').textContent,
- 'Failed to add'
- );
- assert.equal(
- queryAndAssert(element, 'gr-button#cancel-button').textContent,
- 'Cancel'
- );
- assert.isUndefined(query(element, '.loadingText'));
- });
-
test('add multiple hashtag from selected change', async () => {
const alertStub = sinon.stub();
element.addEventListener('show-alert', alertStub);
@@ -400,14 +376,17 @@ suite('gr-change-list-hashtag-flow tests', () => {
assert.deepEqual(setChangeHashtagStub.firstCall.args, [
changes[0]._number,
{add: ['hashtag1', 'hashtag2']},
+ throwingErrorCallback,
]);
assert.deepEqual(setChangeHashtagStub.secondCall.args, [
changes[1]._number,
{add: ['hashtag1', 'hashtag2']},
+ throwingErrorCallback,
]);
assert.deepEqual(setChangeHashtagStub.thirdCall.args, [
changes[2]._number,
{add: ['hashtag1', 'hashtag2']},
+ throwingErrorCallback,
]);
await waitUntilCalled(alertStub, 'alertStub');
@@ -453,14 +432,17 @@ suite('gr-change-list-hashtag-flow tests', () => {
assert.deepEqual(setChangeHashtagStub.firstCall.args, [
changes[0]._number,
{add: ['foo']},
+ throwingErrorCallback,
]);
assert.deepEqual(setChangeHashtagStub.secondCall.args, [
changes[1]._number,
{add: ['foo']},
+ throwingErrorCallback,
]);
assert.deepEqual(setChangeHashtagStub.thirdCall.args, [
changes[2]._number,
{add: ['foo']},
+ throwingErrorCallback,
]);
await waitUntilCalled(alertStub, 'alertStub');
@@ -515,14 +497,17 @@ suite('gr-change-list-hashtag-flow tests', () => {
assert.deepEqual(setChangeHashtagStub.firstCall.args, [
changes[0]._number,
{add: ['foo']},
+ throwingErrorCallback,
]);
assert.deepEqual(setChangeHashtagStub.secondCall.args, [
changes[1]._number,
{add: ['foo']},
+ throwingErrorCallback,
]);
assert.deepEqual(setChangeHashtagStub.thirdCall.args, [
changes[2]._number,
{add: ['foo']},
+ throwingErrorCallback,
]);
await waitUntilCalled(alertStub, 'alertStub');
@@ -568,6 +553,8 @@ suite('gr-change-list-hashtag-flow tests', () => {
'Adding hashtag...'
);
+ // Rest api doesn't reject on error by default, but it does in topic flow,
+ // because we specify a throwing callback.
await rejectPromises();
await element.updateComplete;
await waitUntil(() => query(element, '.error') !== undefined);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts
index 4a01412973..08d3218871 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts
@@ -370,7 +370,14 @@ export class GrChangeListTopicFlow extends LitElement {
change =>
change.topic && this.selectedExistingTopics.has(change.topic)
)
- .map(change => this.restApiService.setChangeTopic(change._number, '')),
+ .map(
+ change =>
+ // With throwing callback guaranteed to be non-null.
+ this.restApiService.removeChangeTopic(
+ change._number,
+ throwingErrorCallback
+ ) as Promise<string>
+ ),
`${this.selectedChanges[0].topic} removed from changes`,
'Failed to remove topic'
);
@@ -384,8 +391,14 @@ export class GrChangeListTopicFlow extends LitElement {
this.loadingText = 'Applying to all';
const topic = Array.from(this.selectedExistingTopics.values())[0];
this.trackPromises(
- this.selectedChanges.map(change =>
- this.restApiService.setChangeTopic(change._number, topic)
+ this.selectedChanges.map(
+ change =>
+ // With throwing callback guaranteed to be non-null.
+ this.restApiService.setChangeTopic(
+ change._number,
+ topic,
+ throwingErrorCallback
+ ) as Promise<string>
),
`${topic} applied to all changes`,
'Failed to apply topic'
@@ -403,8 +416,14 @@ export class GrChangeListTopicFlow extends LitElement {
)} added to ${this.topicToAdd}`;
this.loadingText = loadingText;
this.trackPromises(
- this.selectedChanges.map(change =>
- this.restApiService.setChangeTopic(change._number, this.topicToAdd)
+ this.selectedChanges.map(
+ change =>
+ // With throwing callback guaranteed to be non-null.
+ this.restApiService.setChangeTopic(
+ change._number,
+ this.topicToAdd,
+ throwingErrorCallback
+ ) as Promise<string>
),
alert,
'Failed to set topic'
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow_test.ts
index 489d8ee598..29347726b8 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow_test.ts
@@ -33,6 +33,7 @@ import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
import {GrButton} from '../../shared/gr-button/gr-button';
import './gr-change-list-topic-flow';
import {GrChangeListTopicFlow} from './gr-change-list-topic-flow';
+import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
suite('gr-change-list-topic-flow tests', () => {
let element: GrChangeListTopicFlow;
@@ -193,7 +194,11 @@ suite('gr-change-list-topic-flow tests', () => {
const promise = mockPromise<string>();
setChangeTopicPromises.push(promise);
setChangeTopicStub
- .withArgs(changesWithTopics[i]._number, sinon.match.any)
+ .withArgs(
+ changesWithTopics[i]._number,
+ sinon.match.any,
+ sinon.match.any
+ )
.returns(promise);
}
model = new BulkActionsModel(getAppContext().restApiService);
@@ -339,11 +344,12 @@ suite('gr-change-list-topic-flow tests', () => {
await resolvePromises();
await element.updateComplete;
- // not called for second change which as a different topic
+ // not called for second change which has a different topic
assert.isTrue(setChangeTopicStub.calledOnce);
assert.deepEqual(setChangeTopicStub.firstCall.args, [
changesWithTopics[0]._number,
'',
+ throwingErrorCallback,
]);
await waitUntilCalled(alertStub, 'alertStub');
@@ -372,15 +378,17 @@ suite('gr-change-list-topic-flow tests', () => {
await resolvePromises();
await element.updateComplete;
- // not called for second change which as a different topic
+ // also called for second change which has a different topic
assert.isTrue(setChangeTopicStub.calledTwice);
assert.deepEqual(setChangeTopicStub.firstCall.args, [
changesWithTopics[0]._number,
'',
+ throwingErrorCallback,
]);
assert.deepEqual(setChangeTopicStub.secondCall.args, [
changesWithTopics[1]._number,
'',
+ throwingErrorCallback,
]);
});
@@ -397,6 +405,8 @@ suite('gr-change-list-topic-flow tests', () => {
'Removing topic...'
);
+ // Rest api doesn't reject on error by default, but it does in topic flow,
+ // because we specify a throwing callback.
await rejectPromises();
await element.updateComplete;
@@ -454,10 +464,12 @@ suite('gr-change-list-topic-flow tests', () => {
assert.deepEqual(setChangeTopicStub.firstCall.args, [
changesWithTopics[0]._number,
'topic1',
+ throwingErrorCallback,
]);
assert.deepEqual(setChangeTopicStub.secondCall.args, [
changesWithTopics[1]._number,
'topic1',
+ throwingErrorCallback,
]);
await waitUntilCalled(alertStub, 'alertStub');
@@ -510,7 +522,11 @@ suite('gr-change-list-topic-flow tests', () => {
const promise = mockPromise<string>();
setChangeTopicPromises.push(promise);
setChangeTopicStub
- .withArgs(changesWithNoTopics[i]._number, sinon.match.any)
+ .withArgs(
+ changesWithNoTopics[i]._number,
+ sinon.match.any,
+ sinon.match.any
+ )
.returns(promise);
}
@@ -619,10 +635,12 @@ suite('gr-change-list-topic-flow tests', () => {
assert.deepEqual(setChangeTopicStub.firstCall.args, [
changesWithNoTopics[0]._number,
'foo',
+ throwingErrorCallback,
]);
assert.deepEqual(setChangeTopicStub.secondCall.args, [
changesWithNoTopics[1]._number,
'foo',
+ throwingErrorCallback,
]);
await waitUntilCalled(alertStub, 'alertStub');
@@ -661,6 +679,8 @@ suite('gr-change-list-topic-flow tests', () => {
'Setting topic...'
);
+ // Rest api doesn't reject on error by default, but it does in topic flow,
+ // because we specify a throwing callback.
await rejectPromises();
await element.updateComplete;
await waitUntil(() => query(element, '.error') !== undefined);
@@ -709,10 +729,12 @@ suite('gr-change-list-topic-flow tests', () => {
assert.deepEqual(setChangeTopicStub.firstCall.args, [
changesWithNoTopics[0]._number,
'foo',
+ throwingErrorCallback,
]);
assert.deepEqual(setChangeTopicStub.secondCall.args, [
changesWithNoTopics[1]._number,
'foo',
+ throwingErrorCallback,
]);
await waitUntilCalled(alertStub, 'alertStub');
@@ -725,46 +747,5 @@ suite('gr-change-list-topic-flow tests', () => {
selectedChangeCount: 2,
});
});
-
- test('shows error when setting topic fails', async () => {
- const getTopicsStub = stubRestApi('getChangesWithSimilarTopic').resolves([
- {...createChange(), topic: 'foo' as TopicName},
- ]);
- const alertStub = sinon.stub();
- element.addEventListener('show-alert', alertStub);
- const autocomplete = queryAndAssert<GrAutocomplete>(
- element,
- 'gr-autocomplete'
- );
-
- autocomplete.setFocus(true);
- autocomplete.text = 'foo';
- await element.updateComplete;
- await waitUntilCalled(getTopicsStub, 'getTopicsStub');
- assert.isFalse(
- queryAndAssert<GrButton>(element, '#set-topic-button').disabled
- );
- queryAndAssert<GrButton>(element, '#set-topic-button').click();
- await element.updateComplete;
-
- assert.equal(
- queryAndAssert(element, '.loadingText').textContent,
- 'Setting topic...'
- );
-
- await rejectPromises();
- await element.updateComplete;
-
- await waitUntil(() => query(element, '.error') !== undefined);
- assert.equal(
- queryAndAssert(element, '.error').textContent,
- 'Failed to set topic'
- );
- assert.equal(
- queryAndAssert(element, 'gr-button#cancel-button').textContent,
- 'Cancel'
- );
- assert.isUndefined(query(element, '.loadingText'));
- });
});
});
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
index dbca42f5d2..81710e868d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
@@ -32,9 +32,13 @@ const LIMIT_OPERATOR_PATTERN = /\blimit:(\d+)/i;
@customElement('gr-change-list-view')
export class GrChangeListView extends LitElement {
- @query('#prevArrow') protected prevArrow?: HTMLAnchorElement;
+ @query('#prevArrow') protected prevArrow?:
+ | HTMLAnchorElement
+ | HTMLSpanElement;
- @query('#nextArrow') protected nextArrow?: HTMLAnchorElement;
+ @query('#nextArrow') protected nextArrow?:
+ | HTMLAnchorElement
+ | HTMLSpanElement;
// private but used in test
@state() account?: AccountDetailInfo;
@@ -143,13 +147,17 @@ export class GrChangeListView extends LitElement {
gr-repo-header {
border-bottom: 1px solid var(--border-color);
}
+ span[disabled] gr-icon {
+ background-color: transparent;
+ color: var(--disabled-foreground);
+ cursor: default;
+ }
nav {
align-items: center;
display: flex;
height: 3rem;
justify-content: flex-end;
margin-right: 20px;
- color: var(--deemphasized-text-color);
}
gr-icon {
font-size: 1.85rem;
@@ -212,15 +220,39 @@ export class GrChangeListView extends LitElement {
return html`
<nav>
- Page ${this.computePage()} ${this.renderPrevArrow()}
+ ${this.renderPageNums()}${this.renderPrevArrow()}
${this.renderNextArrow()}
</nav>
`;
}
+ private renderPageNums() {
+ if (this.offset === 0 && this.changes.length <= 1) {
+ return html`<span><strong>${this.changes.length}</strong></span>`;
+ }
+
+ const changesCount = this.changes?.length ?? 0;
+ const hasMore = this.changes?.[changesCount - 1]._more_changes;
+
+ return html`<span>
+ <strong
+ >${this.offset + 1}&nbsp;-&nbsp;${this.offset + changesCount}</strong
+ >&nbsp;of&nbsp;<strong
+ >${hasMore ? 'many' : this.offset + changesCount}
+ </strong></span
+ >`;
+ }
+
private renderPrevArrow() {
- if (this.offset === 0) return nothing;
+ const changesCount = this.changes?.length ?? 0;
+ if (changesCount === 0) return nothing;
+ const isDisabled = this.offset === 0;
+ if (isDisabled) {
+ return html`<span id="prevArrow" disabled>
+ <gr-icon icon="chevron_left" aria-label="Older"></gr-icon>
+ </span>`;
+ }
return html`
<a id="prevArrow" href=${this.computeNavLink(-1)}>
<gr-icon icon="chevron_left" aria-label="Older"></gr-icon>
@@ -231,8 +263,13 @@ export class GrChangeListView extends LitElement {
private renderNextArrow() {
const changesCount = this.changes?.length ?? 0;
if (changesCount === 0) return nothing;
- if (!this.changes?.[changesCount - 1]._more_changes) return nothing;
+ const isDisabled = !this.changes?.[changesCount - 1]._more_changes;
+ if (isDisabled) {
+ return html`<span id="nextArrow" disabled>
+ <gr-icon icon="chevron_right" aria-label="Newer"></gr-icon>
+ </span>`;
+ }
return html`
<a id="nextArrow" href=${this.computeNavLink(1)}>
<gr-icon icon="chevron_right" aria-label="Newer"></gr-icon>
@@ -266,13 +303,15 @@ export class GrChangeListView extends LitElement {
// private but used in test
handleNextPage() {
- if (!this.nextArrow || !this.changesPerPage) return;
+ if (this.nextArrow?.hasAttribute('disabled') || !this.changesPerPage)
+ return;
this.getNavigation().setUrl(this.computeNavLink(1));
}
// private but used in test
handlePreviousPage() {
- if (!this.prevArrow || !this.changesPerPage) return;
+ if (this.prevArrow?.hasAttribute('disabled') || !this.changesPerPage)
+ return;
this.getNavigation().setUrl(this.computeNavLink(-1));
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts
index decc2537fc..93a530df56 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts
@@ -30,9 +30,10 @@ suite('gr-change-list-view tests', () => {
});
test('render', async () => {
- element.changes = Array(25)
+ element.changes = Array(10)
.fill(0)
.map(_ => createChange());
+ element.changes[9]._more_changes = true;
element.changesPerPage = 10;
element.loading = false;
await element.updateComplete;
@@ -43,7 +44,19 @@ suite('gr-change-list-view tests', () => {
<div class="loading" hidden="">Loading...</div>
<div>
<gr-change-list> </gr-change-list>
- <nav>Page 1</nav>
+ <nav>
+ <span>
+ <strong>1&nbsp;-&nbsp;10</strong>&nbsp;of&nbsp;<strong
+ >many</strong
+ >
+ </span>
+ <span disabled="" id="prevArrow">
+ <gr-icon aria-label="Older" icon="chevron_left"> </gr-icon>
+ </span>
+ <a href="/q/test-query,10" id="nextArrow">
+ <gr-icon aria-label="Newer" icon="chevron_right"> </gr-icon>
+ </a>
+ </nav>
</div>
`
);
@@ -136,11 +149,13 @@ suite('gr-change-list-view tests', () => {
element.offset = 0;
element.loading = false;
await element.updateComplete;
- assert.isNotOk(query(element, '#prevArrow'));
+ assert.isTrue(
+ query<HTMLAnchorElement>(element, '#prevArrow')?.hasAttribute('disabled')
+ );
element.offset = 5;
await element.updateComplete;
- assert.isOk(query(element, '#prevArrow'));
+ assert.isFalse(query(element, '#prevArrow')?.hasAttribute('disabled'));
});
test('nextArrow', async () => {
@@ -149,13 +164,13 @@ suite('gr-change-list-view tests', () => {
.map(_ => ({...createChange(), _more_changes: true} as ChangeInfo));
element.loading = false;
await element.updateComplete;
- assert.isOk(query(element, '#nextArrow'));
+ assert.isFalse(query(element, '#nextArrow')?.hasAttribute('disabled'));
element.changes = Array(25)
.fill(0)
.map(_ => createChange());
await element.updateComplete;
- assert.isNotOk(query(element, '#nextArrow'));
+ assert.isTrue(query(element, '#nextArrow')?.hasAttribute('disabled'));
});
test('handleNextPage', async () => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index 31c2b66037..50ec33977a 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -42,6 +42,7 @@ import {
ChangeActionDialog,
ChangeInfo,
CherryPickInput,
+ CommentThread,
CommitId,
InheritedBooleanInfo,
isDetailedLabelInfo,
@@ -115,6 +116,9 @@ import {subscribe} from '../../lit/subscription-controller';
import {userModelToken} from '../../../models/user/user-model';
import {ParsedChangeInfo} from '../../../types/types';
import {configModelToken} from '../../../models/config/config-model';
+import {readJSONResponsePayload} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {commentsModelToken} from '../../../models/comments/comments-model';
+import {when} from 'lit/directives/when.js';
const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.';
const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.';
@@ -313,7 +317,6 @@ interface MenuAction {
interface OverflowAction {
type: ActionType;
key: string;
- overflow?: boolean;
}
interface ActionPriorityOverride {
@@ -366,19 +369,12 @@ export class GrChangeActions
@query('#confirmDeleteEditDialog') confirmDeleteEditDialog?: GrDialog;
+ @query('#confirmPublishEditDialog') confirmPublishEditDialog?: GrDialog;
+
@query('#moreActions') moreActions?: GrDropdown;
@query('#secondaryActions') secondaryActions?: HTMLElement;
- // TODO(TS): Ensure that ActionType, ChangeActions and RevisionActions
- // properties are replaced with enums everywhere and remove them from
- // the GrChangeActions class
- ActionType = ActionType;
-
- ChangeActions = ChangeActions;
-
- RevisionActions = RevisionActions;
-
@state() change?: ParsedChangeInfo;
@state() actions: ActionNameToActionInfoMap = {};
@@ -396,22 +392,20 @@ export class GrChangeActions
@state() changeStatus?: ChangeStatus;
+ @state() mergeable?: boolean;
+
@state() commitNum?: CommitId;
@state() latestPatchNum?: PatchSetNumber;
@state() commitMessage = '';
- @state() revisionActions: ActionNameToActionInfoMap = {};
-
- @state() revisionSubmitAction?: ActionInfo | null;
-
- @state() revisionRebaseAction?: ActionInfo | null;
+ // The unfiltered result of calling `restApiService.getChangeRevisionActions()`.
+ // The DOWNLOAD action is also added to it in `actionsChanged()`.
+ @state() revisionActions?: ActionNameToActionInfoMap;
@state() privateByDefault?: InheritedBooleanInfo;
- @state() loading = true;
-
@state() actionLoadingMessage = '';
@state() inProgressActionKeys = new Set<string>();
@@ -489,6 +483,10 @@ export class GrChangeActions
@state() loggedIn = false;
+ @state() pluginsLoaded = false;
+
+ @state() threadsWithSuggestions?: CommentThread[];
+
private readonly restApiService = getAppContext().restApiService;
private readonly reporting = getAppContext().reportingService;
@@ -505,6 +503,8 @@ export class GrChangeActions
private readonly getNavigation = resolve(this, navigationToken);
+ private readonly getCommentsModel = resolve(this, commentsModelToken);
+
constructor() {
super();
subscribe(
@@ -539,6 +539,11 @@ export class GrChangeActions
);
subscribe(
this,
+ () => this.getChangeModel().mergeable$,
+ x => (this.mergeable = x)
+ );
+ subscribe(
+ this,
() => this.getChangeModel().editMode$,
x => (this.editMode = x)
);
@@ -564,9 +569,19 @@ export class GrChangeActions
);
subscribe(
this,
+ () => this.getPluginLoader().pluginsModel.pluginsLoaded$,
+ x => (this.pluginsLoaded = x)
+ );
+ subscribe(
+ this,
() => this.getConfigModel().repoConfig$,
config => (this.privateByDefault = config?.private_by_default)
);
+ subscribe(
+ this,
+ () => this.getCommentsModel().threadsWithSuggestions$,
+ x => (this.threadsWithSuggestions = x)
+ );
}
override connectedCallback() {
@@ -575,7 +590,6 @@ export class GrChangeActions
TargetElement.CHANGE_ACTIONS,
this
);
- this.handleLoadingComplete();
}
static override get styles() {
@@ -620,6 +634,15 @@ export class GrChangeActions
.hidden {
display: none;
}
+ .info {
+ background-color: var(--info-background);
+ padding: var(--spacing-l) var(--spacing-xl);
+ margin-bottom: var(--spacing-l);
+ }
+ .info gr-icon {
+ color: var(--selected-foreground);
+ margin-right: var(--spacing-xl);
+ }
@media screen and (max-width: 50em) {
#mainContent {
flex-wrap: wrap;
@@ -653,7 +676,7 @@ export class GrChangeActions
</span>
<section
id="primaryActions"
- ?hidden=${this.loading ||
+ ?hidden=${this.isLoading() ||
!this.topLevelActions ||
!this.topLevelActions.length}
>
@@ -663,7 +686,7 @@ export class GrChangeActions
</section>
<section
id="secondaryActions"
- ?hidden=${this.loading ||
+ ?hidden=${this.isLoading() ||
!this.topLevelActions ||
!this.topLevelActions.length}
>
@@ -671,14 +694,14 @@ export class GrChangeActions
this.renderUIAction(action)
)}
</section>
- <gr-button ?hidden=${!this.loading}>Loading actions...</gr-button>
+ <gr-button ?hidden=${!this.isLoading()}>Loading actions...</gr-button>
<gr-dropdown
id="moreActions"
link
.verticalOffset=${32}
.horizontalAlign=${'right'}
@tap-item=${this.handleOverflowItemTap}
- ?hidden=${this.loading ||
+ ?hidden=${this.isLoading() ||
!this.menuActions ||
!this.menuActions.length}
.disabledIds=${this.disabledMenuActions}
@@ -698,9 +721,7 @@ export class GrChangeActions
RevisionActions.REBASE
)}
.branch=${this.change?.branch}
- .rebaseOnCurrent=${this.revisionRebaseAction
- ? !!this.revisionRebaseAction.enabled
- : null}
+ .rebaseOnCurrent=${!!this.revisionActions?.rebase?.enabled}
></gr-confirm-rebase-dialog>
<gr-confirm-cherrypick-dialog
id="confirmCherrypick"
@@ -740,7 +761,7 @@ export class GrChangeActions
<gr-confirm-submit-dialog
id="confirmSubmitDialog"
class="confirmDialog"
- .action=${this.revisionSubmitAction}
+ .action=${this.revisionActions?.submit}
@cancel=${this.handleConfirmDialogCancel}
@confirm=${this.handleSubmitConfirm}
></gr-confirm-submit-dialog>
@@ -788,6 +809,27 @@ export class GrChangeActions
Do you really want to delete the edit?
</div>
</gr-dialog>
+ <gr-dialog
+ id="confirmPublishEditDialog"
+ class="confirmDialog"
+ confirm-label="Publish"
+ confirm-on-enter=""
+ @cancel=${this.handleConfirmDialogCancel}
+ @confirm=${this.handlePublishEditConfirm}
+ >
+ <div class="header" slot="header">Publish Change Edit</div>
+ <div class="main" slot="main">
+ ${when(
+ this.numberOfThreadsWithSuggestions() > 0,
+ () => html`<p class="info">
+ <gr-icon id="icon" icon="info" small></gr-icon>
+ Heads Up! ${this.numberOfThreadsWithSuggestions()} comments have
+ suggestions you can apply before publishing
+ </p>`
+ )}
+ Do you really want to publish the edit?
+ </div>
+ </gr-dialog>
</dialog>
`;
}
@@ -834,7 +876,7 @@ export class GrChangeActions
this.topLevelActions = this.allActionValues.filter(a => {
if (this.hiddenActions.includes(a.__key)) return false;
if (this.editMode) return EDIT_ACTIONS.has(a.__key);
- return this.getActionOverflowIndex(a.__type, a.__key) === -1;
+ return !this.isOverflowAction(a.__type, a.__key);
});
this.topLevelPrimaryActions = this.topLevelActions.filter(
action => action.__primary
@@ -843,31 +885,6 @@ export class GrChangeActions
action => !action.__primary
);
this.menuActions = this.computeMenuActions();
- this.revisionSubmitAction = this.getSubmitAction(this.revisionActions);
- this.revisionRebaseAction = this.getRebaseAction(this.revisionActions);
- }
-
- private getSubmitAction(revisionActions: ActionNameToActionInfoMap) {
- return this.getRevisionAction(revisionActions, 'submit');
- }
-
- private getRebaseAction(revisionActions: ActionNameToActionInfoMap) {
- return this.getRevisionAction(revisionActions, 'rebase');
- }
-
- private getRevisionAction(
- revisionActions: ActionNameToActionInfoMap,
- actionName: string
- ) {
- if (!revisionActions) {
- return undefined;
- }
- if (revisionActions[actionName] === undefined) {
- // Return null to fire an event when revisionActions was loaded
- // but doesn't contain actionName. undefined doesn't fire an event
- return null;
- }
- return revisionActions[actionName];
}
reload() {
@@ -875,33 +892,29 @@ export class GrChangeActions
return Promise.resolve();
}
const change = this.change;
-
- this.loading = true;
+ this.revisionActions = undefined;
return this.restApiService
.getChangeRevisionActions(this.changeNum, this.latestPatchNum)
.then(revisionActions => {
- if (!revisionActions) {
- return;
- }
-
- this.revisionActions = revisionActions;
+ this.revisionActions = revisionActions ?? {};
this.sendShowRevisionActions({
change: change as ChangeInfo,
- revisionActions,
+ revisionActions: this.revisionActions,
});
- this.handleLoadingComplete();
})
.catch(err => {
fireAlert(this, ERR_REVISION_ACTIONS);
- this.loading = false;
throw err;
});
}
- private handleLoadingComplete() {
- this.getPluginLoader()
- .awaitPluginsLoaded()
- .then(() => (this.loading = false));
+ private isLoading() {
+ return (
+ !this.pluginsLoaded ||
+ !this.change ||
+ this.mergeable === undefined ||
+ this.revisionActions === undefined
+ );
}
// private but used in test
@@ -942,22 +955,25 @@ export class GrChangeActions
this.requestUpdate('additionalActions');
}
+ // TODO: Rename to toggleOverflow().
setActionOverflow(type: ActionType, key: string, overflow: boolean) {
if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
throw Error(`Invalid action type given: ${type}`);
}
- const index = this.getActionOverflowIndex(type, key);
- const action: OverflowAction = {
- type,
- key,
- overflow,
- };
- if (!overflow && index !== -1) {
- this.overflowActions.splice(index, 1);
- this.requestUpdate('overflowActions');
- } else if (overflow) {
- this.overflowActions.push(action);
- this.requestUpdate('overflowActions');
+ const isCurrentlyOverflow = this.isOverflowAction(type, key);
+ if (overflow === isCurrentlyOverflow) {
+ return;
+ }
+
+ // remove from overflowActions
+ if (!overflow) {
+ this.overflowActions = this.overflowActions.filter(
+ action => action.type !== type || action.key !== key
+ );
+ }
+ // add to overflowActions
+ if (overflow) {
+ this.overflowActions = [...this.overflowActions, {type, key}];
}
}
@@ -1006,9 +1022,9 @@ export class GrChangeActions
}
getActionDetails(actionName: string) {
- if (this.revisionActions[actionName]) {
+ if (this.revisionActions?.[actionName]) {
return this.revisionActions[actionName];
- } else if (this.actions[actionName]) {
+ } else if (this.actions?.[actionName]) {
return this.actions[actionName];
} else {
return undefined;
@@ -1028,7 +1044,7 @@ export class GrChangeActions
this.actionLoadingMessage = '';
this.disabledMenuActions = [];
- if (!this.revisionActions.download) {
+ if (this.revisionActions && !this.revisionActions.download) {
this.revisionActions = {
...this.revisionActions,
download: DOWNLOAD_ACTION,
@@ -1228,7 +1244,7 @@ export class GrChangeActions
}
private getActionValues(
- actionsChange: ActionNameToActionInfoMap,
+ actionsChange: ActionNameToActionInfoMap | undefined,
primariesChange: PrimaryActionKey[],
additionalActionsChange: UIActionInfo[],
type: ActionType
@@ -1524,7 +1540,7 @@ export class GrChangeActions
default:
this.fireAction(
this.prependSlash(key),
- assertUIActionInfo(this.revisionActions[key]),
+ assertUIActionInfo(this.revisionActions?.[key]),
true
);
}
@@ -1568,7 +1584,7 @@ export class GrChangeActions
const rebaseChain = !!e.detail.rebaseChain;
this.fireAction(
rebaseChain ? '/rebase:chain' : '/rebase',
- assertUIActionInfo(this.revisionActions.rebase),
+ assertUIActionInfo(this.revisionActions?.rebase),
rebaseChain ? false : true,
payload,
{
@@ -1604,7 +1620,7 @@ export class GrChangeActions
el.hidden = true;
this.fireAction(
'/cherrypick',
- assertUIActionInfo(this.revisionActions.cherrypick),
+ assertUIActionInfo(this.revisionActions?.cherrypick),
true,
{
destination: el.branch,
@@ -1719,6 +1735,23 @@ export class GrChangeActions
);
}
+ private handlePublishEditConfirm() {
+ this.hideAllDialogs();
+
+ if (!this.actions.publishEdit) return;
+
+ // We need to make sure that all cached version of a change
+ // edit are deleted.
+ this.getStorage().eraseEditableContentItemsForChangeEdit(this.changeNum);
+
+ this.fireAction(
+ '/edit:publish',
+ assertUIActionInfo(this.actions.publishEdit),
+ false,
+ {notify: NotifyType.NONE}
+ );
+ }
+
// private but used in test
handleSubmitConfirm() {
if (!this.canSubmitChange()) {
@@ -1727,13 +1760,13 @@ export class GrChangeActions
this.hideAllDialogs();
this.fireAction(
'/submit',
- assertUIActionInfo(this.revisionActions.submit),
+ assertUIActionInfo(this.revisionActions?.submit),
true
);
}
- private getActionOverflowIndex(type: string, key: string) {
- return this.overflowActions.findIndex(
+ private isOverflowAction(type: string, key: string) {
+ return this.overflowActions.some(
action => action.type === type && action.key === key
);
}
@@ -1751,8 +1784,7 @@ export class GrChangeActions
buttonKey = ChangeActions.REVERT;
}
- // If the action appears in the overflow menu.
- if (this.getActionOverflowIndex(action.__type, buttonKey) !== -1) {
+ if (this.isOverflowAction(action.__type, buttonKey)) {
this.disabledMenuActions.push(buttonKey === '/' ? 'delete' : buttonKey);
this.requestUpdate('disabledMenuActions');
return () => {
@@ -1833,16 +1865,15 @@ export class GrChangeActions
}
// private but used in test
- async handleResponse(action: UIActionInfo, response?: Response) {
- if (!response) {
+ async handleResponse(action: UIActionInfo, response: Response | undefined) {
+ if (!response?.ok) {
return;
}
- // response is guaranteed to be ok (due to semantics of rest-api methods)
- const obj = await this.restApiService.getResponseObject(response);
switch (action.__key) {
case ChangeActions.REVERT: {
- const revertChangeInfo: ChangeInfo = obj as unknown as ChangeInfo;
- this.restApiService.setInProjectLookup(
+ const revertChangeInfo = (await readJSONResponsePayload(response))
+ .parsed as unknown as ChangeInfo;
+ this.restApiService.addRepoNameToCache(
revertChangeInfo._number,
revertChangeInfo.project
);
@@ -1857,8 +1888,9 @@ export class GrChangeActions
break;
}
case RevisionActions.CHERRYPICK: {
- const cherrypickChangeInfo: ChangeInfo = obj as unknown as ChangeInfo;
- this.restApiService.setInProjectLookup(
+ const cherrypickChangeInfo = (await readJSONResponsePayload(response))
+ .parsed as unknown as ChangeInfo;
+ this.restApiService.addRepoNameToCache(
cherrypickChangeInfo._number,
cherrypickChangeInfo.project
);
@@ -1888,7 +1920,8 @@ export class GrChangeActions
this.getChangeModel().navigateToChangeResetReload();
break;
case ChangeActions.REVERT_SUBMISSION: {
- const revertSubmistionInfo = obj as unknown as RevertSubmissionInfo;
+ const revertSubmistionInfo = (await readJSONResponsePayload(response))
+ .parsed as unknown as RevertSubmissionInfo;
if (
!revertSubmistionInfo.revert_changes ||
!revertSubmistionInfo.revert_changes.length
@@ -2072,18 +2105,8 @@ export class GrChangeActions
}
private handlePublishEditTap() {
- if (!this.actions.publishEdit) return;
-
- // We need to make sure that all cached version of a change
- // edit are deleted.
- this.getStorage().eraseEditableContentItemsForChangeEdit(this.changeNum);
-
- this.fireAction(
- '/edit:publish',
- assertUIActionInfo(this.actions.publishEdit),
- false,
- {notify: NotifyType.NONE}
- );
+ assertIsDefined(this.confirmPublishEditDialog, 'confirmPublishEditDialog');
+ this.showActionDialog(this.confirmPublishEditDialog);
}
private handleRebaseEditTap() {
@@ -2180,7 +2203,7 @@ export class GrChangeActions
private computeMenuActions(): MenuAction[] {
return this.allActionValues
.filter(a => {
- const overflow = this.getActionOverflowIndex(a.__type, a.__key) !== -1;
+ const overflow = this.isOverflowAction(a.__type, a.__key);
return overflow && !this.hiddenActions.includes(a.__key);
})
.map(action => {
@@ -2240,6 +2263,11 @@ export class GrChangeActions
private handleStopEditTap() {
fireNoBubbleNoCompose(this, 'stop-edit-tap', {});
}
+
+ private numberOfThreadsWithSuggestions() {
+ if (!this.threadsWithSuggestions) return 0;
+ return this.threadsWithSuggestions.length;
+ }
}
declare global {
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
index b953eec416..a926f26c9a 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
@@ -17,6 +17,7 @@ import {
} from '../../../test/test-data-generators';
import {ChangeStatus, HttpMethod} from '../../../constants/constants';
import {
+ makePrefixedJSON,
mockPromise,
query,
queryAll,
@@ -39,7 +40,7 @@ import {
ReviewInput,
TopicName,
} from '../../../types/common';
-import {ActionType} from '../../../api/change-actions';
+import {ActionType, RevisionActions} from '../../../api/change-actions';
import {SinonFakeTimers, SinonStubbedMember} from 'sinon';
import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea';
import {GrButton} from '../../shared/gr-button/gr-button';
@@ -98,29 +99,6 @@ suite('gr-change-actions tests', () => {
},
})
);
- stubRestApi('send').callsFake((method, url) => {
- if (method !== 'POST') {
- return Promise.reject(new Error('bad method'));
- }
- if (url === '/changes/test~42/revisions/2/submit') {
- return Promise.resolve({
- ...new Response(),
- ok: true,
- text() {
- return Promise.resolve(")]}'\n{}");
- },
- });
- } else if (url === '/changes/test~42/revisions/2/rebase') {
- return Promise.resolve({
- ...new Response(),
- ok: true,
- text() {
- return Promise.resolve(")]}'\n{}");
- },
- });
- }
- return Promise.reject(new Error('bad url'));
- });
sinon
.stub(testResolver(pluginLoaderToken), 'awaitPluginsLoaded')
@@ -142,6 +120,7 @@ suite('gr-change-actions tests', () => {
},
};
element.changeNum = 42 as NumericChangeId;
+ element.mergeable = false;
element.latestPatchNum = 2 as PatchSetNumber;
element.account = {
_account_id: 123 as AccountId,
@@ -299,11 +278,65 @@ suite('gr-change-actions tests', () => {
Do you really want to delete the edit?
</div>
</gr-dialog>
+ <gr-dialog
+ class="confirmDialog"
+ confirm-label="Publish"
+ confirm-on-enter=""
+ id="confirmPublishEditDialog"
+ role="dialog"
+ >
+ <div class="header" slot="header">Publish Change Edit</div>
+ <div class="main" slot="main">
+ Do you really want to publish the edit?
+ </div>
+ </gr-dialog>
</dialog>
`
);
});
+ suite('isLoading', () => {
+ const isLoading = async () => {
+ await element.updateComplete;
+ const loadingButton = queryAndAssert(
+ element,
+ 'div#mainContent > gr-button'
+ );
+ assert.equal(loadingButton.textContent, 'Loading actions...');
+ return loadingButton.getAttribute('hidden') === null;
+ };
+
+ test('change', async () => {
+ element.change = undefined;
+ await element.updateComplete;
+ assert.shadowDom.equal(element, '');
+ });
+
+ test('mergeable', async () => {
+ element.mergeable = undefined;
+ assert.isTrue(await isLoading());
+
+ element.mergeable = true;
+ assert.isFalse(await isLoading());
+ });
+
+ test('pluginsLoaded', async () => {
+ element.pluginsLoaded = false;
+ assert.isTrue(await isLoading());
+
+ element.pluginsLoaded = true;
+ assert.isFalse(await isLoading());
+ });
+
+ test('revisionActions', async () => {
+ element.revisionActions = undefined;
+ assert.isTrue(await isLoading());
+
+ element.revisionActions = {};
+ assert.isFalse(await isLoading());
+ });
+ });
+
test('show-revision-actions event should fire', async () => {
const spy = sinon.spy(element, 'sendShowRevisionActions');
element.reload();
@@ -409,8 +442,8 @@ suite('gr-change-actions tests', () => {
);
assert.isOk(buttonEl);
element.setActionHidden(
- element.ActionType.REVISION,
- element.RevisionActions.SUBMIT,
+ ActionType.REVISION,
+ RevisionActions.SUBMIT,
true
);
assert.lengthOf(element.hiddenActions, 1);
@@ -419,8 +452,8 @@ suite('gr-change-actions tests', () => {
assert.isNotOk(buttonEl);
element.setActionHidden(
- element.ActionType.REVISION,
- element.RevisionActions.SUBMIT,
+ ActionType.REVISION,
+ RevisionActions.SUBMIT,
false
);
await element.updateComplete;
@@ -429,7 +462,6 @@ suite('gr-change-actions tests', () => {
});
test('buttons exist', async () => {
- element.loading = false;
await element.updateComplete;
const buttonEls = queryAll(element, 'gr-button');
const menuItems = queryAndAssert<GrDropdown>(
@@ -496,9 +528,7 @@ suite('gr-change-actions tests', () => {
test('submit change', async () => {
const showSpy = sinon.spy(element, 'showActionDialog');
- stubRestApi('getFromProjectLookup').returns(
- Promise.resolve('test' as RepoName)
- );
+ stubRestApi('getRepoName').returns(Promise.resolve('test' as RepoName));
element.change = {
...createChangeViewChange(),
revisions: {
@@ -532,9 +562,7 @@ suite('gr-change-actions tests', () => {
'resetFocus'
)
.callsFake(() => submitted.resolve());
- stubRestApi('getFromProjectLookup').returns(
- Promise.resolve('test' as RepoName)
- );
+ stubRestApi('getRepoName').returns(Promise.resolve('test' as RepoName));
element.change = {
...createChangeViewChange(),
revisions: {
@@ -576,7 +604,7 @@ suite('gr-change-actions tests', () => {
assert.isTrue(fireStub.calledOnce);
assert.deepEqual(fireStub.lastCall.args, [
'/submit',
- assertUIActionInfo(element.revisionActions.submit),
+ assertUIActionInfo(element.revisionActions?.submit),
true,
]);
});
@@ -1235,7 +1263,7 @@ suite('gr-change-actions tests', () => {
test('custom actions', async () => {
// Add a button with the same key as a server-based one to ensure
// collisions are taken care of.
- const key = element.addActionButton(element.ActionType.REVISION, 'Bork!');
+ const key = element.addActionButton(ActionType.REVISION, 'Bork!');
const keyTapped = mockPromise();
element.addEventListener(key + '-tap', async e => {
assert.equal(
@@ -2306,7 +2334,7 @@ suite('gr-change-actions tests', () => {
test('adds download revision action', async () => {
const handler = sinon.stub();
element.addEventListener('download-tap', handler);
- assert.ok(element.revisionActions.download);
+ assert.ok(element.revisionActions?.download);
element.handleDownloadTap();
await element.updateComplete;
@@ -2408,7 +2436,6 @@ suite('gr-change-actions tests', () => {
const payload = {foo: 'bar'};
let onShowError: sinon.SinonStub;
let onShowAlert: sinon.SinonStub;
- let getResponseObjectStub: sinon.SinonStub;
setup(async () => {
cleanup = sinon.stub();
@@ -2429,7 +2456,7 @@ suite('gr-change-actions tests', () => {
});
suite('happy path', () => {
- let sendStub: sinon.SinonStub;
+ let executeChangeActionStub: sinon.SinonStub;
setup(() => {
stubRestApi('getChangeDetail').returns(
Promise.resolve({
@@ -2439,8 +2466,7 @@ suite('gr-change-actions tests', () => {
messages: createChangeMessages(1),
})
);
- getResponseObjectStub = stubRestApi('getResponseObject');
- sendStub = stubRestApi('executeChangeAction').returns(
+ executeChangeActionStub = stubRestApi('executeChangeAction').returns(
Promise.resolve(new Response())
);
});
@@ -2457,7 +2483,7 @@ suite('gr-change-actions tests', () => {
assert.isFalse(onShowError.called);
assert.isTrue(cleanup.calledOnce);
assert.isTrue(
- sendStub.calledWith(
+ executeChangeActionStub.calledWith(
42,
HttpMethod.DELETE,
'/endpoint',
@@ -2474,11 +2500,12 @@ suite('gr-change-actions tests', () => {
});
test('revert submission single change', async () => {
- getResponseObjectStub.returns(
- Promise.resolve({
+ const response = new Response(
+ makePrefixedJSON({
revert_changes: [{change_id: 12345, topic: 'T'}],
})
);
+ executeChangeActionStub.resolves(response);
await element.send(
HttpMethod.POST,
{message: 'Revert submission'},
@@ -2493,20 +2520,21 @@ suite('gr-change-actions tests', () => {
__type: ActionType.CHANGE,
label: 'l',
},
- new Response()
+ response
);
assert.isTrue(setUrlStub.called);
assert.equal(setUrlStub.lastCall.args[0], '/q/topic:"T"');
});
test('revert single change', async () => {
- getResponseObjectStub.returns(
- Promise.resolve({
+ const response = new Response(
+ makePrefixedJSON({
change_id: 12345,
project: 'projectId',
_number: 12345,
})
);
+ executeChangeActionStub.resolves(response);
stubRestApi('getChange').returns(
Promise.resolve(createChangeViewChange())
);
@@ -2524,7 +2552,7 @@ suite('gr-change-actions tests', () => {
__type: ActionType.CHANGE,
label: 'l',
},
- new Response()
+ response
);
assert.isTrue(setUrlStub.called);
assert.equal(setUrlStub.lastCall.args[0], '/c/projectId/+/12345');
@@ -2534,15 +2562,17 @@ suite('gr-change-actions tests', () => {
suite('multiple changes revert', () => {
let showActionDialogStub: sinon.SinonStub;
let setUrlStub: sinon.SinonStub;
+ let response: Response;
setup(() => {
- getResponseObjectStub.returns(
- Promise.resolve({
+ response = new Response(
+ makePrefixedJSON({
revert_changes: [
{change_id: 12345, topic: 'T'},
{change_id: 23456, topic: 'T'},
],
})
);
+ executeChangeActionStub.resolves(response);
showActionDialogStub = sinon.stub(element, 'showActionDialog');
setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
});
@@ -2562,7 +2592,7 @@ suite('gr-change-actions tests', () => {
__type: ActionType.CHANGE,
label: 'l',
},
- new Response()
+ response
);
assert.isFalse(showActionDialogStub.called);
assert.isTrue(setUrlStub.called);
@@ -2582,7 +2612,13 @@ suite('gr-change-actions tests', () => {
assert.isFalse(onShowError.called);
assert.isTrue(cleanup.calledOnce);
assert.isTrue(
- sendStub.calledWith(42, 'DELETE', '/endpoint', 12, payload)
+ executeChangeActionStub.calledWith(
+ 42,
+ 'DELETE',
+ '/endpoint',
+ 12,
+ payload
+ )
);
});
});
@@ -2599,7 +2635,7 @@ suite('gr-change-actions tests', () => {
messages: createChangeMessages(1),
})
);
- const sendStub = stubRestApi('executeChangeAction');
+ const executeChangeActionStub = stubRestApi('executeChangeAction');
return element
.send(
@@ -2614,7 +2650,7 @@ suite('gr-change-actions tests', () => {
assert.isTrue(onShowAlert.calledOnce);
assert.isFalse(onShowError.called);
assert.isTrue(cleanup.calledOnce);
- assert.isFalse(sendStub.called);
+ assert.isFalse(executeChangeActionStub.called);
});
});
@@ -2627,11 +2663,13 @@ suite('gr-change-actions tests', () => {
messages: createChangeMessages(1),
})
);
- const sendStub = stubRestApi('executeChangeAction').callsFake(
+ const executeChangeActionStub = stubRestApi(
+ 'executeChangeAction'
+ ).callsFake(
(_num, _method, _patchNum, _endpoint, _payload, onErr) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
onErr!();
- return Promise.resolve(undefined);
+ return Promise.resolve(new Response());
}
);
const handleErrorStub = sinon.stub(element, 'handleResponseError');
@@ -2648,7 +2686,7 @@ suite('gr-change-actions tests', () => {
.then(() => {
assert.isFalse(onShowError.called);
assert.isTrue(cleanup.called);
- assert.isTrue(sendStub.calledOnce);
+ assert.isTrue(executeChangeActionStub.calledOnce);
assert.isTrue(handleErrorStub.called);
});
});
@@ -2662,7 +2700,6 @@ suite('gr-change-actions tests', () => {
messages: createChangeMessages(1),
})
);
- getResponseObjectStub = stubRestApi('getResponseObject');
const setUrlStub = sinon.stub(
testResolver(navigationToken),
'setUrl'
@@ -2671,8 +2708,8 @@ suite('gr-change-actions tests', () => {
element,
'setReviewOnRevert'
);
- getResponseObjectStub.returns(
- Promise.resolve({
+ const response = new Response(
+ makePrefixedJSON({
change_id: 12345,
project: 'projectId',
_number: 12345,
@@ -2705,7 +2742,7 @@ suite('gr-change-actions tests', () => {
__type: ActionType.CHANGE,
label: 'l',
},
- new Response()
+ response
);
assert.isTrue(errorFired);
@@ -2761,12 +2798,12 @@ suite('gr-change-actions tests', () => {
assert.strictEqual(
queryAndAssert<GrConfirmSubmitDialog>(element, '#confirmSubmitDialog')
.action,
- null
+ undefined
);
assert.strictEqual(
queryAndAssert<GrConfirmRebaseDialog>(element, '#confirmRebase')
.rebaseOnCurrent,
- null
+ false
);
});
});
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
index 634fbf8fe1..f5e403db37 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
@@ -59,7 +59,12 @@ import {
isSectionSet,
DisplayRules,
} from '../../../utils/change-metadata-util';
-import {fireAlert, fire, fireReload} from '../../../utils/event-util';
+import {
+ fireAlert,
+ fire,
+ fireReload,
+ fireError,
+} from '../../../utils/event-util';
import {
EditRevisionInfo,
isDefined,
@@ -89,6 +94,7 @@ import {configModelToken} from '../../../models/config/config-model';
import {changeModelToken} from '../../../models/change/change-model';
import {relatedChangesModelToken} from '../../../models/change/related-changes-model';
import {truncatePath} from '../../../utils/path-list-util';
+import {accountEmail, getDisplayName} from '../../../utils/display-name-util';
const HASHTAG_ADD_MESSAGE = 'Add Hashtag';
@@ -134,6 +140,8 @@ export class GrChangeMetadata extends LitElement {
@state() revertedChange?: ChangeInfo;
+ @state() editMode = false;
+
@state() account?: AccountDetailInfo;
@state() revision?: RevisionInfo | EditRevisionInfo;
@@ -209,6 +217,11 @@ export class GrChangeMetadata extends LitElement {
() => this.getRelatedChangesModel().revertingChange$,
revertingChange => (this.revertedChange = revertingChange)
);
+ subscribe(
+ this,
+ () => this.getChangeModel().editMode$,
+ x => (this.editMode = x)
+ );
this.queryTopic = (input: string) => this.getTopicSuggestions(input);
this.queryHashtag = (input: string) => this.getHashtagSuggestions(input);
}
@@ -231,6 +244,9 @@ export class GrChangeMetadata extends LitElement {
gr-weblink {
display: block;
}
+ gr-account-chip {
+ display: inline;
+ }
gr-account-chip[disabled],
gr-linked-chip[disabled] {
opacity: 0;
@@ -471,6 +487,21 @@ export class GrChangeMetadata extends LitElement {
circle-shape
></gr-vote-chip>
</gr-account-chip>
+ ${when(
+ this.editMode &&
+ (role === ChangeRole.AUTHOR || role === ChangeRole.COMMITTER),
+ () => html`
+ <gr-editable-label
+ id="${role}-edit-label"
+ placeholder="Update ${name}"
+ @changed=${(e: CustomEvent<string>) =>
+ this.handleIdentityChanged(e, role)}
+ showAsEditPencil
+ autocomplete
+ .query=${(text: string) => this.getIdentitySuggestions(text)}
+ ></gr-editable-label>
+ `
+ )}
</span>
</section>`;
}
@@ -865,6 +896,31 @@ export class GrChangeMetadata extends LitElement {
}
// private but used in test
+ async handleIdentityChanged(e: CustomEvent<string>, role: ChangeRole) {
+ assertIsDefined(this.change, 'change');
+ const input = e.detail.length ? e.detail.trim() : undefined;
+ if (!input?.length) return;
+ const reg = /(\w+.*)\s<(\S+@\S+.\S+)>/;
+ const [, name, email] = input.match(reg) ?? [];
+ if (!name || !email) {
+ fireError(
+ this,
+ 'Invalid input format, valid identity format is "FullName <user@example.com>"'
+ );
+ return;
+ }
+ fireAlert(this, 'Saving identity and reloading ...');
+ await this.restApiService.updateIdentityInChangeEdit(
+ this.change._number,
+ name,
+ email,
+ role.toUpperCase()
+ );
+ fire(this, 'hide-alert', {});
+ fireReload(this);
+ }
+
+ // private but used in test
computeTopicReadOnly() {
return !this.mutable || !this.change?.actions?.topic?.enabled;
}
@@ -973,16 +1029,9 @@ export class GrChangeMetadata extends LitElement {
return createSearchUrl({repo: project});
}
- private computeBranchUrl(project?: RepoName, branch?: BranchName) {
- if (!project || !branch || !this.change || !this.change.status) return '';
- return createSearchUrl({
- branch,
- repo: project,
- statuses:
- this.change.status === ChangeStatus.NEW
- ? ['open']
- : [this.change.status.toLowerCase()],
- });
+ private computeBranchUrl(repo?: RepoName, branch?: BranchName) {
+ if (!repo || !branch || !this.change || !this.change.status) return '';
+ return createSearchUrl({branch, repo});
}
private computeCherryPickOfUrl(
@@ -1140,7 +1189,7 @@ export class GrChangeMetadata extends LitElement {
if (
role === ChangeRole.AUTHOR &&
rev.commit?.author &&
- this.change.owner.email !== rev.commit.author.email
+ (this.editMode || this.change.owner.email !== rev.commit.author.email)
) {
return rev.commit.author;
}
@@ -1148,10 +1197,12 @@ export class GrChangeMetadata extends LitElement {
if (
role === ChangeRole.COMMITTER &&
rev.commit?.committer &&
- this.change.owner.email !== rev.commit.committer.email &&
- !(
- rev.uploader?.email && rev.uploader.email === rev.commit.committer.email
- )
+ (this.editMode ||
+ (this.change.owner.email !== rev.commit.committer.email &&
+ !(
+ rev.uploader?.email &&
+ rev.uploader.email === rev.commit.committer.email
+ )))
) {
return rev.commit.committer;
}
@@ -1227,6 +1278,25 @@ export class GrChangeMetadata extends LitElement {
);
}
+ private async getIdentitySuggestions(
+ input: string
+ ): Promise<AutocompleteSuggestion[]> {
+ const suggestions = await this.restApiService.getAccountSuggestions(input);
+ if (!suggestions) return [];
+ const identitySuggestions: AutocompleteSuggestion[] = [];
+ suggestions.forEach(account => {
+ const name = getDisplayName(this.serverConfig, account);
+ const emails: string[] = [];
+ account.email && emails.push(account.email);
+ account.secondary_emails && emails.push(...account.secondary_emails);
+ emails.forEach(email => {
+ const identity = name + ' ' + accountEmail(email);
+ identitySuggestions.push({name: identity});
+ });
+ });
+ return identitySuggestions;
+ }
+
private computeVoteForRole(role: ChangeRole) {
const reviewer = this.getNonOwnerRole(role);
if (reviewer && isAccount(reviewer)) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
index 93ef3e39e2..87875b2727 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
@@ -166,7 +166,7 @@ suite('gr-change-metadata tests', () => {
test-project
</a>
|
- <a href="/q/project:test-project+branch:test-branch+status:open">
+ <a href="/q/project:test-project+branch:test-branch">
test-branch
</a>
</span>
@@ -407,6 +407,15 @@ suite('gr-change-metadata tests', () => {
element.change = change;
assert.isNotOk(element.getNonOwnerRole(ChangeRole.COMMITTER));
});
+
+ test('getNonOwnerRole returns committer with same email as owner in edit mode', () => {
+ // Set the committer email to be the same as the owner.
+ change!.revisions.rev1.commit!.committer.email =
+ 'abc@def' as EmailAddress;
+ element.change = change;
+ element.editMode = true;
+ assert.isOk(element.getNonOwnerRole(ChangeRole.COMMITTER));
+ });
});
suite('role=author', () => {
@@ -430,6 +439,14 @@ suite('gr-change-metadata tests', () => {
element.change = change;
assert.isNotOk(element.getNonOwnerRole(ChangeRole.AUTHOR));
});
+
+ test('getNonOwnerRole returns author with same email as owner in edit mode', () => {
+ // Set the author email to be the same as the owner.
+ change!.revisions.rev1.commit!.author.email = 'abc@def' as EmailAddress;
+ element.change = change;
+ element.editMode = true;
+ assert.isOk(element.getNonOwnerRole(ChangeRole.AUTHOR));
+ });
});
});
@@ -938,6 +955,35 @@ suite('gr-change-metadata tests', () => {
});
});
+ test('update author identity', async () => {
+ const change = createParsedChange();
+ element.change = change;
+ element.editMode = true;
+ await element.updateComplete;
+ const updateIdentityInChangeEditStub = stubRestApi(
+ 'updateIdentityInChangeEdit'
+ ).resolves();
+ const alertStub = sinon.stub();
+ element.addEventListener('show-alert', alertStub);
+ queryAndAssert(element, '#author-edit-label').dispatchEvent(
+ new CustomEvent('changed', {detail: 'user <user@example.com>'})
+ );
+ assert.isTrue(
+ updateIdentityInChangeEditStub.calledWith(
+ 42 as NumericChangeId,
+ 'user',
+ 'user@example.com',
+ 'AUTHOR'
+ )
+ );
+ await updateIdentityInChangeEditStub.lastCall.returnValue;
+ await waitUntilCalled(alertStub, 'alertStub');
+ assert.deepEqual(alertStub.lastCall.args[0].detail, {
+ message: 'Saving identity and reloading ...',
+ showDismiss: true,
+ });
+ });
+
test('editTopic', async () => {
element.account = createAccountDetailWithId();
element.change = {
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index f6099faaab..2a9912691b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -348,7 +348,11 @@ export class GrChangeSummary extends LitElement {
<gr-icon icon="info" filled></gr-icon>
</div>
<div class="right">
- <div class="message" title=${m}>${m}</div>
+ <gr-formatted-text
+ class="message"
+ .markdown=${true}
+ .content=${m}
+ ></gr-formatted-text>
</div>
</div>
`
@@ -534,11 +538,8 @@ export class GrChangeSummary extends LitElement {
<tr>
<td class="key">Comments</td>
<td class="value">
- ${when(
- this.commentsLoading,
- () => html`<span class="loadingSpin"></span>`
- )}
<gr-comments-summary
+ .commentsLoading=${this.commentsLoading}
.commentThreads=${this.commentThreads}
.draftCount=${this.draftCount}
.mentionCount=${this.mentionCount}
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
index c3d9774a9d..c2ea4e5ee9 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
@@ -92,7 +92,7 @@ suite('gr-change-summary test', () => {
<gr-icon icon="info" filled></gr-icon>
</div>
<div class="right">
- <div class="message" title="a message">a message</div>
+ <gr-formatted-text class="message"></gr-formatted-text>
</div>
</div>
</div>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 47f17562d9..2914a0355b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -153,6 +153,7 @@ import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-load
import {modalStyles} from '../../../styles/gr-modal-styles';
import {relatedChangesModelToken} from '../../../models/change/related-changes-model';
import {KnownExperimentId} from '../../../services/flags/flags';
+import {assign} from '../../../utils/location-util';
const MIN_LINES_FOR_COMMIT_COLLAPSE = 18;
@@ -408,6 +409,10 @@ export class GrChangeView extends LitElement {
@state()
replyModalOpened = false;
+ @state() private loginUrl = '';
+
+ @state() private loginText = '';
+
// Accessed in tests.
readonly reporting = getAppContext().reportingService;
@@ -488,6 +493,7 @@ export class GrChangeView extends LitElement {
// TODO: Do we still need docOnly bindings?
this.shortcutsController.addAbstract(Shortcut.EMOJI_DROPDOWN, () => {}); // docOnly
this.shortcutsController.addAbstract(Shortcut.MENTIONS_DROPDOWN, () => {}); // docOnly
+ this.shortcutsController.addAbstract(Shortcut.SAVE_COMMENT, () => {}); // docOnly
this.shortcutsController.addAbstract(Shortcut.REFRESH_CHANGE, () =>
this.getChangeModel().navigateToChangeResetReload()
);
@@ -710,6 +716,16 @@ export class GrChangeView extends LitElement {
);
subscribe(
this,
+ () => this.getConfigModel().loginUrl$,
+ loginUrl => (this.loginUrl = loginUrl)
+ );
+ subscribe(
+ this,
+ () => this.getConfigModel().loginText$,
+ loginText => (this.loginText = loginText)
+ );
+ subscribe(
+ this,
() => this.getRelatedChangesModel().revertingChange$,
revertingChange => {
this.revertingChange = revertingChange;
@@ -1298,7 +1314,6 @@ export class GrChangeView extends LitElement {
Shortcut.OPEN_REPLY_DIALOG,
ShortcutSection.ACTIONS
)}
- ?hidden=${!this.loggedIn}
primary=""
.disabled=${this.replyDisabled}
@click=${this.handleReplyTap}
@@ -1649,6 +1664,7 @@ export class GrChangeView extends LitElement {
if (!this.commitMessageEditor || this.commitMessageEditor.disabled) return;
// Trim trailing whitespace from each line.
const message = e.detail.content.replace(TRAILING_WHITESPACE_REGEX, '');
+ const committerEmail = e.detail.committerEmail;
this.getPluginLoader().jsApiService.handleCommitMessage(
this.change,
@@ -1657,7 +1673,7 @@ export class GrChangeView extends LitElement {
this.commitMessageEditor.disabled = true;
this.restApiService
- .putChangeCommitMessage(this.changeNum, message)
+ .putChangeCommitMessage(this.changeNum, message, committerEmail)
.then(resp => {
assertIsDefined(this.commitMessageEditor);
this.commitMessageEditor.disabled = false;
@@ -1792,9 +1808,15 @@ export class GrChangeView extends LitElement {
);
}
- private handleReplyTap(e: MouseEvent) {
- e.preventDefault();
- this.openReplyDialog(FocusTarget.ANY);
+ private handleReplyTap() {
+ if (this.loggedIn) {
+ this.openReplyDialog(FocusTarget.ANY);
+ } else {
+ // We are not using `this.getNavigation().setUrl()`, because the login
+ // page is served directly from the backend and is not part of the web
+ // app.
+ assign(window.location, this.loginUrl);
+ }
}
private onReplyModalCanceled() {
@@ -1983,6 +2005,9 @@ export class GrChangeView extends LitElement {
// Private but used in tests.
computeReplyButtonLabel() {
+ if (!this.loggedIn) {
+ return this.loginText;
+ }
let label = this.canStartReview() ? 'Start Review' : 'Reply';
if (this.draftCount > 0) {
label += ` (${this.draftCount})`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 009a92f441..4d6403ad28 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -1061,14 +1061,11 @@ suite('gr-change-view tests', () => {
});
});
- test('reply button is not visible when logged out', async () => {
+ test('reply button is a login button when logged out', async () => {
assertIsDefined(element.replyBtn);
element.loggedIn = false;
await element.updateComplete;
- assert.equal(getComputedStyle(element.replyBtn).display, 'none');
- element.loggedIn = true;
- await element.updateComplete;
- assert.notEqual(getComputedStyle(element.replyBtn).display, 'none');
+ assert.equal(element.replyBtn.textContent, 'Sign in');
});
test('download tap calls handleOpenDownloadDialog', () => {
@@ -1194,17 +1191,24 @@ suite('gr-change-view tests', () => {
Promise.resolve(new Response(null, {status: 500}))
);
await element.updateComplete;
- const mockEvent = (content: string) =>
- new CustomEvent('', {detail: {content}});
+ const committerEmail = 'test@example.org';
+ const mockEvent = (content: string, committerEmail: string) =>
+ new CustomEvent('', {
+ detail: {content, committerEmail},
+ });
assertIsDefined(element.commitMessageEditor);
- element.handleCommitMessageSave(mockEvent('test \n test '));
+ element.handleCommitMessageSave(
+ mockEvent('test \n test ', committerEmail)
+ );
assert.equal(putStub.lastCall.args[1], 'test\n test');
element.commitMessageEditor.disabled = false;
- element.handleCommitMessageSave(mockEvent(' test\ntest'));
+ element.handleCommitMessageSave(mockEvent(' test\ntest', committerEmail));
assert.equal(putStub.lastCall.args[1], ' test\ntest');
element.commitMessageEditor.disabled = false;
- element.handleCommitMessageSave(mockEvent('\n\n\n\n\n\n\n\n'));
+ element.handleCommitMessageSave(
+ mockEvent('\n\n\n\n\n\n\n\n', committerEmail)
+ );
assert.equal(putStub.lastCall.args[1], '\n\n\n\n\n\n\n\n');
});
diff --git a/polygerrit-ui/app/elements/change/gr-comments-summary/gr-comments-summary.ts b/polygerrit-ui/app/elements/change/gr-comments-summary/gr-comments-summary.ts
index bbd6003617..9123cd60e1 100644
--- a/polygerrit-ui/app/elements/change/gr-comments-summary/gr-comments-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-comments-summary/gr-comments-summary.ts
@@ -23,12 +23,17 @@ import {SummaryChipStyles} from '../gr-change-summary/gr-summary-chip';
import {subscribe} from '../../lit/subscription-controller';
import {resolve} from '../../../models/dependency';
import {userModelToken} from '../../../models/user/user-model';
+import {when} from 'lit/directives/when.js';
+import {spinnerStyles} from '../../../styles/gr-spinner-styles';
@customElement('gr-comments-summary')
export class GrCommentsSummary extends LitElement {
@property({type: Object})
commentThreads?: CommentThread[];
+ @property({type: Boolean})
+ commentsLoading = false;
+
@property({type: Number})
draftCount = 0;
@@ -63,7 +68,18 @@ export class GrCommentsSummary extends LitElement {
static override get styles() {
return [
+ spinnerStyles,
css`
+ /* The basics of .loadingSpin are defined in shared styles. */
+ .loadingSpin {
+ width: calc(var(--line-height-normal) - 2px);
+ height: calc(var(--line-height-normal) - 2px);
+ display: inline-block;
+ vertical-align: top;
+ position: relative;
+ /* Making up for the 2px reduced height above. */
+ top: 1px;
+ }
.zeroState {
color: var(--deemphasized-text-color);
}
@@ -91,6 +107,10 @@ export class GrCommentsSummary extends LitElement {
? this.getAccounts(commentThreads.filter(isResolved))
: undefined;
return html`
+ ${when(
+ this.commentsLoading,
+ () => html`<span class="loadingSpin"></span>`
+ )}
${this.renderZeroState(countResolvedComments, countUnresolvedComments)}
${this.renderDraftChip()} ${this.renderMentionChip()}
${this.renderUnresolvedCommentsChip(
@@ -112,6 +132,10 @@ export class GrCommentsSummary extends LitElement {
!!countUnresolvedComments
)
return nothing;
+ if (this.commentsLoading) {
+ return html`<span class="zeroState"> Loading comments...</span>`;
+ }
+
return html`<span class="zeroState"> No comments</span>`;
}
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
index 5e4e255d3b..5d2c6a4545 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
@@ -631,7 +631,10 @@ export class GrConfirmCherrypickDialog
payload,
handleError
)
- .then(() => {
+ .then(response => {
+ if (!response.ok) {
+ return;
+ }
this.updateStatus(change, {status: ProgressStatus.SUCCESSFUL});
const failedOrPending = Object.values(this.statuses).find(
v => v.status !== ProgressStatus.SUCCESSFUL
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts
index 83e6f9e14e..4655c71a3b 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts
@@ -207,7 +207,7 @@ suite('gr-confirm-cherrypick-dialog tests', () => {
await element.updateComplete;
const executeChangeActionStub = stubRestApi(
'executeChangeAction'
- ).returns(Promise.resolve(new Response()));
+ ).resolves(new Response());
queryAndAssert<GrDialog>(element, 'gr-dialog').confirmButton!.click();
await element.updateComplete;
const args = executeChangeActionStub.args[0];
@@ -228,7 +228,7 @@ suite('gr-confirm-cherrypick-dialog tests', () => {
await element.updateComplete;
const executeChangeActionStub = stubRestApi(
'executeChangeAction'
- ).returns(Promise.resolve(new Response()));
+ ).resolves(new Response());
const checkboxes = queryAll<HTMLInputElement>(
element,
'input[type="checkbox"]'
@@ -247,7 +247,7 @@ suite('gr-confirm-cherrypick-dialog tests', () => {
await element.updateComplete;
const executeChangeActionStub = stubRestApi(
'executeChangeAction'
- ).returns(Promise.resolve(new Response()));
+ ).resolves(new Response());
const checkboxes = queryAll<HTMLInputElement>(
element,
'input[type="checkbox"]'
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
index a4c52d7f33..c086359ed1 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
@@ -166,6 +166,9 @@ export class GrDownloadDialog extends LitElement {
@selected-scheme-changed=${(e: BindValueChangeEvent) => {
this.selectedScheme = e.detail.value;
}}
+ @item-copied=${(e: Event) => {
+ this.handleCloseTap(e);
+ }}
></gr-download-commands>
</section>
`;
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
index 73d7618724..c730671c06 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
@@ -19,7 +19,12 @@ import {
} from '../../../types/common';
import './gr-download-dialog';
import {GrDownloadDialog} from './gr-download-dialog';
-import {mockPromise, queryAll, queryAndAssert} from '../../../test/test-utils';
+import {
+ mockPromise,
+ queryAll,
+ queryAndAssert,
+ waitUntil,
+} from '../../../test/test-utils';
import {GrDownloadCommands} from '../../shared/gr-download-commands/gr-download-commands';
import {fixture, html, assert} from '@open-wc/testing';
import {GrButton} from '../../shared/gr-button/gr-button';
@@ -153,6 +158,20 @@ suite('gr-download-dialog', () => {
);
});
+ test('closes when gr-download-commands fires item-selected', async () => {
+ const fireStub = sinon.stub(element, 'dispatchEvent');
+ const commands = queryAndAssert<GrDownloadCommands>(
+ element,
+ 'gr-download-commands'
+ );
+ commands.dispatchEvent(new CustomEvent('item-copied'));
+
+ await waitUntil(() => fireStub.called);
+
+ const events = fireStub.args.map(arg => arg[0].type || '');
+ assert.isTrue(events.includes('close'));
+ });
+
test('anchors use download attribute', () => {
const anchors = Array.from(queryAll(element, 'a'));
assert.isTrue(!anchors.some(a => !a.hasAttribute('download')));
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
index f939374b4e..5186a1d07f 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
@@ -143,8 +143,8 @@ export class GrFileListHeader extends LitElement {
return [
sharedStyles,
css`
- .prefsButton {
- float: right;
+ #diffPrefsContainer {
+ display: flex;
}
.patchInfoOldPatchSet.patchInfo-header {
background-color: var(--emphasis-color);
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index 5533add204..dc75a16a18 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -51,12 +51,13 @@ import {GrDiffCursor} from '../../../embed/diff/gr-diff-cursor/gr-diff-cursor';
import {GrCursorManager} from '../../shared/gr-cursor-manager/gr-cursor-manager';
import {ChangeComments} from '../../diff/gr-comment-api/gr-comment-api';
import {ParsedChangeInfo, PatchSetFile} from '../../../types/types';
-import {Timing} from '../../../constants/reporting';
+import {Timing, Interaction} from '../../../constants/reporting';
import {RevisionInfo} from '../../shared/revision-info/revision-info';
import {select} from '../../../utils/observable-util';
import {resolve} from '../../../models/dependency';
import {browserModelToken} from '../../../models/browser/browser-model';
import {commentsModelToken} from '../../../models/comments/comments-model';
+import {RunResult, checksModelToken} from '../../../models/checks/checks-model';
import {changeModelToken} from '../../../models/change/change-model';
import {filesModelToken} from '../../../models/change/files-model';
import {ShortcutController} from '../../lit/shortcut-controller';
@@ -86,6 +87,7 @@ import {
import {userModelToken} from '../../../models/user/user-model';
import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {FileMode, fileModeToString} from '../../../utils/file-util';
+import {ChecksIcon, iconFor} from '../../../models/checks/checks-util';
export const DEFAULT_NUM_FILES_SHOWN = 200;
@@ -198,6 +200,9 @@ export class GrFileList extends LitElement {
@property({type: Object})
changeComments?: ChangeComments;
+ @property({type: Array})
+ checkResults?: RunResult[];
+
@state() selectedIndex = 0;
@property({type: Object})
@@ -300,6 +305,8 @@ export class GrFileList extends LitElement {
private readonly getCommentsModel = resolve(this, commentsModelToken);
+ private readonly getChecksModel = resolve(this, checksModelToken);
+
private readonly getBrowserModel = resolve(this, browserModelToken);
shortcutsController = new ShortcutController(this);
@@ -643,6 +650,46 @@ export class GrFileList extends LitElement {
:host(.hideComments) {
--gr-comment-thread-display: none;
}
+ .checkChip {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--spacing-xs);
+ border: 1px solid;
+ border-radius: 999px;
+ padding: var(--spacing-xxs) var(--spacing-m) var(--spacing-xxs)
+ var(--spacing-s);
+ vertical-align: top;
+ position: relative;
+ top: 2px;
+ font-size: var(--font-size-small);
+ font-weight: var(--font-weight-normal);
+ line-height: var(--line-height-small);
+ color: var(--primary-text-color);
+ & gr-icon {
+ font-size: var(--line-height-small);
+ }
+ &.info {
+ border-color: var(--info-foreground);
+ background-color: var(--info-background);
+ & gr-icon {
+ color: var(--info-foreground);
+ }
+ }
+ &.warning {
+ border-color: var(--warning-foreground);
+ background-color: var(--warning-background);
+ & gr-icon {
+ color: var(--warning-foreground);
+ }
+ }
+ &.error {
+ border-color: var(--error-foreground);
+ background-color: var(--error-background);
+ & gr-icon {
+ color: var(--error-foreground);
+ }
+ }
+ }
`,
];
}
@@ -744,6 +791,13 @@ export class GrFileList extends LitElement {
);
subscribe(
this,
+ () => this.getChecksModel().allResultsSelected$,
+ results => {
+ this.checkResults = results;
+ }
+ );
+ subscribe(
+ this,
() => this.getFilesModel().filesIncludingUnmodified$,
files => {
this.files = [...files];
@@ -1280,6 +1334,7 @@ export class GrFileList extends LitElement {
return html` <div role="gridcell">
<div class="comments desktop">
<span>${this.renderCommentsChips(file)}</span>
+ <span>${this.renderChecksChips(file)}</span>
<span class="noCommentsScreenReaderText">
<!-- Screen readers read the following content only if 2 other
spans in the parent div is empty. The content is not visible on
@@ -1654,6 +1709,35 @@ export class GrFileList extends LitElement {
></gr-comments-summary>`;
}
+ renderChecksChips(file?: NormalizedFileInfo) {
+ if (!this.checkResults || !this.patchRange || !file?.__path) {
+ return nothing;
+ }
+
+ const iconsByName: Record<string, ChecksIcon[]> = {};
+ for (const result of this.checkResults ?? []) {
+ if (
+ result.codePointers === undefined ||
+ !result.codePointers.some(pointer => pointer.path === file.__path)
+ ) {
+ continue;
+ }
+ const icon = iconFor(result.category);
+ iconsByName[icon.name] ??= [];
+ iconsByName[icon.name].push(icon);
+ }
+
+ return Object.values(iconsByName).map(
+ icons =>
+ html`
+ <div class="checkChip ${icons[0].name}">
+ <gr-icon icon=${icons[0].name} ?filled=${icons[0].filled}></gr-icon>
+ <div>${icons.length}</div>
+ </div>
+ `
+ );
+ }
+
protected override firstUpdated(): void {
this.detectChromiteButler();
this.reporting.fileListDisplayed();
@@ -1756,8 +1840,10 @@ export class GrFileList extends LitElement {
f => f.path === file.path
);
if (indexInExpanded === -1) {
+ this.reporting.reportInteraction(Interaction.FILE_LIST_DIFF_EXPANDED);
this.expandedFiles = this.expandedFiles.concat([file]);
} else {
+ this.reporting.reportInteraction(Interaction.FILE_LIST_DIFF_COLLAPSED);
this.expandedFiles = this.expandedFiles.filter(
(_val, idx) => idx !== indexInExpanded
);
@@ -1800,10 +1886,12 @@ export class GrFileList extends LitElement {
}
}
+ this.reporting.reportInteraction(Interaction.FILE_LIST_ALL_DIFFS_EXPANDED);
this.expandedFiles = newFiles.concat(this.expandedFiles);
}
collapseAllDiffs() {
+ this.reporting.reportInteraction(Interaction.FILE_LIST_ALL_DIFFS_COLLAPSED);
this.expandedFiles = [];
}
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
index 0f9cf6a43f..1cfbb83a24 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
@@ -196,6 +196,7 @@ suite('gr-file-list tests', () => {
emptywhennocomments=""
></gr-comments-summary
></span>
+ <span></span>
<span class="noCommentsScreenReaderText"> No comments </span>
</div>
<div class="comments mobile">
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
index df04f68f98..68fbe8bd10 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
@@ -33,7 +33,12 @@ import {fixture, assert} from '@open-wc/testing';
import {GrButton} from '../../shared/gr-button/gr-button';
import {PaperToggleButtonElement} from '@polymer/paper-toggle-button';
import {testResolver} from '../../../test/common-test-setup';
-import {commentsModelToken} from '../../../models/comments/comments-model';
+import {TEST_PROJECT_NAME} from '../../../test/test-data-generators';
+import {
+ ChangeChildView,
+ changeViewModelToken,
+} from '../../../models/views/change';
+import {GerritView} from '../../../services/router/router-model';
const author = {
_account_id: 42 as AccountId,
@@ -138,9 +143,12 @@ suite('gr-messages-list tests', () => {
element = await fixture<GrMessagesList>(
html`<gr-messages-list></gr-messages-list>`
);
- await testResolver(commentsModelToken).reloadComments(
- 0 as NumericChangeId
- );
+ testResolver(changeViewModelToken).setState({
+ view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
+ changeNum: 123 as NumericChangeId,
+ repo: TEST_PROJECT_NAME,
+ });
element.messages = messages;
await element.updateComplete;
});
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index a0ad2f0873..0bf49db467 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -49,7 +49,6 @@ import {
isDetailedLabelInfo,
isReviewerAccountSuggestion,
isReviewerGroupSuggestion,
- ParsedJSON,
ReviewerInput,
ReviewInput,
ReviewResult,
@@ -131,6 +130,10 @@ import {GrReviewerUpdatesParser} from '../../shared/gr-rest-api-interface/gr-rev
import {formStyles} from '../../../styles/form-styles';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {getDocUrl} from '../../../utils/url-util';
+import {
+ readJSONResponsePayload,
+ ResponsePayload,
+} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
export enum FocusTarget {
ANY = 'any',
@@ -1367,6 +1370,9 @@ export class GrReplyDialog extends LitElement {
// timer will be ended.
this.reporting.time(Timing.SEND_REPLY);
const labels = this.getLabelScores().getLabelValues();
+ if (labels[StandardLabels.CODE_REVIEW] === 2) {
+ this.reporting.reportInteraction(Interaction.CODE_REVIEW_APPROVAL);
+ }
const reviewInput: ReviewInput = {
drafts: includeComments
@@ -1435,7 +1441,8 @@ export class GrReplyDialog extends LitElement {
if (this.patchsetLevelGrComment) {
this.patchsetLevelGrComment.disableAutoSaving = true;
await this.restApiService.awaitPendingDiffDrafts();
- const comment = this.patchsetLevelGrComment.convertToCommentInput();
+ const comment =
+ await this.patchsetLevelGrComment.convertToCommentInputAndOrDiscard();
if (comment && comment.path && comment.message) {
reviewInput.comments ??= {};
reviewInput.comments[comment.path] ??= [];
@@ -1445,6 +1452,7 @@ export class GrReplyDialog extends LitElement {
assertIsDefined(this.change, 'change');
reviewInput.reviewers = this.computeReviewers();
+ this.reportStartReview(reviewInput);
const errFn = (r?: Response | null) => this.handle400Error(r);
this.getNavigation().blockNavigation('sending review');
@@ -1460,6 +1468,7 @@ export class GrReplyDialog extends LitElement {
this.includeComments = true;
fireNoBubble(this, 'send', {});
fireIronAnnounce(this, 'Reply sent');
+ this.getPluginLoader().jsApiService.handleReplySent();
return;
})
.then(result => result)
@@ -1474,6 +1483,31 @@ export class GrReplyDialog extends LitElement {
});
}
+ private reportStartReview(reviewInput: ReviewInput) {
+ const changeHasReviewers =
+ (this.change?.reviewers.REVIEWER ?? []).length > 0;
+ const newReviewersAdded =
+ (this.reviewersList?.additions() ?? []).length > 0;
+
+ // A review starts if either a WIP change is set to active with reviewers ...
+ const setActiveWithReviewers =
+ this.change?.work_in_progress &&
+ reviewInput.ready &&
+ // Setting a change active and *removing* all reviewers at the same time
+ // is an obscure corner case that we don't care about. :-)
+ (changeHasReviewers || newReviewersAdded);
+ // ... or if reviewers are added to an already active change that has no reviewers yet.
+ const isActiveAddReviewers =
+ !this.change?.work_in_progress &&
+ !reviewInput.work_in_progress &&
+ !changeHasReviewers &&
+ newReviewersAdded;
+
+ if (setActiveWithReviewers || isActiveAddReviewers) {
+ this.reporting.reportInteraction(Interaction.START_REVIEW);
+ }
+ }
+
focusOn(section?: FocusTarget) {
// Safeguard- always want to focus on something.
if (!section || section === FocusTarget.ANY) {
@@ -1514,26 +1548,29 @@ export class GrReplyDialog extends LitElement {
//
this.disabled = false;
- // Using response.clone() here, because getResponseObject() and
+ // Using response.clone() here, because readJSONResponsePayload() and
// potentially the generic error handler will want to call text() on the
// response object, which can only be done once per object.
- const jsonPromise = this.restApiService.getResponseObject(response.clone());
- return jsonPromise.then((parsed: ParsedJSON) => {
- const result = parsed as ReviewResult;
- // Only perform custom error handling for 400s and a parsable
- // ReviewResult response.
- if (response.status === 400 && result && result.reviewers) {
- const errors: string[] = [];
- const addReviewers = Object.values(result.reviewers);
- addReviewers.forEach(r => errors.push(r.error ?? 'no explanation'));
- response = {
- ...response,
- ok: false,
- text: () => Promise.resolve(errors.join(', ')),
- };
- }
- fireServerError(response);
- });
+ const jsonPromise = readJSONResponsePayload(response.clone());
+ return jsonPromise
+ .then((payload: ResponsePayload) => {
+ const result = payload.parsed as ReviewResult;
+ // Only perform custom error handling for 400s and a parsable
+ // ReviewResult response.
+ if (response.status === 400 && result.reviewers) {
+ const errors: string[] = [];
+ const addReviewers = Object.values(result.reviewers);
+ addReviewers.forEach(r => errors.push(r.error ?? 'no explanation'));
+ response = {
+ ...response,
+ ok: false,
+ text: () => Promise.resolve(errors.join(', ')),
+ };
+ }
+ })
+ .finally(() => {
+ fireServerError(response);
+ });
}
computeDraftsTitle(draftCommentThreads?: CommentThread[]) {
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
index 3bc27700ca..af018c3616 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
@@ -93,6 +93,9 @@ export class GrReviewerList extends LitElement {
--gr-vote-chip-width: 14px;
--gr-vote-chip-height: 14px;
}
+ .reviewersAndControls {
+ text-wrap: pretty;
+ }
`,
];
}
@@ -103,7 +106,7 @@ export class GrReviewerList extends LitElement {
this.reviewers.length - this.displayedReviewers.length;
return html`
<div class="container">
- <div>
+ <div class="reviewersAndControls">
${repeat(
this.displayedReviewers,
reviewer => accountKey(reviewer),
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.ts
index 1f1bef3c61..f050ab507b 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.ts
@@ -28,7 +28,7 @@ suite('gr-reviewer-list tests', () => {
element,
/* HTML */ `
<div class="container">
- <div>
+ <div class="reviewersAndControls">
<div class="controlsContainer" hidden="">
<gr-button
aria-disabled="false"
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index 29a22a36b9..830a35d1a9 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -23,10 +23,13 @@ import {
} from '../../../api/rest-api';
import {
extractAssociatedLabels,
+ extractLabelsWithCountFrom,
getAllUniqueApprovals,
getRequirements,
getTriggerVotes,
+ hasApprovedVote,
hasNeutralStatus,
+ hasRejectedVote,
hasVotes,
iconForRequirement,
orderSubmitRequirements,
@@ -286,6 +289,7 @@ export class GrSubmitRequirements extends LitElement {
label =>
html`<div class="votes-line">
${this.renderLabelVote(label, allLabels)}
+ ${this.renderVoteCountHelpLabel(requirement, label, allLabels)}
${this.renderOverrideLabels(
requirement,
label,
@@ -297,6 +301,30 @@ export class GrSubmitRequirements extends LitElement {
</div> `;
}
+ // Help when submit requirement needs more votes and there is already 1 vote
+ renderVoteCountHelpLabel(
+ requirement: SubmitRequirementResultInfo,
+ label: string,
+ labels: LabelNameToInfoMap
+ ) {
+ if (requirement.status !== SubmitRequirementStatus.UNSATISFIED) {
+ return nothing;
+ }
+
+ const labelInfo = labels[label];
+ if (!hasApprovedVote(labelInfo) || hasRejectedVote(labelInfo)) {
+ return nothing;
+ }
+
+ const count = extractLabelsWithCountFrom(
+ requirement.submittability_expression_result.expression
+ ).find(labelWithCount => labelWithCount.label === label)?.count;
+
+ if (!count || count === 1) return nothing;
+
+ return html`Requires ${count} votes`;
+ }
+
renderLabelVote(label: string, labels: LabelNameToInfoMap) {
const labelInfo = labels[label];
if (isDetailedLabelInfo(labelInfo)) {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 1057664a9a..72d3268654 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -241,6 +241,20 @@ export class GrResultRow extends LitElement {
display: inline-block;
margin-left: var(--spacing-s);
}
+ /* actions-shown-on-collapsed are shown only when .actions is hidden
+ and vice versa. */
+ tr.container td .summary-cell .actions-shown-on-collapsed,
+ tr.container.collapsed:focus-within
+ td
+ .summary-cell
+ .actions-shown-on-collapsed,
+ tr.container.collapsed:hover
+ td
+ .summary-cell
+ .actions-shown-on-collapsed,
+ :host(.dropdown-open) tr td .summary-cell .actions-shown-on-collapsed {
+ display: none;
+ }
tr.container.collapsed td .summary-cell .message {
color: var(--deemphasized-text-color);
}
@@ -248,6 +262,10 @@ export class GrResultRow extends LitElement {
tr.container.collapsed td .summary-cell .actions {
display: none;
}
+ tr.container.collapsed td .summary-cell .actions-shown-on-collapsed {
+ display: inline-block;
+ margin-left: var(--spacing-s);
+ }
tr.detailsRow.collapsed {
display: none;
}
@@ -278,6 +296,7 @@ export class GrResultRow extends LitElement {
td .summary-cell .tag.brown {
background-color: var(--tag-brown);
}
+ .actions-shown-on-collapsed gr-checks-action,
.actions gr-checks-action,
.actions gr-dropdown {
/* Fitting a 28px button into 20px line-height. */
@@ -463,7 +482,7 @@ export class GrResultRow extends LitElement {
}
renderSummary(text?: string) {
- if (!text) return;
+ text = text ?? '';
return html`
<!-- The &nbsp; is for being able to shrink a tiny amount without
the text itself getting shrunk with an ellipsis. -->
@@ -549,24 +568,31 @@ export class GrResultRow extends LitElement {
const disabledItems = overflowItems
.filter(action => action.disabled)
.map(action => action.id);
- return html`<div class="actions">
- ${this.renderAction(actions[0])} ${this.renderAction(actions[1])}
- <gr-dropdown
- id="moreActions"
- link=""
- vertical-offset="32"
- horizontal-align="right"
- @tap-item=${this.handleAction}
- @opened-changed=${(e: ValueChangedEvent<boolean>) =>
- this.classList.toggle('dropdown-open', e.detail.value)}
- ?hidden=${overflowItems.length === 0}
- .items=${overflowItems}
- .disabledIds=${disabledItems}
- >
- <gr-icon icon="more_vert" aria-labelledby="moreMessage"></gr-icon>
- <span id="moreMessage">More</span>
- </gr-dropdown>
- </div>`;
+ return html` ${when(
+ fixAction,
+ () =>
+ html`<div class="actions-shown-on-collapsed">
+ ${this.renderAction(fixAction)}
+ </div> `
+ )}
+ <div class="actions">
+ ${this.renderAction(actions[0])} ${this.renderAction(actions[1])}
+ <gr-dropdown
+ id="moreActions"
+ link=""
+ vertical-offset="32"
+ horizontal-align="right"
+ @tap-item=${this.handleAction}
+ @opened-changed=${(e: ValueChangedEvent<boolean>) =>
+ this.classList.toggle('dropdown-open', e.detail.value)}
+ ?hidden=${overflowItems.length === 0}
+ .items=${overflowItems}
+ .disabledIds=${disabledItems}
+ >
+ <gr-icon icon="more_vert" aria-labelledby="moreMessage"></gr-icon>
+ <span id="moreMessage">More</span>
+ </gr-dropdown>
+ </div>`;
}
private handleAction(e: CustomEvent<Action>) {
@@ -585,24 +611,6 @@ export class GrResultRow extends LitElement {
></gr-checks-action>`;
}
- renderPrimaryActions() {
- const primaryActions = (this.result?.actions ?? []).slice(0, 2);
- if (primaryActions.length === 0) return;
- return html`
- <div class="primaryActions">${primaryActions.map(this.renderAction)}</div>
- `;
- }
-
- renderSecondaryActions() {
- const secondaryActions = (this.result?.actions ?? []).slice(2);
- if (secondaryActions.length === 0) return;
- return html`
- <div class="secondaryActions">
- ${secondaryActions.map(this.renderAction)}
- </div>
- `;
- }
-
renderTag(tag: Tag) {
return html`<button
class="tag ${tag.color}"
@@ -1500,6 +1508,7 @@ export class GrChecksResults extends LitElement {
<tbody @checks-results-filter=${this.handleFilter}>
${repeat(
filtered,
+ // @ts-ignore: temporarily unblock typescript 5.3 migration
result => result.internalResultId,
(result?: RunResult) => html`
<gr-result-row
diff --git a/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts b/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts
index 58b939ced6..89c7fd76d0 100644
--- a/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts
+++ b/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts
@@ -211,7 +211,6 @@ export class GrDiffCheckResult extends LitElement {
}
private renderActions() {
- if (!this.isExpanded) return nothing;
return html`<div class="actions">
${this.renderShowFixButton()}${this.renderPleaseFixButton()}
</div>`;
diff --git a/polygerrit-ui/app/elements/checks/gr-diff-check-result_test.ts b/polygerrit-ui/app/elements/checks/gr-diff-check-result_test.ts
index b913c87d95..5aa266c6ce 100644
--- a/polygerrit-ui/app/elements/checks/gr-diff-check-result_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-diff-check-result_test.ts
@@ -31,31 +31,42 @@ suite('gr-diff-check-result tests', () => {
assert.shadowDom.equal(
element,
`
- <div class="container font-normal warning">
- <div class="header">
- <div class="icon">
- <gr-icon icon="warning" filled></gr-icon>
- </div>
- <div class="name">
- <gr-hovercard-run> </gr-hovercard-run>
- <div class="name" role="button" tabindex="0">FAKE Super Check</div>
- </div>
- <div class="summary">We think that you could improve this.</div>
- <div class="message">
- There is a lot to be said. A lot. I say, a lot.
+ <div class="container font-normal warning">
+ <div class="header">
+ <div class="icon">
+ <gr-icon icon="warning" filled></gr-icon>
+ </div>
+ <div class="name">
+ <gr-hovercard-run> </gr-hovercard-run>
+ <div class="name" role="button" tabindex="0">
+ FAKE Super Check
+ </div>
+ </div>
+ <div class="summary">We think that you could improve this.</div>
+ <div class="message">
+ There is a lot to be said. A lot. I say, a lot.
So please keep reading.
+ </div>
+ <div
+ aria-checked="false"
+ aria-label="Expand result row"
+ class="show-hide"
+ role="switch"
+ tabindex="0"
+ >
+ <gr-icon icon="expand_more"></gr-icon>
+ </div>
</div>
- <div aria-checked="false"
- aria-label="Expand result row"
- class="show-hide"
- role="switch"
- tabindex="0">
- <gr-icon icon="expand_more"></gr-icon>
+ <div class="details">
+ <div class="actions">
+ <gr-checks-action
+ id="please-fix"
+ context="diff-fix"
+ ></gr-checks-action>
+ </div>
</div>
</div>
- <div class="details"></div>
- </div>
- `
+ `
);
});
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
index 94241eaab1..3d21e07d92 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
@@ -10,7 +10,12 @@ export const navigationToken = define<NavigationService>('navigation');
export interface NavigationService {
/**
* This is similar to letting the browser navigate to this URL when the user
- * clicks it, or to just setting `window.location.href` directly.
+ * clicks it, or to just calling `window.location.assign()` directly.
+ *
+ * CAUTION: You should actually use `window.location.assign()` directly for
+ * URLs that are not handled by gr-router. Otherwise we will call
+ * `pushState()` and then `window.location.reload()` from the router, which
+ * will break the browser's back button.
*
* This adds a new entry to the browser location history. Consier using
* `replaceUrl()`, if you want to avoid that.
@@ -23,6 +28,11 @@ export interface NavigationService {
* Navigate to this URL, but replace the current URL in the history instead of
* adding a new one (which is what `setUrl()` would do).
*
+ * CAUTION: You should actually use `window.location.replace()` directly for
+ * URLs that are not handled by gr-router. Otherwise we will call
+ * `replaceState()` and then `window.location.reload()` from the router, which
+ * will break the browser's back button.
+ *
* page.redirect() eventually just calls `window.history.replaceState()`.
*/
replaceUrl(url: string): void;
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-page.ts b/polygerrit-ui/app/elements/core/gr-router/gr-page.ts
index 264c6e0960..cb96d77017 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-page.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-page.ts
@@ -46,12 +46,18 @@ export interface PageOptions {
/**
* The browser `History` API allows `pushState()` to contain an arbitrary state
* object. Our router only sets `path` on the state and inspects it when
- * handling `popstate` events. This interface is internal only.
+ * handling `popstate` events. This interface is for internal use within the
+ * router.
*/
interface PageState {
path?: string;
}
+export const UNHANDLED_URL_PATTERNS = [
+ /^\/log(in|out)(\/(.+))?$/,
+ /^\/plugins\/(.+)$/,
+];
+
const clickEvent = document.ontouchstart ? 'touchstart' : 'click';
export class Page {
@@ -238,6 +244,18 @@ export class Page {
if (this.base && orig === path && window.location.protocol !== 'file:') {
return;
}
+
+ // See issue 40015337: We have to make sure that we only use
+ // show()/pushState() for URLs that gr-router will actually handle.
+ // Calling pushState() tells the browser that both the previous and the
+ // next URL are handled by the same single page application with a
+ // popstate event handler. But if we call pushState() and then
+ // later `window.location.reload()` from the router and a separate page
+ // and document are loaded, then the BACK button will stop working.
+ if (UNHANDLED_URL_PATTERNS.find(pattern => pattern.test(path))) {
+ return;
+ }
+
e.preventDefault();
this.show(orig);
};
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-page_test.ts b/polygerrit-ui/app/elements/core/gr-router/gr-page_test.ts
index d194bf5547..729a15b21a 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-page_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-page_test.ts
@@ -20,14 +20,49 @@ suite('gr-page tests', () => {
page.stop();
});
- test('click handler', async () => {
- const spy = sinon.spy();
- page.registerRoute(/\/settings/, spy);
- const link = await fixture<HTMLAnchorElement>(
- html`<a href="/settings"></a>`
- );
- link.click();
- assert.isTrue(spy.calledOnce);
+ suite('click handler', () => {
+ const clickListener = (e: Event) => e.preventDefault();
+ let spy: sinon.SinonSpy;
+ let link: HTMLAnchorElement;
+
+ setup(async () => {
+ spy = sinon.spy();
+ link = await fixture<HTMLAnchorElement>(html`<a href="/settings"></a>`);
+
+ document.addEventListener('click', clickListener);
+ });
+
+ teardown(() => {
+ document.removeEventListener('click', clickListener);
+ });
+
+ test('click handled by specific route', async () => {
+ page.registerRoute(/\/settings/, spy);
+ link.href = '/settings';
+ link.click();
+ assert.isTrue(spy.calledOnce);
+ });
+
+ test('click handled by default route', async () => {
+ page.registerRoute(/.*/, spy);
+ link.href = '/something';
+ link.click();
+ assert.isTrue(spy.called);
+ });
+
+ test('click not handled for /plugins/... links', async () => {
+ page.registerRoute(/.*/, spy);
+ link.href = '/plugins/gitiles';
+ link.click();
+ assert.isFalse(spy.called);
+ });
+
+ test('click not handled for /login/... links', async () => {
+ page.registerRoute(/.*/, spy);
+ link.href = '/login';
+ link.click();
+ assert.isFalse(spy.called);
+ });
});
test('register route and exit', () => {
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 20585ffd80..ad3e15ca95 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -106,6 +106,7 @@ import {
timeoutPromise,
} from '../../../utils/async-util';
import {Finalizable} from '../../../types/types';
+import {assign} from '../../../utils/location-util';
// TODO: Move all patterns to view model files and use the `Route` interface,
// which will enforce using `RegExp` in its `urlPattern` property.
@@ -120,12 +121,6 @@ const RoutePattern = {
NEW_AGREEMENTS: /^\/settings\/new-agreement\/?/,
REGISTER: /^\/register(\/.*)?$/,
- // Pattern for login and logout URLs intended to be passed-through. May
- // include a return URL.
- // TODO: Maybe this pattern and its handler can just be removed, because
- // passing through is what the default router would eventually do anyway.
- LOG_IN_OR_OUT: /^\/log(in|out)(\/(.+))?$/,
-
// Pattern for a catchall route when no other pattern is matched.
DEFAULT: /.*/,
@@ -171,8 +166,6 @@ const RoutePattern = {
// Matches /admin/repos/<repos>,access.
REPO_DASHBOARDS: /^\/admin\/repos\/(.+),dashboards$/,
- PLUGINS: /^\/plugins\/(.+)$/,
-
// Matches /admin/plugins with optional filter and offset.
PLUGIN_LIST: /^\/admin\/plugins\/?(?:\/q\/filter:(.*?))?(?:,(\d+))?$/,
// Matches /admin/groups with optional filter and offset.
@@ -197,6 +190,10 @@ const RoutePattern = {
CHANGE_ID_QUERY: /^\/id\/(I[0-9a-f]{40})$/,
+ // Matches /c/<changeNum>/[*][/].
+ CHANGE_LEGACY: /^\/c\/(\d+)\/?(.*)$/,
+ CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/,
+
// Matches
// /c/<project>/+/<changeNum>/[<basePatchNum|edit>..][<patchNum|edit>].
// TODO(kaspern): Migrate completely to project based URLs, with backwards
@@ -397,7 +394,7 @@ export class GrRouter implements Finalizable, NavigationService {
setState(state: AppElementParams) {
// TODO: Move this logic into the change model.
if ('repo' in state && state.repo !== undefined && 'changeNum' in state)
- this.restApiService.setInProjectLookup(state.changeNum, state.repo);
+ this.restApiService.addRepoNameToCache(state.changeNum, state.repo);
this.routerModel.setState({view: state.view});
// We are trying to reset the change (view) model when navigating to other
@@ -456,7 +453,11 @@ export class GrRouter implements Finalizable, NavigationService {
*/
redirectToLogin(returnUrl: string) {
const basePath = getBaseUrl() || '';
- this.setUrl(
+ // We are not using `this.getNavigation().setUrl()`, because the login
+ // page is served directly from the backend and is not part of the web
+ // app.
+ assign(
+ window.location,
'/login/' + encodeURIComponent(returnUrl.substring(basePath.length))
);
}
@@ -579,6 +580,8 @@ export class GrRouter implements Finalizable, NavigationService {
* page.show() eventually just calls `window.history.pushState()`.
*/
setUrl(url: string) {
+ // TODO: Use window.location.assign() instead of page.show(), if the URL is
+ // external, i.e. not handled by the router.
this.page.show(url);
}
@@ -589,6 +592,8 @@ export class GrRouter implements Finalizable, NavigationService {
* this.page.redirect() eventually just calls `window.history.replaceState()`.
*/
replaceUrl(url: string) {
+ // TODO: Use window.location.replace() instead of page.redirect(), if the
+ // URL is external, i.e. not handled by the router.
this.redirect(url);
}
@@ -805,10 +810,6 @@ export class GrRouter implements Finalizable, NavigationService {
this.handleRepoRoute(ctx)
);
- this.mapRoute(RoutePattern.PLUGINS, 'handlePassThroughRoute', () =>
- this.handlePassThroughRoute()
- );
-
this.mapRoute(
RoutePattern.PLUGIN_LIST,
'handlePluginListFilterRoute',
@@ -846,6 +847,12 @@ export class GrRouter implements Finalizable, NavigationService {
);
this.mapRoute(
+ RoutePattern.CHANGE_NUMBER_LEGACY,
+ 'handleChangeNumberLegacyRoute',
+ ctx => this.handleChangeNumberLegacyRoute(ctx)
+ );
+
+ this.mapRoute(
RoutePattern.DIFF_EDIT,
'handleDiffEditRoute',
ctx => this.handleDiffEditRoute(ctx),
@@ -875,6 +882,10 @@ export class GrRouter implements Finalizable, NavigationService {
this.handleChangeRoute(ctx)
);
+ this.mapRoute(RoutePattern.CHANGE_LEGACY, 'handleChangeLegacyRoute', ctx =>
+ this.handleChangeLegacyRoute(ctx)
+ );
+
this.mapRoute(
RoutePattern.AGREEMENTS,
'handleAgreementsRoute',
@@ -907,10 +918,6 @@ export class GrRouter implements Finalizable, NavigationService {
this.handleRegisterRoute(ctx)
);
- this.mapRoute(RoutePattern.LOG_IN_OR_OUT, 'handlePassThroughRoute', () =>
- this.handlePassThroughRoute()
- );
-
this.mapRoute(
RoutePattern.IMPROPERLY_ENCODED_PLUS,
'handleImproperlyEncodedPlusRoute',
@@ -1294,6 +1301,14 @@ export class GrRouter implements Finalizable, NavigationService {
this.redirect(ctx.path.replace(LEGACY_QUERY_SUFFIX_PATTERN, ''));
}
+ handleChangeNumberLegacyRoute(ctx: PageContext) {
+ this.redirect(
+ '/c/' +
+ ctx.params[0] +
+ (ctx.querystring.length > 0 ? `?${ctx.querystring}` : '')
+ );
+ }
+
handleChangeRoute(ctx: PageContext) {
// Parameter order is based on the regex group number matched.
const changeNum = Number(ctx.params[1]) as NumericChangeId;
@@ -1339,7 +1354,7 @@ export class GrRouter implements Finalizable, NavigationService {
const repo = ctx.params[0] as RepoName;
const commentId = ctx.params[2] as UrlEncodedCommentId;
- this.restApiService.setInProjectLookup(changeNum, repo);
+ this.restApiService.addRepoNameToCache(changeNum, repo);
const [comments, robotComments, drafts, change] = await Promise.all([
this.restApiService.getDiffComments(changeNum),
this.restApiService.getDiffRobotComments(changeNum),
@@ -1446,6 +1461,26 @@ export class GrRouter implements Finalizable, NavigationService {
this.changeViewModel.setState(state);
}
+ handleChangeLegacyRoute(ctx: PageContext) {
+ const changeNum = Number(ctx.params[0]) as NumericChangeId;
+ if (!changeNum) {
+ this.show404();
+ return;
+ }
+ this.restApiService.getRepoName(changeNum).then(project => {
+ // Show a 404 and terminate if the lookup request failed. Attempting
+ // to redirect after failing to get the project loops infinitely.
+ if (!project) {
+ this.show404();
+ return;
+ }
+ this.redirect(
+ `/c/${project}/+/${changeNum}/${ctx.params[1]}` +
+ (ctx.querystring.length > 0 ? `?${ctx.querystring}` : '')
+ );
+ });
+ }
+
handleLegacyLinenum(ctx: PageContext) {
this.redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1'));
}
@@ -1547,14 +1582,6 @@ export class GrRouter implements Finalizable, NavigationService {
}
/**
- * Handler for routes that should pass through the router and not be caught
- * by the catchall _handleDefaultRoute handler.
- */
- handlePassThroughRoute() {
- windowLocationReload();
- }
-
- /**
* URL may sometimes have /+/ encoded to / /.
* Context: Issue 6888, Issue 7100
*/
@@ -1609,10 +1636,15 @@ export class GrRouter implements Finalizable, NavigationService {
this.show404();
} else {
// Route can be recognized by server, so we pass it to server.
- this.handlePassThroughRoute();
+ this.windowReload();
}
}
+ // Allows stubbing in tests.
+ windowReload() {
+ windowLocationReload();
+ }
+
private show404() {
// Note: the app's 404 display is tightly-coupled with catching 404
// network responses, so we simulate a 404 response status to display it.
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
index ae45326381..6f4e528520 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
@@ -169,17 +169,18 @@ suite('gr-router tests', () => {
const unauthenticatedHandlers = [
'handleBranchListRoute',
'handleChangeIdQueryRoute',
+ 'handleChangeNumberLegacyRoute',
'handleChangeRoute',
'handleCommentRoute',
'handleCommentsRoute',
'handleDiffRoute',
'handleDefaultRoute',
+ 'handleChangeLegacyRoute',
'handleDocumentationRedirectRoute',
'handleDocumentationSearchRoute',
'handleDocumentationSearchRedirectRoute',
'handleLegacyLinenum',
'handleImproperlyEncodedPlusRoute',
- 'handlePassThroughRoute',
'handleProjectDashboardRoute',
'handleLegacyProjectDashboardRoute',
'handleProjectsOldRoute',
@@ -261,7 +262,7 @@ suite('gr-router tests', () => {
let urlPromise: MockPromise<string>;
setup(() => {
- stubRestApi('setInProjectLookup');
+ stubRestApi('addRepoNameToCache');
urlPromise = mockPromise<string>();
redirectStub = sinon
.stub(router, 'redirect')
@@ -332,7 +333,7 @@ suite('gr-router tests', () => {
suite('route handlers', () => {
let redirectStub: sinon.SinonStub;
let setStateStub: sinon.SinonStub;
- let handlePassThroughRoute: sinon.SinonStub;
+ let windowReloadStub: sinon.SinonStub;
let redirectToLoginStub: sinon.SinonStub;
async function checkUrlToState<T extends ViewState>(
@@ -365,18 +366,12 @@ suite('gr-router tests', () => {
assert.equal(redirectToLoginStub.lastCall.firstArg, toUrl);
}
- async function checkUrlNotMatched(url: string) {
- handlePassThroughRoute.reset();
- router.page.show(url);
- await waitUntilCalled(handlePassThroughRoute, 'handlePassThroughRoute');
- }
-
setup(() => {
- stubRestApi('setInProjectLookup');
+ stubRestApi('addRepoNameToCache');
redirectStub = sinon.stub(router, 'redirect');
redirectToLoginStub = sinon.stub(router, 'redirectToLogin');
setStateStub = sinon.stub(router, 'setState');
- handlePassThroughRoute = sinon.stub(router, 'handlePassThroughRoute');
+ windowReloadStub = sinon.stub(router, 'windowReload');
router._testOnly_startRouter();
});
@@ -443,7 +438,7 @@ suite('gr-router tests', () => {
onExit!('', () => {}); // we left page;
router.handleDefaultRoute();
- assert.isTrue(handlePassThroughRoute.calledOnce);
+ assert.isTrue(windowReloadStub.calledOnce);
});
test('IMPROPERLY_ENCODED_PLUS', async () => {
@@ -854,6 +849,21 @@ suite('gr-router tests', () => {
});
suite('CHANGE* / DIFF*', () => {
+ test('CHANGE_NUMBER_LEGACY', async () => {
+ // CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/,
+ await checkRedirect('/12345', '/c/12345');
+ });
+
+ test('CHANGE_LEGACY', async () => {
+ // CHANGE_LEGACY: /^\/c\/(\d+)\/?(.*)$/,
+ stubRestApi('getRepoName').resolves('project' as RepoName);
+ await checkRedirect('/c/1234', '/c/project/+/1234/');
+ await checkRedirect(
+ '/c/1234/comment/6789',
+ '/c/project/+/1234/comment/6789'
+ );
+ });
+
test('DIFF_LEGACY_LINENUM', async () => {
await checkRedirect(
'/c/1234/3..8/foo/bar@321',
@@ -1022,12 +1032,6 @@ suite('gr-router tests', () => {
});
});
- test('LOG_IN_OR_OUT pass through', async () => {
- // LOG_IN_OR_OUT: /^\/log(in|out)(\/(.+))?$/,
- await checkUrlNotMatched('/login/asdf');
- await checkUrlNotMatched('/logout/asdf');
- });
-
test('PLUGIN_SCREEN', async () => {
// PLUGIN_SCREEN: /^\/x\/([\w-]+)\/([\w-]+)\/?/,
await checkUrlToState('/x/foo/bar', {
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
index 98e9eba8c1..368eb22e29 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
@@ -126,7 +126,11 @@ const SEARCH_OPERATORS_WITH_NEGATIONS_SET: ReadonlySet<string> = new Set(
const MAX_AUTOCOMPLETE_RESULTS = 10;
-const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g;
+// 3 types of tokens
+// 1. predicate:expression (?:[^\s":]+:\s*[^\s"]+)
+// 2. quotes with anything inside "[^"]*"
+// 3. anything else like unfinished predicate [^\s"]+
+const TOKENIZE_REGEX = /(?:(?:[^\s":]+:\s*[^\s"]+)|[^\s"]+|"[^"]*")+\s*/g;
export type SuggestionProvider = (
predicate: string,
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
index f67024fffc..bc8da05d1b 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
@@ -256,6 +256,18 @@ suite('gr-search-bar tests', () => {
const s = await element.getSearchSuggestions('is:mergeab');
assert.isEmpty(s);
});
+
+ test('Autocompletes correctly second condition', async () => {
+ const s = await element.getSearchSuggestions('is:open me');
+ assert.equal(s[0].value, 'mergedafter:');
+ });
+
+ test('Autocomplete handles space before expression correctly', async () => {
+ // This previously suggested "mergedafter" (incorrectly) due to the
+ // leading space.
+ const s = await element.getSearchSuggestions('author: me');
+ assert.isEmpty(s);
+ });
});
[
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
index 8b4b52ffe0..48f5a05606 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
@@ -172,7 +172,7 @@ export class GrSmartSearch extends LitElement {
return Promise.resolve([]);
}
return this.restApiService
- .getSuggestedAccounts(
+ .queryAccounts(
expression,
MAX_AUTOCOMPLETE_RESULTS,
/* canSee=*/ undefined,
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts
index 7e3b896046..0551f23698 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts
@@ -25,7 +25,7 @@ suite('gr-smart-search tests', () => {
});
test('Autocompletes accounts', () => {
- stubRestApi('getSuggestedAccounts').callsFake(() =>
+ stubRestApi('queryAccounts').callsFake(() =>
Promise.resolve([
{
name: 'fred',
@@ -39,7 +39,7 @@ suite('gr-smart-search tests', () => {
});
test('Inserts self as option when valid', () => {
- stubRestApi('getSuggestedAccounts').callsFake(() =>
+ stubRestApi('queryAccounts').callsFake(() =>
Promise.resolve([
{
name: 'fred',
@@ -60,7 +60,7 @@ suite('gr-smart-search tests', () => {
});
test('Inserts me as option when valid', () => {
- stubRestApi('getSuggestedAccounts').callsFake(() =>
+ stubRestApi('queryAccounts').callsFake(() =>
Promise.resolve([
{
name: 'fred',
@@ -118,7 +118,7 @@ suite('gr-smart-search tests', () => {
});
test('Autocompletes accounts with no email', () => {
- stubRestApi('getSuggestedAccounts').callsFake(() =>
+ stubRestApi('queryAccounts').callsFake(() =>
Promise.resolve([{name: 'fred'}])
);
return element.fetchAccounts('owner', 'fr').then(s => {
@@ -127,7 +127,7 @@ suite('gr-smart-search tests', () => {
});
test('Autocompletes accounts with email', () => {
- stubRestApi('getSuggestedAccounts').callsFake(() =>
+ stubRestApi('queryAccounts').callsFake(() =>
Promise.resolve([{email: 'fred@goog.co' as EmailAddress}])
);
return element.fetchAccounts('owner', 'fr').then(s => {
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index 7e6e23b34e..0a31677db8 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -315,6 +315,8 @@ export class GrApplyFixDialog extends LitElement {
fixSuggestion.replacements
);
} else {
+ // TODO(b/227463363) Remove once Robot Comments are deprecated.
+ // We don't use this for user suggestions or comments.fix_suggestions.
res = await this.restApiService.getRobotCommentFixPreview(
this.changeNum,
this.patchNum,
@@ -421,7 +423,7 @@ export class GrApplyFixDialog extends LitElement {
this.currentFix.fix_id
);
}
- if (res && res.ok) {
+ if (res?.ok) {
this.getNavigation().setUrl(
createChangeUrl({
change,
@@ -432,7 +434,10 @@ export class GrApplyFixDialog extends LitElement {
this.close(true);
}
this.isApplyFixLoading = false;
- this.reporting.timeEnd(Timing.APPLY_FIX_LOAD);
+ this.reporting.timeEnd(Timing.APPLY_FIX_LOAD, {
+ method: 'apply-fix-dialog',
+ description: this.fixSuggestions?.[0].description,
+ });
}
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts
index 7fc1044819..739468b235 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts
@@ -7,9 +7,13 @@ import '../../../test/common-test-setup';
import './gr-diff-preferences-dialog';
import {GrDiffPreferencesDialog} from './gr-diff-preferences-dialog';
import {createDefaultDiffPrefs} from '../../../constants/constants';
-import {queryAndAssert, stubRestApi, waitUntil} from '../../../test/test-utils';
+import {
+ makePrefixedJSON,
+ queryAndAssert,
+ stubRestApi,
+ waitUntil,
+} from '../../../test/test-utils';
import {DiffPreferencesInfo} from '../../../api/diff';
-import {ParsedJSON} from '../../../types/common';
import {GrButton} from '../../shared/gr-button/gr-button';
import {fixture, html, assert} from '@open-wc/testing';
@@ -95,11 +99,13 @@ suite('gr-diff-preferences-dialog', () => {
assert.isTrue(element.diffPrefsChanged);
assert.isTrue(originalDiffPrefs.line_wrapping);
- stubRestApi('getResponseObject').returns(
- Promise.resolve({
- ...originalDiffPrefs,
- line_wrapping: false,
- } as unknown as ParsedJSON)
+ stubRestApi('saveDiffPreferences').resolves(
+ new Response(
+ makePrefixedJSON({
+ ...originalDiffPrefs,
+ line_wrapping: false,
+ })
+ )
);
queryAndAssert<GrButton>(element, '#saveButton').click();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index acb1ef3d6b..bc2d3b5872 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -49,6 +49,7 @@ import {
RevisionPatchSetNum,
Comment,
CommentMap,
+ DropdownLink,
} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo, WebLinkInfo} from '../../../types/diff';
import {ParsedChangeInfo} from '../../../types/types';
@@ -105,13 +106,18 @@ import {
} from '../../../models/change/files-model';
import {isImageDiff} from '../../../utils/diff-util';
import {formStyles} from '../../../styles/form-styles';
+import {NormalizedFileInfo} from '../../change/gr-file-list/gr-file-list';
+import {configModelToken} from '../../../models/config/config-model';
-const LOADING_BLAME = 'Loading blame...';
+const LOADING_BLAME = 'Loading blame information. This may take a while ...';
const LOADED_BLAME = 'Blame loaded';
// Time in which pressing n key again after the toast navigates to next file
const NAVIGATE_TO_NEXT_FILE_TIMEOUT_MS = 5000;
+// Files larger than this cannot be downloaded.
+const FILE_DOWNLOAD_LIMIT_BYTES = 50 * 1000 * 1000;
+
// visible for testing
export interface Files {
/** All file paths sorted by `specialFilePathCompare`. */
@@ -195,12 +201,18 @@ export class GrDiffView extends LitElement {
@state() path?: string;
+ @state() file?: NormalizedFileInfo;
+
@state() private shownSidebar?: string;
/** Allows us to react when the user switches to the DIFF view. */
// Private but used in tests.
@state() isActiveChildView = false;
+ // Whether to allow the "Show Blame button"
+ @state()
+ allowBlame = false;
+
// Private but used in tests.
@state()
loggedIn = false;
@@ -254,6 +266,8 @@ export class GrDiffView extends LitElement {
private readonly getViewModel = resolve(this, changeViewModelToken);
+ private readonly getConfigModel = resolve(this, configModelToken);
+
private throttledToggleFileReviewed?: (e: KeyboardEvent) => void;
@state()
@@ -415,6 +429,11 @@ export class GrDiffView extends LitElement {
);
subscribe(
this,
+ () => this.getFilesModel().file$(this.getViewModel().diffPath$),
+ file => (this.file = file)
+ );
+ subscribe(
+ this,
() => this.getViewModel().diffLine$,
line => (this.focusLineNum = line)
);
@@ -445,6 +464,13 @@ export class GrDiffView extends LitElement {
}
);
+ subscribe(
+ this,
+ () => this.getConfigModel().serverConfig$,
+ serverConfig =>
+ (this.allowBlame = serverConfig?.change.allow_blame ?? false)
+ );
+
// When user initially loads the diff view, we want to automatically mark
// the file as reviewed if they have it enabled. We can't observe these
// properties since the method will be called anytime a property updates
@@ -924,13 +950,12 @@ export class GrDiffView extends LitElement {
<gr-endpoint-param
name="onTrigger"
.value=${(pluginName: string) => {
- this.shownSidebar =
- this.shownSidebar === pluginName ? undefined : pluginName;
+ const closeSidebar = this.shownSidebar === pluginName;
+ this.shownSidebar = closeSidebar ? undefined : pluginName;
this.getUserModel().updatePreferences({
- diff_page_sidebar:
- this.shownSidebar === pluginName
- ? 'NONE'
- : `plugin-${pluginName}`,
+ diff_page_sidebar: closeSidebar
+ ? 'NONE'
+ : `plugin-${pluginName}`,
});
}}
></gr-endpoint-param>
@@ -1022,6 +1047,7 @@ export class GrDiffView extends LitElement {
<gr-patch-range-select
id="rangeSelect"
.filesWeblinks=${this.filesWeblinks}
+ .path=${this.path}
@patch-range-change=${this.handlePatchChange}
>
</gr-patch-range-select>
@@ -1031,6 +1057,9 @@ export class GrDiffView extends LitElement {
link=""
down-arrow=""
.items=${this.computeDownloadDropdownLinks()}
+ .disabledIds=${this.isTooLargeForDownload()
+ ? ['left-content', 'right-content']
+ : []}
horizontal-align="left"
>
<span class="downloadTitle"> Download </span>
@@ -1040,26 +1069,9 @@ export class GrDiffView extends LitElement {
}
private renderRightControls() {
- const blameLoaderClass =
- !isMagicPath(this.path) && !isImageDiff(this.diff) ? 'show' : '';
- const blameToggleLabel =
- this.isBlameLoaded && !this.isBlameLoading ? 'Hide blame' : 'Show blame';
const diffModeSelectorClass = !this.diff || this.diff.binary ? 'hide' : '';
return html` <div class="rightControls">
- ${this.renderSidebarTriggers()}
- <span class="blameLoader ${blameLoaderClass}">
- <gr-button
- link=""
- id="toggleBlame"
- title=${this.createTitle(
- Shortcut.TOGGLE_BLAME,
- ShortcutSection.DIFFS
- )}
- ?disabled=${this.isBlameLoading}
- @click=${this.toggleBlame}
- >${blameToggleLabel}</gr-button
- >
- </span>
+ ${this.renderSidebarTriggers()} ${this.renderBlameButton()}
${when(
this.computeCanEdit(),
() => html`
@@ -1129,6 +1141,26 @@ export class GrDiffView extends LitElement {
</div>`;
}
+ private renderBlameButton() {
+ if (!this.allowBlame) return;
+ const blameLoaderClass =
+ !isMagicPath(this.path) && !isImageDiff(this.diff) ? 'show' : '';
+ let blameToggleLabel = 'Loading blame ...';
+ if (!this.isBlameLoading) {
+ blameToggleLabel = this.isBlameLoaded ? 'Hide blame' : 'Show blame';
+ }
+ return html` <span class="blameLoader ${blameLoaderClass}">
+ <gr-button
+ link=""
+ id="toggleBlame"
+ title=${this.createTitle(Shortcut.TOGGLE_BLAME, ShortcutSection.DIFFS)}
+ ?disabled=${this.isBlameLoading}
+ @click=${this.toggleBlame}
+ >${blameToggleLabel}</gr-button
+ >
+ </span>`;
+ }
+
private renderDialogs() {
return html`
<gr-apply-fix-dialog id="applyFixDialog"></gr-apply-fix-dialog>
@@ -1611,14 +1643,18 @@ export class GrDiffView extends LitElement {
this.updateUrlToDiffUrl(lineNumber as number, e.detail.side === Side.LEFT);
}
+ private isTooLargeForDownload() {
+ return (this.file?.size ?? 0) > FILE_DOWNLOAD_LIMIT_BYTES;
+ }
+
// Private but used in tests.
- computeDownloadDropdownLinks() {
+ computeDownloadDropdownLinks(): DropdownLink[] {
if (!this.change?.project) return [];
if (!this.changeNum) return [];
if (!this.patchRange) return [];
if (!this.path) return [];
- const links = [
+ const links: DropdownLink[] = [
{
url: this.computeDownloadPatchLink(
this.change.project,
@@ -1630,34 +1666,45 @@ export class GrDiffView extends LitElement {
},
];
- if (this.diff && this.diff.meta_a) {
- let leftPath = this.path;
- if (this.diff.change_type === 'RENAMED') {
- leftPath = this.diff.meta_a.name;
- }
+ if (this.isTooLargeForDownload()) {
links.push({
- url: this.computeDownloadFileLink(
- this.change.project,
- this.changeNum,
- this.patchRange,
- leftPath,
- true
- ),
- name: 'Left Content',
+ id: 'left-content',
+ name: 'Left Content (Too Large)',
});
- }
-
- if (this.diff && this.diff.meta_b) {
links.push({
- url: this.computeDownloadFileLink(
- this.change.project,
- this.changeNum,
- this.patchRange,
- this.path,
- false
- ),
- name: 'Right Content',
+ id: 'right-content',
+ name: 'Right Content (Too Large)',
});
+ } else {
+ if (this.diff && this.diff.meta_a) {
+ let leftPath = this.path;
+ if (this.diff.change_type === 'RENAMED') {
+ leftPath = this.diff.meta_a.name;
+ }
+ links.push({
+ url: this.computeDownloadFileLink(
+ this.change.project,
+ this.changeNum,
+ this.patchRange,
+ leftPath,
+ true
+ ),
+ name: 'Left Content',
+ });
+ }
+
+ if (this.diff && this.diff.meta_b) {
+ links.push({
+ url: this.computeDownloadFileLink(
+ this.change.project,
+ this.changeNum,
+ this.patchRange,
+ this.path,
+ false
+ ),
+ name: 'Right Content',
+ });
+ }
}
return links;
@@ -1742,12 +1789,13 @@ export class GrDiffView extends LitElement {
* Otherwise hide it.
*/
private toggleBlame() {
+ if (!this.allowBlame) return;
assertIsDefined(this.diffHost, 'diffHost');
if (this.isBlameLoaded) {
this.diffHost.clearBlame();
- return;
+ } else {
+ this.loadBlame();
}
- this.loadBlame();
}
private handleToggleHideAllCommentThreads() {
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
index 6bf1adccbf..28fcfb818c 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
@@ -98,6 +98,9 @@ export class GrPatchRangeSelect extends LitElement {
@state()
changeNum?: NumericChangeId;
+ @property()
+ path?: string;
+
@property({type: Object})
filesWeblinks?: FilesWebLinks;
@@ -349,6 +352,7 @@ export class GrPatchRangeSelect extends LitElement {
value: patchNum,
commentThreads: this.changeComments?.computeCommentThreads(
{
+ path: this.path,
patchNum,
},
true
@@ -429,6 +433,7 @@ export class GrPatchRangeSelect extends LitElement {
const commentThreadCount = this.changeComments.computeCommentThreads(
{
+ path: this.path,
patchNum,
},
true
@@ -436,7 +441,10 @@ export class GrPatchRangeSelect extends LitElement {
const commentThreadString = pluralize(commentThreadCount, 'comment');
const unresolvedCount = this.changeComments.computeUnresolvedNum(
- {patchNum},
+ {
+ path: this.path,
+ patchNum,
+ },
true
);
const unresolvedString =
@@ -487,10 +495,12 @@ export class GrPatchRangeSelect extends LitElement {
if (target === this.patchNumDropdown) {
if (detail.patchNum === patchSetValue) return;
this.reporting.reportInteraction('right-patchset-changed', {
+ path: this.path,
previous: detail.patchNum,
current: patchSetValue,
latest: latestPatchNum,
commentCount: this.changeComments?.computeCommentThreads({
+ path: this.path,
patchNum: patchSetValue,
}).length,
});
@@ -498,9 +508,11 @@ export class GrPatchRangeSelect extends LitElement {
} else {
if (detail.basePatchNum === patchSetValue) return;
this.reporting.reportInteraction('left-patchset-changed', {
+ path: this.path,
previous: detail.basePatchNum,
current: patchSetValue,
commentCount: this.changeComments?.computeCommentThreads({
+ path: this.path,
patchNum: patchSetValue,
}).length,
});
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
index e7ed97bb91..65c6f1a083 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
@@ -384,6 +384,14 @@ suite('gr-patch-range-select tests', () => {
' (3 comments, 1 unresolved)'
);
+ // Test string for specific file path.
+ element.path = 'foo';
+ assert.equal(
+ element.computePatchSetCommentsString(1 as PatchSetNum),
+ ' (1 comment, 1 unresolved)'
+ );
+ element.path = undefined;
+
// Test string with no unresolved comments.
delete comments['foo'];
element.changeComments = new ChangeComments(comments);
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index dc3bf7b417..a54c8bc2f6 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -479,8 +479,8 @@ export class GrEditorView extends LitElement {
this.showAlert(PUBLISHING_EDIT_MSG);
- // restApiService has some quirks where it will still call .then() with
- // undefined or Response status 429 when it hits an error.
+ // restApiService return undefined if server response with non-200 error
+ // code.
this.restApiService
.executeChangeAction(
changeNum,
@@ -491,10 +491,7 @@ export class GrEditorView extends LitElement {
handleError
)
.then(res => {
- if (
- res === undefined ||
- (res instanceof Response && res.status === 429)
- ) {
+ if (res === undefined) {
// In an error case we should not navigate and lose edits.
return;
}
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
index c86f02f656..e20d66d44a 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
@@ -291,7 +291,7 @@ suite('gr-editor-view tests', () => {
test('file modification and publish', async () => {
const saveSpy = sinon.spy(element, 'saveEdit');
const alertStub = sinon.stub(element, 'showAlert');
- const changeActionsStub = stubRestApi('executeChangeAction');
+ const changeActionsStub = stubRestApi('executeChangeAction').resolves();
saveFileStub.returns(Promise.resolve({ok: true}));
element.newContent = newText;
await element.updateComplete;
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
index d1a9932c7a..e1641b054a 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
@@ -47,7 +47,7 @@ export class GrEditPreferences extends LitElement {
@state() private originalEditPrefs?: EditPreferencesInfo;
- private readonly getUserModel = resolve(this, userModelToken);
+ readonly getUserModel = resolve(this, userModelToken);
constructor() {
super();
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.ts b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.ts
index bd682b81f8..d45ff5950f 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.ts
@@ -5,9 +5,14 @@
*/
import '../../../test/common-test-setup';
import './gr-edit-preferences';
-import {queryAll, stubRestApi} from '../../../test/test-utils';
+import {
+ makePrefixedJSON,
+ queryAll,
+ stubRestApi,
+ waitUntil,
+} from '../../../test/test-utils';
import {GrEditPreferences} from './gr-edit-preferences';
-import {EditPreferencesInfo, ParsedJSON} from '../../../types/common';
+import {EditPreferencesInfo} from '../../../types/common';
import {IronInputElement} from '@polymer/iron-input';
import {createDefaultEditPrefs} from '../../../constants/constants';
import {fixture, html, assert} from '@open-wc/testing';
@@ -194,16 +199,18 @@ suite('gr-edit-preferences tests', () => {
assert.isTrue(element.hasUnsavedChanges());
- const getResponseObjStub = stubRestApi('getResponseObject').returns(
- Promise.resolve(element.editPrefs! as unknown as ParsedJSON)
+ const savePrefStub = stubRestApi('saveEditPreferences').resolves(
+ new Response(makePrefixedJSON(element.editPrefs))
);
await element.save();
+ // Wait for model state update, since this is not awaited by element.save()
+ await waitUntil(
+ () => !element.getUserModel().getState().editPreferences?.show_tabs
+ );
- assert.isTrue(getResponseObjStub.called);
-
+ assert.isTrue(savePrefStub.called);
assert.isFalse(element.editPrefs?.show_tabs);
-
assert.isFalse(element.hasUnsavedChanges());
});
});
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
index 47d992f913..985f9be4bb 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
@@ -24,13 +24,16 @@ export class GrHttpPassword extends LitElement {
generatedPasswordModal?: HTMLDialogElement;
@property({type: String})
- _username?: string;
+ username?: string;
@property({type: String})
- _generatedPassword?: string;
+ generatedPassword?: string;
@property({type: String})
- _passwordUrl: string | null = null;
+ status?: string;
+
+ @property({type: String})
+ passwordUrl: string | null = null;
private readonly restApiService = getAppContext().restApiService;
@@ -45,7 +48,7 @@ export class GrHttpPassword extends LitElement {
promises.push(
this.restApiService.getAccount().then(account => {
if (account) {
- this._username = account.username;
+ this.username = account.username;
}
})
);
@@ -53,9 +56,9 @@ export class GrHttpPassword extends LitElement {
promises.push(
this.restApiService.getConfig().then(info => {
if (info) {
- this._passwordUrl = info.auth.http_password_url || null;
+ this.passwordUrl = info.auth.http_password_url || null;
} else {
- this._passwordUrl = null;
+ this.passwordUrl = null;
}
})
);
@@ -104,18 +107,18 @@ export class GrHttpPassword extends LitElement {
override render() {
return html` <div class="gr-form-styles">
- <div ?hidden=${!!this._passwordUrl}>
+ <div ?hidden=${!!this.passwordUrl}>
<section>
<span class="title">Username</span>
- <span class="value">${this._username ?? ''}</span>
+ <span class="value">${this.username ?? ''}</span>
</section>
<gr-button id="generateButton" @click=${this._handleGenerateTap}
>Generate new password</gr-button
>
</div>
- <span ?hidden=${!this._passwordUrl}>
+ <span ?hidden=${!this.passwordUrl}>
<a
- href=${this._passwordUrl!}
+ href=${this.passwordUrl!}
target="_blank"
rel="noopener noreferrer"
>
@@ -132,12 +135,12 @@ export class GrHttpPassword extends LitElement {
<div class="gr-form-styles">
<section id="generatedPasswordDisplay">
<span class="title">New Password:</span>
- <span class="value">${this._generatedPassword}</span>
+ <span class="value">${this.status || this.generatedPassword}</span>
<gr-copy-clipboard
hasTooltip=""
buttonTitle="Copy password to clipboard"
hideInput=""
- .text=${this._generatedPassword}
+ .text=${this.status ? '' : this.generatedPassword}
>
</gr-copy-clipboard>
</section>
@@ -153,10 +156,15 @@ export class GrHttpPassword extends LitElement {
}
_handleGenerateTap() {
- this._generatedPassword = 'Generating...';
+ this.status = 'Generating...';
this.generatedPasswordModal?.showModal();
this.restApiService.generateAccountHttpPassword().then(newPassword => {
- this._generatedPassword = newPassword;
+ if (newPassword) {
+ this.generatedPassword = newPassword;
+ this.status = undefined;
+ } else {
+ this.status = 'Failed to generate';
+ }
});
}
@@ -165,6 +173,7 @@ export class GrHttpPassword extends LitElement {
}
_generatedPasswordModalClosed() {
- this._generatedPassword = '';
+ this.status = undefined;
+ this.generatedPassword = '';
}
}
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.ts b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.ts
index 7fe3be6494..1abf8a75c4 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.ts
@@ -102,27 +102,27 @@ suite('gr-http-password tests', () => {
})
);
- assert.isNotOk(element._generatedPassword);
+ assert.isNotOk(element.generatedPassword);
button.click();
assert.isTrue(generateStub.called);
- assert.equal(element._generatedPassword, 'Generating...');
+ assert.equal(element.status, 'Generating...');
generateStub.lastCall.returnValue.then(() => {
generateResolve(nextPassword);
- assert.equal(element._generatedPassword, nextPassword);
+ assert.equal(element.generatedPassword, nextPassword);
});
});
test('without http_password_url', () => {
- assert.isNull(element._passwordUrl);
+ assert.isNull(element.passwordUrl);
});
test('with http_password_url', async () => {
config.auth.http_password_url = 'http://example.com/';
await element.loadData();
- assert.isNotNull(element._passwordUrl);
- assert.equal(element._passwordUrl, config.auth.http_password_url);
+ assert.isNotNull(element.passwordUrl);
+ assert.equal(element.passwordUrl, config.auth.http_password_url);
});
});
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
index 4d4834e3f2..29ece75b00 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
@@ -71,6 +71,8 @@ import {modalStyles} from '../../../styles/gr-modal-styles';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {getDocUrl, rootUrl} from '../../../utils/url-util';
import {configModelToken} from '../../../models/config/config-model';
+import {SuggestionsProvider} from '../../../api/suggestions';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
const HTTP_AUTH = ['HTTP', 'HTTP_LDAP'];
@@ -118,6 +120,9 @@ export class GrSettingsView extends LitElement {
@query('#allowBrowserNotifications')
allowBrowserNotifications?: HTMLInputElement;
+ @query('#allowSuggestCodeWhileCommenting')
+ allowSuggestCodeWhileCommenting?: HTMLInputElement;
+
@query('#disableKeyboardShortcuts')
disableKeyboardShortcuts!: HTMLInputElement;
@@ -196,6 +201,9 @@ export class GrSettingsView extends LitElement {
@state() private docsBaseUrl = '';
+ @state()
+ suggestionsProvider?: SuggestionsProvider;
+
// private but used in test
public _testOnly_loadingPromise?: Promise<void>;
@@ -212,6 +220,8 @@ export class GrSettingsView extends LitElement {
private readonly getConfigModel = resolve(this, configModelToken);
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
+
constructor() {
super();
subscribe(
@@ -265,6 +275,14 @@ export class GrSettingsView extends LitElement {
// we need to manually calling scrollIntoView when hash changed
document.addEventListener('location-change', this.handleLocationChange);
fireTitleChange('Settings');
+ this.getPluginLoader()
+ .awaitPluginsLoaded()
+ .then(() => {
+ const suggestionsPlugins =
+ this.getPluginLoader().pluginsModel.getState().suggestionsPlugins;
+ // We currently support results from only 1 provider.
+ this.suggestionsProvider = suggestionsPlugins?.[0]?.provider;
+ });
}
override firstUpdated() {
@@ -451,6 +469,7 @@ export class GrSettingsView extends LitElement {
${this.renderTheme()} ${this.renderChangesPerPages()}
${this.renderDateTimeFormat()} ${this.renderEmailNotification()}
${this.renderEmailFormat()} ${this.renderBrowserNotifications()}
+ ${this.renderGenerateSuggestionWhenCommenting()}
${this.renderDefaultBaseForMerges()}
${this.renderRelativeDateInChangeTable()} ${this.renderDiffView()}
${this.renderShowSizeBarsInFileList()}
@@ -867,6 +886,45 @@ export class GrSettingsView extends LitElement {
`;
}
+ private renderGenerateSuggestionWhenCommenting() {
+ if (
+ !this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT) ||
+ !this.suggestionsProvider
+ )
+ return nothing;
+ return html`
+ <section id="allowSuggestCodeWhileCommentingSection">
+ <div class="title">
+ <label for="allowSuggestCodeWhileCommenting"
+ >Allow generating suggestions while commenting</label
+ >
+ <a
+ href=${getDocUrl(
+ this.docsBaseUrl,
+ 'user-suggest-edits.html#_generate_suggestion'
+ )}
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <gr-icon icon="help" title="read documentation"></gr-icon>
+ </a>
+ </div>
+ <span class="value">
+ <input
+ id="allowSuggestCodeWhileCommenting"
+ type="checkbox"
+ ?checked=${this.localPrefs.allow_suggest_code_while_commenting}
+ @change=${() => {
+ this.localPrefs.allow_suggest_code_while_commenting =
+ this.allowSuggestCodeWhileCommenting!.checked;
+ this.prefsChanged = true;
+ }}
+ />
+ </span>
+ </section>
+ `;
+ }
+
private renderDefaultBaseForMerges() {
if (!this.localPrefs.default_base_for_merges) return nothing;
return nothing;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
index d7c6c8b391..9709df2d39 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
@@ -119,6 +119,9 @@ export class GrAccountChip extends LitElement {
gr-icon {
font-size: 1.2rem;
}
+ gr-icon[icon='close'] {
+ margin-top: var(--spacing-xxs);
+ }
.container gr-account-label::part(gr-account-label-text) {
color: var(--deemphasized-text-color);
}
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
index cf7ff22090..4c73fcfbd8 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
@@ -192,7 +192,12 @@ export class GrAccountLabel extends LitElement {
override async updated() {
assertIsDefined(this.account, 'account');
const account = await this.getAccountsModel().fillDetails(this.account);
- if (account) this.account = account;
+ // AccountInfo returned by fillDetails has the email property set
+ // to the primary email of the account. This poses a problem in
+ // cases where a secondary email is used as the committer or author
+ // email. Therefore, only fill in the missing details to avoid
+ // displaying incorrect author or committer email.
+ if (account) this.account = Object.assign(account, this.account);
}
override render() {
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
index 49d3c7c771..5ec33d947b 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
@@ -232,6 +232,7 @@ export class GrButton extends LitElement {
this.reporting.reportInteraction(Interaction.BUTTON_CLICK, {
path: getEventPath(e),
+ text: this.innerText,
});
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts
index 1cf05ab0a3..145e39df5a 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts
@@ -206,13 +206,14 @@ suite('gr-button tests', () => {
assert.equal(reportStub.lastCall.args[0], 'button-click');
assert.deepEqual(reportStub.lastCall.args[1], {
path: 'html>body>div>gr-button',
+ text: '',
});
});
test('report event after click on nested', async () => {
const nestedElement = await fixture<HTMLDivElement>(html`
<div id="test">
- <gr-button class="testBtn"></gr-button>
+ <gr-button class="testBtn">Click Me</gr-button>
</div>
`);
queryAndAssert<GrButton>(nestedElement, 'gr-button').click();
@@ -220,6 +221,7 @@ suite('gr-button tests', () => {
assert.equal(reportStub.lastCall.args[0], 'button-click');
assert.deepEqual(reportStub.lastCall.args[1], {
path: 'html>body>div>div#test>gr-button.testBtn',
+ text: 'CLICK ME',
});
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 1e91345b86..e9629b4757 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -15,6 +15,7 @@ import '../gr-tooltip-content/gr-tooltip-content';
import '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
import '../gr-account-label/gr-account-label';
import '../gr-suggestion-diff-preview/gr-suggestion-diff-preview';
+import '../gr-fix-suggestions/gr-fix-suggestions';
import {getAppContext} from '../../../services/app-context';
import {css, html, LitElement, nothing, PropertyValues} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
@@ -63,7 +64,11 @@ import {CommentSide, SpecialFilePath} from '../../../constants/constants';
import {Subject} from 'rxjs';
import {debounceTime} from 'rxjs/operators';
import {changeModelToken} from '../../../models/change/change-model';
-import {isBase64FileContent} from '../../../api/rest-api';
+import {
+ ChangeInfo,
+ FixSuggestionInfo,
+ isBase64FileContent,
+} from '../../../api/rest-api';
import {createDiffUrl} from '../../../models/views/change';
import {userModelToken} from '../../../models/user/user-model';
import {modalStyles} from '../../../styles/gr-modal-styles';
@@ -75,11 +80,19 @@ import {
} from '../gr-comment-model/gr-comment-model';
import {formStyles} from '../../../styles/form-styles';
import {Interaction} from '../../../constants/reporting';
-import {Suggestion} from '../../../api/suggestions';
+import {Suggestion, SuggestionsProvider} from '../../../api/suggestions';
+import {when} from 'lit/directives/when.js';
+import {getDocUrl} from '../../../utils/url-util';
+import {configModelToken} from '../../../models/config/config-model';
+import {getFileExtension} from '../../../utils/file-util';
+import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
+import {deepEqual} from '../../../utils/deep-util';
// visible for testing
export const AUTO_SAVE_DEBOUNCE_DELAY_MS = 2000;
-export const GENERATE_SUGGESTION_DEBOUNCE_DELAY_MS = 1500;
+export const GENERATE_SUGGESTION_DEBOUNCE_DELAY_MS = 500;
+export const ENABLE_GENERATE_SUGGESTION_STORAGE_KEY =
+ 'enableGenerateSuggestionStorageKeyForCommentWithId-';
declare global {
interface HTMLElementEventMap {
@@ -87,6 +100,7 @@ declare global {
'comment-unresolved-changed': ValueChangedEvent<boolean>;
'comment-text-changed': ValueChangedEvent<string>;
'comment-anchor-tap': CustomEvent<CommentAnchorTapEventDetail>;
+ 'apply-user-suggestion': CustomEvent;
}
}
@@ -211,7 +225,20 @@ export class GrComment extends LitElement {
generatedSuggestion?: Suggestion;
@state()
- generatedReplacementId?: string;
+ generatedFixSuggestion: FixSuggestionInfo | undefined =
+ this.comment?.fix_suggestions?.[0];
+
+ @state()
+ generatedSuggestionId?: string;
+
+ @state()
+ addedGeneratedSuggestion?: string;
+
+ @state()
+ suggestionsProvider?: SuggestionsProvider;
+
+ @state()
+ suggestionLoading = false;
@property({type: Boolean, attribute: 'show-patchset'})
showPatchset = false;
@@ -231,6 +258,8 @@ export class GrComment extends LitElement {
@state()
commentedText?: string;
+ @state() private docsBaseUrl = '';
+
private readonly restApiService = getAppContext().restApiService;
private readonly reporting = getAppContext().reportingService;
@@ -243,6 +272,10 @@ export class GrComment extends LitElement {
private readonly getPluginLoader = resolve(this, pluginLoaderToken);
+ private readonly getConfigModel = resolve(this, configModelToken);
+
+ private readonly getStorage = resolve(this, storageServiceToken);
+
private readonly flagsService = getAppContext().flagsService;
private readonly shortcuts = new ShortcutController(this);
@@ -284,8 +317,16 @@ export class GrComment extends LitElement {
for (const modifier of [Modifier.CTRL_KEY, Modifier.META_KEY]) {
this.shortcuts.addLocal(
{key: Key.ENTER, modifiers: [modifier]},
- () => {
+ e => {
this.save();
+ // We don't stop propagation for patchset comment
+ // (this.permanentEditingMode = true), but we stop it for normal
+ // comments. This prevents accidentally sending a reply when
+ // editing/saving them in the reply dialog.
+ if (!this.permanentEditingMode) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
},
{preventDefault: false}
);
@@ -297,6 +338,9 @@ export class GrComment extends LitElement {
this.save();
});
}
+ this.addEventListener('apply-user-suggestion', () => {
+ this.handleAppliedFix();
+ });
this.addEventListener('open-user-suggest-preview', e => {
this.handleShowFix(e.detail.code);
});
@@ -338,7 +382,15 @@ export class GrComment extends LitElement {
this.autoSave();
}
);
- if (this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT)) {
+ subscribe(
+ this,
+ () => this.getConfigModel().docsBaseUrl$,
+ docsBaseUrl => (this.docsBaseUrl = docsBaseUrl)
+ );
+ if (
+ this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT) ||
+ this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT_V2)
+ ) {
subscribe(
this,
() =>
@@ -346,14 +398,47 @@ export class GrComment extends LitElement {
debounceTime(GENERATE_SUGGESTION_DEBOUNCE_DELAY_MS)
),
() => {
- if (this.generateSuggestion) {
- this.generateSuggestEdit();
+ this.generateSuggestEdit();
+ }
+ );
+ subscribe(
+ this,
+ () => this.getUserModel().preferences$,
+ prefs => {
+ if (
+ this.generateSuggestion !==
+ !!prefs.allow_suggest_code_while_commenting
+ ) {
+ this.generateSuggestion =
+ !!prefs.allow_suggest_code_while_commenting;
}
}
);
}
}
+ override connectedCallback() {
+ super.connectedCallback();
+ this.getPluginLoader()
+ .awaitPluginsLoaded()
+ .then(() => {
+ const suggestionsPlugins =
+ this.getPluginLoader().pluginsModel.getState().suggestionsPlugins;
+ // We currently support results from only 1 provider.
+ this.suggestionsProvider = suggestionsPlugins?.[0]?.provider;
+ });
+
+ if (this.comment?.id) {
+ const generateSuggestionStoredContent =
+ this.getStorage().getEditableContentItem(
+ ENABLE_GENERATE_SUGGESTION_STORAGE_KEY + this.comment.id
+ );
+ if (generateSuggestionStoredContent?.message === 'false') {
+ this.generateSuggestion = false;
+ }
+ }
+ }
+
override disconnectedCallback() {
// Clean up emoji dropdown.
if (this.textarea) this.textarea.closeDropdown();
@@ -550,6 +635,16 @@ export class GrComment extends LitElement {
color: var(--selected-foreground);
margin-right: var(--spacing-xl);
}
+ /* The basics of .loadingSpin are defined in shared styles. */
+ .loadingSpin {
+ width: calc(var(--line-height-normal) - 2px);
+ height: calc(var(--line-height-normal) - 2px);
+ display: inline-block;
+ vertical-align: top;
+ position: relative;
+ /* Making up for the 2px reduced height above. */
+ top: 1px;
+ }
`,
];
}
@@ -580,7 +675,8 @@ export class GrComment extends LitElement {
<gr-endpoint-slot name="above-actions"></gr-endpoint-slot>
${this.renderHumanActions()} ${this.renderRobotActions()}
</div>
- ${this.renderGeneratedSuggestionPreview()}
+ ${/* if this.editing */ this.renderGeneratedSuggestionPreview()}
+ ${/* if !this.editing */ this.renderFixSuggestionPreview()}
</div>
</gr-endpoint-decorator>
${this.renderConfirmDialog()}
@@ -824,7 +920,7 @@ export class GrComment extends LitElement {
<input
type="checkbox"
id="resolvedCheckbox"
- ?checked=${!this.unresolved}
+ .checked=${!this.unresolved}
@change=${this.handleToggleResolved}
/>
Resolved
@@ -916,61 +1012,98 @@ export class GrComment extends LitElement {
`;
}
- private showGeneratedSuggestion() {
+ private renderFixSuggestionPreview() {
+ if (
+ !this.comment?.fix_suggestions ||
+ this.editing ||
+ isRobot(this.comment) ||
+ this.collapsed
+ )
+ return nothing;
+ return html`<gr-fix-suggestions
+ .comment=${this.comment}
+ ></gr-fix-suggestions>`;
+ }
+
+ // private but used in test
+ showGeneratedSuggestion() {
return (
- this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT) &&
+ (this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT) ||
+ this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT_V2)) &&
+ this.suggestionsProvider &&
this.editing &&
!this.permanentEditingMode &&
this.comment &&
+ this.comment.path &&
this.comment.path !== SpecialFilePath.PATCHSET_LEVEL_COMMENTS &&
this.comment.path !== SpecialFilePath.COMMIT_MESSAGE &&
+ (!this.suggestionsProvider.supportedFileExtensions ||
+ this.suggestionsProvider.supportedFileExtensions.includes(
+ getFileExtension(this.comment.path)
+ )) &&
this.comment === this.comments?.[0] && // Is first comment
(this.comment.range || this.comment.line) && // Disabled for File comments
- !hasUserSuggestion(this.comment)
+ !hasUserSuggestion(this.comment) &&
+ this.getChangeModel().getChange()?.is_private !== true
);
}
private renderGeneratedSuggestionPreview() {
if (
+ !this.editing ||
!this.showGeneratedSuggestion() ||
- !this.generateSuggestion ||
- !this.generatedSuggestion
+ !this.generateSuggestion
)
return nothing;
- // TODO(milutin): This is temporary warning, will be removed, once we are
- // able to change range of a comment
- if (this.generatedSuggestion.newRange) {
- const range = this.generatedSuggestion.newRange;
- return html`<div class="info">
- <gr-icon icon="info" filled></gr-icon>
- There is a suggestion in range (${range.start_line}, ${range.end_line})
- </div>`;
+ if (!isDraft(this.comment)) return nothing;
+
+ if (this.generatedFixSuggestion) {
+ return html`<gr-suggestion-diff-preview
+ .fixSuggestionInfo=${this.generatedFixSuggestion}
+ ></gr-suggestion-diff-preview>`;
+ } else if (this.generatedSuggestion) {
+ return html`<gr-suggestion-diff-preview
+ .showAddSuggestionButton=${true}
+ .suggestion=${this.generatedSuggestion?.replacement}
+ .uuid=${this.generatedSuggestionId}
+ ></gr-suggestion-diff-preview>`;
+ } else {
+ return nothing;
}
- return html`<gr-suggestion-diff-preview
- .showAddSuggestionButton=${true}
- .suggestion=${this.generatedSuggestion?.replacement}
- .uuid=${this.generatedReplacementId}
- ></gr-suggestion-diff-preview>`;
}
private renderGenerateSuggestEditButton() {
if (!this.showGeneratedSuggestion()) {
return nothing;
}
- const numberOfSuggestions = !this.generatedSuggestion ? '' : ' (1)';
+ const tooltip =
+ 'Select to show a generated suggestion based on your comment for commented text. This suggestion can be inserted as a code block in your comment.';
return html`
<div class="action">
- <label>
+ <label title=${tooltip}>
<input
type="checkbox"
id="generateSuggestCheckbox"
?checked=${this.generateSuggestion}
@change=${() => {
this.generateSuggestion = !this.generateSuggestion;
- if (!this.generateSuggestion) {
- this.generatedSuggestion = undefined;
- } else {
+ if (this.comment?.id) {
+ this.getStorage().setEditableContentItem(
+ ENABLE_GENERATE_SUGGESTION_STORAGE_KEY + this.comment.id,
+ this.generateSuggestion.toString()
+ );
+ }
+ if (this.generateSuggestion) {
this.generateSuggestionTrigger$.next();
+ } else {
+ if (
+ this.flagsService.isEnabled(
+ KnownExperimentId.ML_SUGGESTED_EDIT_V2
+ )
+ ) {
+ this.generatedFixSuggestion = undefined;
+ this.autoSaveTrigger$.next();
+ }
}
this.reporting.reportInteraction(
this.generateSuggestion
@@ -979,60 +1112,166 @@ export class GrComment extends LitElement {
);
}}
/>
- Generate Suggestion${numberOfSuggestions}
+ ${this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT_V2)
+ ? 'Attach ML-suggested edit'
+ : 'Generate Suggestion'}
+ ${when(
+ this.suggestionLoading,
+ () => html`<span class="loadingSpin"></span>`,
+ () => html`${this.getNumberOfSuggestions()}`
+ )}
</label>
+ <a
+ href=${this.suggestionsProvider?.getDocumentationLink?.() ||
+ getDocUrl(
+ this.docsBaseUrl,
+ 'user-suggest-edits.html$_generate_suggestion'
+ )}
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <gr-icon
+ icon="help"
+ title="About Generated Suggested Edits"
+ ></gr-icon>
+ </a>
</div>
`;
}
+ private getNumberOfSuggestions() {
+ if (!this.generateSuggestion) {
+ return '';
+ }
+ if (this.generatedSuggestion || this.generatedFixSuggestion) {
+ return '(1)';
+ } else {
+ return '(0)';
+ }
+ }
+
private handleAddGeneratedSuggestion(code: string) {
const addNewLine = this.messageText.length !== 0;
- this.messageText += `${
+ this.addedGeneratedSuggestion = `${
addNewLine ? '\n' : ''
}${USER_SUGGESTION_START_PATTERN}${code}${'\n```'}`;
+ this.messageText += this.addedGeneratedSuggestion;
}
- private async generateSuggestEdit() {
- const suggestionsPlugins =
- this.getPluginLoader().pluginsModel.getState().suggestionsPlugins;
- if (suggestionsPlugins.length === 0) return;
+ private generateSuggestEdit() {
+ if (this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT_V2)) {
+ this.generateSuggestEdit_v2();
+ } else if (
+ this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT)
+ ) {
+ this.generateSuggestEdit_v1();
+ }
+ }
+
+ private async generateSuggestEdit_v1() {
+ const suggestionsProvider = this.suggestionsProvider;
+ const changeInfo = this.getChangeModel().getChange();
if (
+ !suggestionsProvider?.suggestCode ||
!this.showGeneratedSuggestion() ||
- !this.changeNum ||
+ !this.generateSuggestion ||
+ !changeInfo ||
!this.comment ||
!this.comment.patch_set ||
!this.comment.path ||
this.messageText.length === 0
)
return;
- this.generatedReplacementId = uuid();
+ this.generatedSuggestionId = uuid();
this.reporting.reportInteraction(Interaction.GENERATE_SUGGESTION_REQUEST, {
- uuid: this.generatedReplacementId,
+ uuid: this.generatedSuggestionId,
+ type: 'suggest-code',
+ commentId: this.comment.id,
});
- const suggestionResponse = await suggestionsPlugins[0].provider.suggestCode(
- {
+ this.suggestionLoading = true;
+ let suggestionResponse;
+ try {
+ suggestionResponse = await suggestionsProvider.suggestCode({
prompt: this.messageText,
- changeNumber: this.changeNum,
+ changeInfo: changeInfo as ChangeInfo,
patchsetNumber: this.comment?.patch_set,
filePath: this.comment.path,
range: this.comment.range,
lineNumber: this.comment.line,
- }
- );
+ });
+ } finally {
+ this.suggestionLoading = false;
+ }
+
+ if (!suggestionResponse) return;
// TODO(milutin): The suggestionResponse can contain multiple suggestion
// options. We pick the first one for now. In future we shouldn't ignore
// other suggestions.
this.reporting.reportInteraction(Interaction.GENERATE_SUGGESTION_RESPONSE, {
- uuid: this.generatedReplacementId,
+ uuid: this.generatedSuggestionId,
+ type: 'suggest-code',
response: suggestionResponse.responseCode,
numSuggestions: suggestionResponse.suggestions.length,
hasNewRange: suggestionResponse.suggestions?.[0]?.newRange !== undefined,
});
const suggestion = suggestionResponse.suggestions?.[0];
- if (!suggestion) return;
+ if (!suggestion?.replacement) return;
this.generatedSuggestion = suggestion;
}
+ private async generateSuggestEdit_v2() {
+ const suggestionsProvider = this.suggestionsProvider;
+ const changeInfo = this.getChangeModel().getChange();
+ if (
+ !suggestionsProvider?.suggestFix ||
+ !this.showGeneratedSuggestion() ||
+ !this.generateSuggestion ||
+ !changeInfo ||
+ !this.comment ||
+ !this.comment.patch_set ||
+ !this.comment.path ||
+ this.messageText.length === 0
+ )
+ return;
+ this.generatedSuggestionId = uuid();
+ this.reporting.reportInteraction(Interaction.GENERATE_SUGGESTION_REQUEST, {
+ uuid: this.generatedSuggestionId,
+ type: 'suggest-fix',
+ commentId: this.comment.id,
+ });
+ this.suggestionLoading = true;
+ let suggestionResponse;
+ try {
+ suggestionResponse = await suggestionsProvider.suggestFix({
+ prompt: this.messageText,
+ changeInfo: changeInfo as ChangeInfo,
+ patchsetNumber: this.comment?.patch_set,
+ filePath: this.comment.path,
+ range: this.comment.range,
+ lineNumber: this.comment.line,
+ });
+ } finally {
+ this.suggestionLoading = false;
+ }
+
+ if (!suggestionResponse) return;
+ // TODO(milutin): The suggestionResponse can contain multiple suggestion
+ // options. We pick the first one for now. In future we shouldn't ignore
+ // other suggestions.
+ this.reporting.reportInteraction(Interaction.GENERATE_SUGGESTION_RESPONSE, {
+ uuid: this.generatedSuggestionId,
+ type: 'suggest-fix',
+ response: suggestionResponse.responseCode,
+ numSuggestions: suggestionResponse.fix_suggestions.length,
+ });
+ const suggestion = suggestionResponse.fix_suggestions?.[0];
+ if (!suggestion?.replacements || suggestion.replacements.length === 0) {
+ return;
+ }
+ this.generatedFixSuggestion = suggestion;
+ this.autoSaveTrigger$.next();
+ }
+
private renderRobotActions() {
if (!this.account || !isRobot(this.comment)) return;
const endpoint = html`
@@ -1130,7 +1369,7 @@ export class GrComment extends LitElement {
if (
changed.has('changeNum') ||
changed.has('comment') ||
- changed.has('generatedReplacement')
+ changed.has('generatedSuggestion')
) {
if (
!this.changeNum ||
@@ -1231,7 +1470,11 @@ export class GrComment extends LitElement {
],
};
}
- if (isRobot(this.comment) && this.comment.fix_suggestions.length > 0) {
+ if (
+ isRobot(this.comment) &&
+ this.comment.fix_suggestions &&
+ this.comment.fix_suggestions.length > 0
+ ) {
const id = this.comment.robot_id;
return {
fixSuggestions: this.comment.fix_suggestions.map(s => {
@@ -1373,7 +1616,7 @@ export class GrComment extends LitElement {
assert(isDraft(this.comment), 'only drafts are editable');
const messageToSave = this.messageText.trimEnd();
if (messageToSave === '') return;
- if (messageToSave === this.comment.message) return;
+ if (!this.somethingToSave()) return;
try {
this.autoSaving = this.rawSave({showToast: false});
@@ -1388,13 +1631,19 @@ export class GrComment extends LitElement {
await this.save();
}
- convertToCommentInput(): CommentInput | undefined {
+ async convertToCommentInputAndOrDiscard(): Promise<CommentInput | undefined> {
if (!this.somethingToSave() || !this.comment) return;
- return convertToCommentInput({
- ...this.comment,
- message: this.messageText.trimEnd(),
- unresolved: this.unresolved,
- });
+ const messageToSave = this.messageText.trimEnd();
+ if (messageToSave === '') {
+ await this.getCommentsModel().discardDraft(id(this.comment));
+ return undefined;
+ } else {
+ return convertToCommentInput({
+ ...this.comment,
+ message: this.messageText.trimEnd(),
+ unresolved: this.unresolved,
+ });
+ }
}
async save() {
@@ -1420,6 +1669,7 @@ export class GrComment extends LitElement {
} else {
// No need to make a backend call when nothing has changed.
while (this.somethingToSave()) {
+ this.trackGeneratedSuggestionEdit();
this.comment = await this.rawSave({showToast: true});
if (isError(this.comment)) return;
}
@@ -1431,7 +1681,8 @@ export class GrComment extends LitElement {
return (
isError(this.comment) ||
this.messageText.trimEnd() !== this.comment?.message ||
- this.unresolved !== this.comment.unresolved
+ this.unresolved !== this.comment.unresolved ||
+ !deepEqual(this.comment?.fix_suggestions, this.getFixSuggestions())
);
}
@@ -1444,11 +1695,20 @@ export class GrComment extends LitElement {
...this.comment,
message: this.messageText.trimEnd(),
unresolved: this.unresolved,
+ fix_suggestions: this.getFixSuggestions(),
},
options.showToast
);
}
+ getFixSuggestions(): FixSuggestionInfo[] | undefined {
+ if (!this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT_V2))
+ return undefined;
+ if (!this.generateSuggestion) return undefined;
+ if (!this.generatedFixSuggestion) return undefined;
+ return [this.generatedFixSuggestion];
+ }
+
private handleToggleResolved() {
this.unresolved = !this.unresolved;
if (!this.editing) {
@@ -1491,6 +1751,23 @@ export class GrComment extends LitElement {
);
this.closeDeleteCommentModal();
}
+
+ private trackGeneratedSuggestionEdit() {
+ const hasUserSuggestion = this.messageText.includes(
+ USER_SUGGESTION_START_PATTERN
+ );
+ const wasGeneratedSuggestionEdited =
+ this.addedGeneratedSuggestion &&
+ hasUserSuggestion &&
+ !this.messageText.includes(this.addedGeneratedSuggestion);
+ if (wasGeneratedSuggestionEdited) {
+ this.reporting.reportInteraction(Interaction.GENERATE_SUGGESTION_EDITED, {
+ uuid: this.generatedSuggestionId,
+ commentId: this.comment?.id ?? '',
+ });
+ this.addedGeneratedSuggestion = undefined;
+ }
+ }
}
declare global {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
index a01d3a4803..098b79a671 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
@@ -28,6 +28,7 @@ import {
PatchSetNum,
Timestamp,
UrlEncodedCommentId,
+ FixId,
} from '../../../types/common';
import {
createComment,
@@ -38,7 +39,7 @@ import {
import {ReplyToCommentEvent} from '../../../types/events';
import {GrConfirmDeleteCommentDialog} from '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
import {assertIsDefined} from '../../../utils/common-util';
-import {Modifier} from '../../../utils/dom-util';
+import {Key, Modifier} from '../../../utils/dom-util';
import {SinonStubbedMember} from 'sinon';
import {fixture, html, assert} from '@open-wc/testing';
import {GrButton} from '../gr-button/gr-button';
@@ -47,6 +48,7 @@ import {
CommentsModel,
commentsModelToken,
} from '../../../models/comments/comments-model';
+import {KnownExperimentId} from '../../../services/flags/flags';
suite('gr-comment tests', () => {
let element: GrComment;
@@ -316,11 +318,7 @@ suite('gr-comment tests', () => {
<div class="leftActions">
<div class="action resolve">
<label>
- <input
- checked=""
- id="resolvedCheckbox"
- type="checkbox"
- />
+ <input id="resolvedCheckbox" type="checkbox" />
Resolved
</label>
</div>
@@ -422,11 +420,7 @@ suite('gr-comment tests', () => {
<div class="leftActions">
<div class="action resolve">
<label>
- <input
- checked=""
- id="resolvedCheckbox"
- type="checkbox"
- />
+ <input id="resolvedCheckbox" type="checkbox" />
Resolved
</label>
</div>
@@ -602,6 +596,46 @@ suite('gr-comment tests', () => {
assert.isTrue(spy.called);
});
+ suite('ctrl+ENTER ', () => {
+ test('saves comment', async () => {
+ const spy = sinon.stub(element, 'save');
+ element.messageText = 'is that the horse from horsing around??';
+ element.editing = true;
+ await element.updateComplete;
+ pressKey(
+ element.textarea!.textarea!.textarea,
+ Key.ENTER,
+ Modifier.CTRL_KEY
+ );
+ assert.isTrue(spy.called);
+ });
+ test('propagates on patchset comment', async () => {
+ const event = new KeyboardEvent('keydown', {
+ key: Key.ENTER,
+ ctrlKey: true,
+ });
+ const stopPropagationStub = sinon.stub(event, 'stopPropagation');
+ element.permanentEditingMode = true;
+ element.messageText = 'is that the horse from horsing around??';
+ element.editing = true;
+ await element.updateComplete;
+ element.dispatchEvent(event);
+ assert.isFalse(stopPropagationStub.called);
+ });
+ test('does not propagate on normal comment', async () => {
+ const event = new KeyboardEvent('keydown', {
+ key: Key.ENTER,
+ ctrlKey: true,
+ });
+ const stopPropagationStub = sinon.stub(event, 'stopPropagation');
+ element.messageText = 'is that the horse from horsing around??';
+ element.editing = true;
+ await element.updateComplete;
+ element.dispatchEvent(event);
+ assert.isTrue(stopPropagationStub.called);
+ });
+ });
+
test('save', async () => {
const savePromise = mockPromise<DraftInfo>();
const stub = sinon.stub(commentsModel, 'saveDraft').returns(savePromise);
@@ -734,6 +768,21 @@ suite('gr-comment tests', () => {
assert.isFalse(saveStub.called);
});
+ test('converting to input for empty text calls discard()', async () => {
+ const saveStub = sinon.stub(commentsModel, 'saveDraft');
+ const discardStub = sinon.stub(commentsModel, 'discardDraft');
+ element.comment = createDraft();
+ element.editing = true;
+ await element.updateComplete;
+
+ element.messageText = '';
+ await element.updateComplete;
+
+ await element.convertToCommentInputAndOrDiscard();
+ assert.isTrue(discardStub.called);
+ assert.isFalse(saveStub.called);
+ });
+
test('handlePleaseFix fires reply-to-comment event', async () => {
const listener = listenOnce<ReplyToCommentEvent>(
element,
@@ -886,4 +935,154 @@ suite('gr-comment tests', () => {
);
});
});
+
+ suite('suggested fix', () => {
+ let element: GrComment;
+ const generatedFixSuggestion = {
+ description: 'prompt_to_edit',
+ fix_id: 'ml' as FixId,
+ replacements: [
+ {
+ path: 'google3/ts',
+ range: {
+ start_line: 83,
+ start_character: 0,
+ end_line: 83,
+ end_character: 0,
+ },
+ replacement: "import {useUtil} from '../../../utils/use_util';\n",
+ },
+ {
+ path: 'google3/ts',
+ range: {
+ start_line: 985,
+ start_character: 0,
+ end_line: 988,
+ end_character: 0,
+ },
+ replacement:
+ ' this.suggestionsProvider.supportedFileExtensions.includes(useUtil.getExtension(this.comment.path))) &&\n',
+ },
+ ],
+ };
+ setup(async () => {
+ stubFlags('isEnabled')
+ .withArgs(KnownExperimentId.ML_SUGGESTED_EDIT_V2)
+ .returns(true);
+ });
+
+ test('renders suggestions in comment', async () => {
+ const comment = {
+ ...createComment(),
+ author: {
+ name: 'MitoMr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com' as EmailAddress,
+ },
+ line: 5,
+ path: 'test',
+ savingState: SavingState.OK,
+ message: 'hello world',
+ fix_suggestions: [generatedFixSuggestion],
+ };
+ element = await fixture(
+ html`<gr-comment
+ .account=${account}
+ .showPatchset=${true}
+ .comment=${comment}
+ .initiallyCollapsed=${false}
+ ></gr-comment>`
+ );
+ element.editing = false;
+ await element.updateComplete;
+ assert.dom.equal(
+ queryAndAssert(element, 'gr-fix-suggestions'),
+ /* HTML */ '<gr-fix-suggestions> </gr-fix-suggestions>'
+ );
+ });
+
+ test('renders suggestions in draft', async () => {
+ const comment: DraftInfo = {
+ ...createDraft(),
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com' as EmailAddress,
+ },
+ line: 5,
+ path: 'test',
+ savingState: SavingState.OK,
+ message: 'hello world',
+ fix_suggestions: [generatedFixSuggestion],
+ };
+ element = await fixture(
+ html`<gr-comment
+ .account=${account}
+ .showPatchset=${true}
+ .comment=${comment}
+ .initiallyCollapsed=${false}
+ ></gr-comment>`
+ );
+ element.editing = false;
+ await element.updateComplete;
+ assert.dom.equal(
+ queryAndAssert(element, 'gr-fix-suggestions'),
+ /* HTML */ '<gr-fix-suggestions> </gr-fix-suggestions>'
+ );
+ });
+
+ test('doesn`t render fix_suggestion when not in draft', async () => {
+ const comment: DraftInfo = {
+ ...createDraft(),
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com' as EmailAddress,
+ },
+ line: 5,
+ path: 'test',
+ savingState: SavingState.OK,
+ message: 'hello world',
+ };
+ element = await fixture(
+ html`<gr-comment
+ .account=${account}
+ .showPatchset=${true}
+ .comment=${comment}
+ .initiallyCollapsed=${false}
+ ></gr-comment>`
+ );
+ element.editing = true;
+ await element.updateComplete;
+ assert.isUndefined(query(element, 'gr-suggestion-diff-preview'));
+ });
+
+ test('render suggestions in draft is in editing & generatedFixSuggestions is not empty', async () => {
+ const comment: DraftInfo = {
+ ...createDraft(),
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com' as EmailAddress,
+ },
+ line: 5,
+ path: 'test',
+ savingState: SavingState.OK,
+ message: 'hello world',
+ };
+ element = await fixture(
+ html`<gr-comment
+ .account=${account}
+ .showPatchset=${true}
+ .comment=${comment}
+ .initiallyCollapsed=${false}
+ ></gr-comment>`
+ );
+ element.editing = true;
+ sinon.stub(element, 'showGeneratedSuggestion').returns(true);
+ element.generateSuggestion = true;
+ element.generatedFixSuggestion = generatedFixSuggestion;
+ await element.updateComplete;
+ assert.dom.equal(
+ queryAndAssert(element, 'gr-suggestion-diff-preview'),
+ /* HTML */ '<gr-suggestion-diff-preview> </gr-suggestion-diff-preview>'
+ );
+ });
+ });
});
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
index f71c390313..1a536d7c7c 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
@@ -22,6 +22,7 @@ import {getAppContext} from '../../../services/app-context';
import {Timing} from '../../../constants/reporting';
import {when} from 'lit/directives/when.js';
import {formStyles} from '../../../styles/form-styles';
+import {fire} from '../../../utils/event-util';
const COPY_TIMEOUT_MS = 1000;
@@ -189,6 +190,7 @@ export class GrCopyClipboard extends LitElement {
e.preventDefault();
e.stopPropagation();
+ fire(this, 'item-copied', {});
this.text = queryAndAssert<HTMLInputElement>(this, '#input').value;
assertIsDefined(this.text, 'text');
this.iconEl.icon = 'check';
@@ -201,3 +203,10 @@ export class GrCopyClipboard extends LitElement {
setTimeout(() => (this.iconEl.icon = 'content_copy'), COPY_TIMEOUT_MS);
}
}
+
+declare global {
+ interface HTMLElementEventMap {
+ /** Fired when an item has been copied. */
+ 'item-copied': CustomEvent<{}>;
+ }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
index 828672b117..35567be6e9 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
@@ -391,10 +391,12 @@ export class GrCursorManager {
}
_targetIsVisible(top: number) {
+ // Targets near the top are often covered by sticky header UI, so we
+ // consider it not-visible if it is within 100px of the top.
return (
this.scrollMode === ScrollMode.KEEP_VISIBLE &&
- top > window.pageYOffset &&
- top < window.pageYOffset + window.innerHeight
+ top > window.scrollY + 100 &&
+ top < window.scrollY + window.innerHeight
);
}
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
index 74ba635b6b..408efbe142 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
@@ -52,7 +52,7 @@ export class GrDiffPreferences extends LitElement {
@state() private originalDiffPrefs?: DiffPreferencesInfo;
- private readonly getUserModel = resolve(this, userModelToken);
+ readonly getUserModel = resolve(this, userModelToken);
constructor() {
super();
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts
index 05f5ea1931..97e4221190 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts
@@ -6,12 +6,16 @@
import '../../../test/common-test-setup';
import './gr-diff-preferences';
import {GrDiffPreferences} from './gr-diff-preferences';
-import {queryAll, stubRestApi} from '../../../test/test-utils';
+import {
+ makePrefixedJSON,
+ queryAll,
+ stubRestApi,
+ waitUntil,
+} from '../../../test/test-utils';
import {DiffPreferencesInfo} from '../../../types/diff';
import {createDefaultDiffPrefs} from '../../../constants/constants';
import {IronInputElement} from '@polymer/iron-input';
import {GrSelect} from '../gr-select/gr-select';
-import {ParsedJSON} from '../../../types/common';
import {fixture, html, assert} from '@open-wc/testing';
suite('gr-diff-preferences tests', () => {
@@ -223,17 +227,20 @@ suite('gr-diff-preferences tests', () => {
assert.isTrue(element.hasUnsavedChanges());
- const getResponseObjStub = stubRestApi('getResponseObject').returns(
- Promise.resolve(element.diffPrefs! as unknown as ParsedJSON)
+ const savePrefStub = stubRestApi('saveDiffPreferences').resolves(
+ new Response(makePrefixedJSON(element.diffPrefs))
);
- // Save the change.
await element.save();
+ // Wait for model state update, since this is not awaited by element.save()
+ await waitUntil(
+ () =>
+ !element.getUserModel().getState().diffPreferences
+ ?.show_whitespace_errors
+ );
- assert.isTrue(getResponseObjStub.called);
-
+ assert.isTrue(savePrefStub.called);
assert.isFalse(element.diffPrefs!.show_whitespace_errors);
-
assert.isFalse(element.hasUnsavedChanges());
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
index 886894e9a2..babe5e2931 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
@@ -36,9 +36,8 @@ export interface Command {
@customElement('gr-download-commands')
export class GrDownloadCommands extends LitElement {
- // TODO(TS): maybe default to [] as only used in dom-repeat
@property({type: Array})
- commands?: Command[];
+ commands: Command[] = [];
// private but used in test
@state() loggedIn = false;
@@ -49,6 +48,10 @@ export class GrDownloadCommands extends LitElement {
@property({type: String})
selectedScheme?: string;
+ // description of selected scheme
+ @property({type: String})
+ description?: string;
+
@property({type: Boolean, attribute: 'show-keyboard-shortcut-tooltips'})
showKeyboardShortcutTooltips = false;
@@ -121,6 +124,9 @@ export class GrDownloadCommands extends LitElement {
display: flex;
justify-content: space-between;
}
+ .description {
+ margin-bottom: var(--spacing-m);
+ }
.commands {
display: flex;
flex-direction: column;
@@ -138,7 +144,7 @@ export class GrDownloadCommands extends LitElement {
override render() {
return html`
<div class="schemes">${this.renderDownloadTabs()}</div>
- ${this.renderCommands()}
+ ${this.renderDescription()} ${this.renderCommands()}
`;
}
@@ -157,6 +163,11 @@ export class GrDownloadCommands extends LitElement {
`;
}
+ private renderDescription() {
+ if (!this.description) return;
+ return html`<div class="description">${this.description}</div>`;
+ }
+
private renderPaperTab(scheme: string) {
return html` <paper-tab data-scheme=${scheme}>${scheme}</paper-tab> `;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
index 7dee49faa2..92505d2c83 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
@@ -10,6 +10,8 @@ import '../gr-icon/gr-icon';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
import '../../plugins/gr-endpoint-slot/gr-endpoint-slot';
+import {subscribe} from '../../lit/subscription-controller';
+import {changeModelToken} from '../../../models/change/change-model';
import {fire, fireAlert} from '../../../utils/event-util';
import {getAppContext} from '../../../services/app-context';
import {debounce, DelayedTask} from '../../../utils/async-util';
@@ -26,6 +28,7 @@ import {
EditableContentSaveEvent,
ValueChangedEvent,
} from '../../../types/events';
+import {EmailInfo, GitPersonInfo} from '../../../types/common';
import {nothing} from 'lit';
import {classMap} from 'lit/directives/class-map.js';
import {when} from 'lit/directives/when.js';
@@ -89,6 +92,19 @@ export class GrEditableContent extends LitElement {
@state() newContent = '';
+ @state()
+ emails: EmailInfo[] = [];
+
+ @state()
+ committerEmail?: string;
+
+ @state()
+ latestCommitter?: GitPersonInfo;
+
+ private readonly restApiService = getAppContext().restApiService;
+
+ private readonly getChangeModel = resolve(this, changeModelToken);
+
private readonly getStorage = resolve(this, storageServiceToken);
private readonly reporting = getAppContext().reportingService;
@@ -96,6 +112,15 @@ export class GrEditableContent extends LitElement {
// Tests use this so needs to be non private
storeTask?: DelayedTask;
+ constructor() {
+ super();
+ subscribe(
+ this,
+ () => this.getChangeModel().latestCommitter$,
+ x => (this.latestCommitter = x)
+ );
+ }
+
override disconnectedCallback() {
this.storeTask?.flush();
super.disconnectedCallback();
@@ -148,7 +173,6 @@ export class GrEditableContent extends LitElement {
.show-all-container {
background-color: var(--view-background-color);
display: flex;
- justify-content: flex-end;
border: 1px solid transparent;
border-top-color: var(--border-color);
border-radius: 0 0 4px 4px;
@@ -162,6 +186,14 @@ export class GrEditableContent extends LitElement {
:host([editing]) .show-all-container {
box-shadow: none;
border: 1px solid var(--border-color);
+ justify-content: space-between;
+ }
+ :host(:not([editing])) .show-all-container {
+ justify-content: flex-end;
+ }
+ div:only-child {
+ align-self: flex-end;
+ margin-left: auto;
}
.flex-space {
flex-grow: 1;
@@ -169,6 +201,10 @@ export class GrEditableContent extends LitElement {
.show-all-container gr-icon {
color: inherit;
}
+ .email-dropdown {
+ margin-left: var(--spacing-s);
+ align-self: center;
+ }
.cancel-button {
margin-right: var(--spacing-l);
}
@@ -273,22 +309,33 @@ export class GrEditableContent extends LitElement {
)}
${when(
this.editing,
- () => html` <div class="editButtons">
- <gr-button
- link
- class="cancel-button"
- @click=${this.handleCancel}
- ?disabled=${this.disabled}
- >Cancel</gr-button
+ () => html` ${when(
+ this.canShowEmailDropdown(),
+ () => html` <div class="email-dropdown" id="editMessageEmailDropdown">Committer Email
+ <gr-dropdown-list
+ .items=${this.getEmailDropdownItems()}
+ .value=${this.committerEmail}
+ @value-change=${this.setCommitterEmail}
>
- <gr-button
- class="save-button"
- primary=""
- @click=${this.handleSave}
- ?disabled=${this.computeSaveDisabled()}
- >Save</gr-button
- >
- </div>`
+ </gr-dropdown-list>
+ <span></div>`
+ )}
+ <div class="editButtons">
+ <gr-button
+ link
+ class="cancel-button"
+ @click=${this.handleCancel}
+ ?disabled=${this.disabled}
+ >Cancel</gr-button
+ >
+ <gr-button
+ class="save-button"
+ primary=""
+ @click=${this.handleSave}
+ ?disabled=${this.computeSaveDisabled()}
+ >Save</gr-button
+ >
+ </div>`
)}
</div>
</div>
@@ -384,7 +431,10 @@ export class GrEditableContent extends LitElement {
handleSave(e: Event) {
e.preventDefault();
- fire(this, 'editable-content-save', {content: this.newContent});
+ fire(this, 'editable-content-save', {
+ content: this.newContent,
+ committerEmail: this.committerEmail ? this.committerEmail : null,
+ });
// It would be nice, if we would set this.newContent = undefined here,
// but we can only do that when we are sure that the save operation has
// succeeded.
@@ -408,8 +458,43 @@ export class GrEditableContent extends LitElement {
}
async handleEditCommitMessage() {
+ await this.loadEmails();
this.editing = true;
await this.updateComplete;
this.focusTextarea();
}
+
+ async loadEmails() {
+ const accountEmails: EmailInfo[] =
+ (await this.restApiService.getAccountEmails()) ?? [];
+ let selectedEmail: string | undefined;
+ accountEmails.forEach(e => {
+ if (e.preferred) {
+ selectedEmail = e.email;
+ }
+ });
+
+ if (accountEmails.some(e => e.email === this.latestCommitter?.email)) {
+ selectedEmail = this.latestCommitter?.email;
+ }
+ this.emails = accountEmails;
+ this.committerEmail = selectedEmail;
+ }
+
+ private canShowEmailDropdown() {
+ return this.emails.length > 1;
+ }
+
+ private getEmailDropdownItems() {
+ return this.emails.map(e => {
+ return {
+ text: e.email,
+ value: e.email,
+ };
+ });
+ }
+
+ private setCommitterEmail(e: CustomEvent<{value: string}>) {
+ this.committerEmail = e.detail.value;
+ }
}
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
index 4a5611b855..f8f530dfbf 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
@@ -12,6 +12,18 @@ import {fixture, html, assert} from '@open-wc/testing';
import {StorageService} from '../../../services/storage/gr-storage';
import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
import {testResolver} from '../../../test/common-test-setup';
+import {GrDropdownList} from '../gr-dropdown-list/gr-dropdown-list';
+
+const emails = [
+ {
+ email: 'primary@example.com',
+ preferred: true,
+ },
+ {
+ email: 'secondary@example.com',
+ preferred: false,
+ },
+];
suite('gr-editable-content tests', () => {
let element: GrEditableContent;
@@ -233,4 +245,38 @@ suite('gr-editable-content tests', () => {
assert.deepEqual([element.storageKey], eraseStub.lastCall.args);
});
});
+
+ suite('edit with committer email', () => {
+ test('hide email dropdown when user has one email', async () => {
+ element.emails = emails.slice(0, 1);
+ element.editing = true;
+ await element.updateComplete;
+ assert.notExists(query(element, '#editMessageEmailDropdown'));
+ });
+
+ test('show email dropdown when user has more than one email', async () => {
+ element.emails = emails;
+ element.editing = true;
+ await element.updateComplete;
+ const editMessageEmailDropdown = queryAndAssert(
+ element,
+ '#editMessageEmailDropdown'
+ );
+ assert.dom.equal(
+ editMessageEmailDropdown,
+ `<div class="email-dropdown" id="editMessageEmailDropdown">Committer Email
+ <gr-dropdown-list></gr-dropdown-list>
+ <span></span>
+ </div>`
+ );
+ const emailDropdown = queryAndAssert<GrDropdownList>(
+ editMessageEmailDropdown,
+ 'gr-dropdown-list'
+ );
+ assert.deepEqual(
+ emailDropdown.items?.map(e => e.value),
+ emails.map(e => e.email)
+ );
+ });
+ });
});
diff --git a/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts b/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
new file mode 100644
index 0000000000..9e78e2a005
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
@@ -0,0 +1,194 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../shared/gr-button/gr-button';
+import '../../shared/gr-icon/gr-icon';
+import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
+import '../gr-suggestion-diff-preview/gr-suggestion-diff-preview';
+import {css, html, LitElement} from 'lit';
+import {customElement, state, query, property} from 'lit/decorators.js';
+import {fire} from '../../../utils/event-util';
+import {getDocUrl} from '../../../utils/url-util';
+import {subscribe} from '../../lit/subscription-controller';
+import {resolve} from '../../../models/dependency';
+import {configModelToken} from '../../../models/config/config-model';
+import {GrSuggestionDiffPreview} from '../gr-suggestion-diff-preview/gr-suggestion-diff-preview';
+import {changeModelToken} from '../../../models/change/change-model';
+import {Comment, isDraft, PatchSetNumber} from '../../../types/common';
+import {OpenFixPreviewEventDetail} from '../../../types/events';
+import {pluginLoaderToken} from '../gr-js-api-interface/gr-plugin-loader';
+import {SuggestionsProvider} from '../../../api/suggestions';
+import {PROVIDED_FIX_ID} from '../../../utils/comment-util';
+
+/**
+ * gr-fix-suggestions is UI for comment.fix_suggestions.
+ * gr-fix-suggestions is wrapper for gr-suggestion-diff-preview with buttons
+ * to preview and apply fix and for giving a context about suggestion.
+ */
+@customElement('gr-fix-suggestions')
+export class GrFixSuggestions extends LitElement {
+ @query('gr-suggestion-diff-preview')
+ suggestionDiffPreview?: GrSuggestionDiffPreview;
+
+ @property({type: Object})
+ comment?: Comment;
+
+ @state() private docsBaseUrl = '';
+
+ @state() private applyingFix = false;
+
+ @state() latestPatchNum?: PatchSetNumber;
+
+ @state()
+ suggestionsProvider?: SuggestionsProvider;
+
+ private readonly getConfigModel = resolve(this, configModelToken);
+
+ private readonly getChangeModel = resolve(this, changeModelToken);
+
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
+
+ constructor() {
+ super();
+ subscribe(
+ this,
+ () => this.getConfigModel().docsBaseUrl$,
+ docsBaseUrl => (this.docsBaseUrl = docsBaseUrl)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().latestPatchNum$,
+ x => (this.latestPatchNum = x)
+ );
+ }
+
+ override connectedCallback() {
+ super.connectedCallback();
+ this.getPluginLoader()
+ .awaitPluginsLoaded()
+ .then(() => {
+ const suggestionsPlugins =
+ this.getPluginLoader().pluginsModel.getState().suggestionsPlugins;
+ // We currently support results from only 1 provider.
+ this.suggestionsProvider = suggestionsPlugins?.[0]?.provider;
+ });
+ }
+
+ static override get styles() {
+ return [
+ css`
+ .header {
+ background-color: var(--background-color-primary);
+ border: 1px solid var(--border-color);
+ padding: var(--spacing-xs) var(--spacing-xl);
+ display: flex;
+ align-items: center;
+ border-top-left-radius: var(--border-radius);
+ border-top-right-radius: var(--border-radius);
+ }
+ .header .title {
+ flex: 1;
+ }
+ .copyButton {
+ margin-right: var(--spacing-l);
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ if (!this.comment?.fix_suggestions) return;
+ const fix_suggestions = this.comment.fix_suggestions;
+ return html`<div class="header">
+ <div class="title">
+ <span
+ >${this.suggestionsProvider?.getFixSuggestionTitle?.(
+ fix_suggestions
+ ) || 'Suggested edit'}</span
+ >
+ <a
+ href=${this.suggestionsProvider?.getDocumentationLink?.(
+ fix_suggestions
+ ) || getDocUrl(this.docsBaseUrl, 'user-suggest-edits.html')}
+ target="_blank"
+ rel="noopener noreferrer"
+ ><gr-icon icon="help" title="read documentation"></gr-icon
+ ></a>
+ </div>
+ <div>
+ <gr-button
+ secondary
+ flatten
+ class="action show-fix"
+ @click=${this.handleShowFix}
+ >
+ Show edit
+ </gr-button>
+ <gr-button
+ secondary
+ flatten
+ .loading=${this.applyingFix}
+ .disabled=${this.isApplyEditDisabled()}
+ class="action show-fix"
+ @click=${this.handleApplyFix}
+ .title=${this.computeApplyEditTooltip()}
+ >
+ Apply edit
+ </gr-button>
+ </div>
+ </div>
+ <gr-suggestion-diff-preview
+ .fixSuggestionInfo=${this.comment?.fix_suggestions?.[0]}
+ ></gr-suggestion-diff-preview>`;
+ }
+
+ handleShowFix() {
+ if (!this.comment?.fix_suggestions || !this.comment?.patch_set) return;
+ const eventDetail: OpenFixPreviewEventDetail = {
+ fixSuggestions: this.comment.fix_suggestions.map(s => {
+ return {
+ ...s,
+ fix_id: PROVIDED_FIX_ID,
+ description:
+ this.suggestionsProvider?.getFixSuggestionTitle?.(
+ this.comment?.fix_suggestions
+ ) || 'Suggested edit',
+ };
+ }),
+ patchNum: this.comment.patch_set,
+ onCloseFixPreviewCallbacks: [],
+ };
+ fire(this, 'open-fix-preview', eventDetail);
+ }
+
+ async handleApplyFix() {
+ if (!this.comment?.fix_suggestions) return;
+ this.applyingFix = true;
+ try {
+ await this.suggestionDiffPreview?.applyFixSuggestion();
+ } finally {
+ this.applyingFix = false;
+ }
+ }
+
+ private isApplyEditDisabled() {
+ if (this.comment?.patch_set === undefined) return true;
+ if (isDraft(this.comment)) return true;
+ return this.comment.patch_set !== this.latestPatchNum;
+ }
+
+ private computeApplyEditTooltip() {
+ if (this.comment?.patch_set === undefined) return '';
+ return this.comment.patch_set !== this.latestPatchNum
+ ? 'You cannot apply this fix because it is from a previous patchset'
+ : '';
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-fix-suggestions': GrFixSuggestions;
+ }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
index e8106371b4..b57e4ffbb4 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
@@ -126,7 +126,7 @@ export class GrFormattedText extends LitElement {
this.repoCommentLinks = repoCommentLinks;
// Always linkify URLs starting with https?://
this.repoCommentLinks['ALWAYS_LINK_HTTP'] = {
- match: '(https?://\\S+[\\w/~-])',
+ match: '(https?://((?!&(gt|lt|amp|quot|apos);)\\S)+[\\w/~-])',
link: '$1',
enabled: true,
};
@@ -194,15 +194,25 @@ export class GrFormattedText extends LitElement {
// 4. Rewrite plain text ("text") to apply linking and other config-based
// rewrites. Text within code blocks is not passed here.
// 5. Open links in a new tab by rendering with target="_blank" attribute.
+ // 6. Relative links without "/" prefix are assumed to be absolute links.
function customRenderer(renderer: {[type: string]: Function}) {
- renderer['link'] = (href: string, title: string, text: string) =>
+ renderer['link'] = (href: string, title: string, text: string) => {
+ if (
+ !href.startsWith('https://') &&
+ !href.startsWith('mailto:') &&
+ !href.startsWith('http://') &&
+ !href.startsWith('/')
+ ) {
+ href = `https://${href}`;
+ }
/* HTML */
- `<a
+ return `<a
href="${href}"
${sameOrigin(href) ? '' : 'target="_blank" rel="noopener noreferrer"'}
${title ? `title="${title}"` : ''}
>${text}</a
>`;
+ };
renderer['image'] = (href: string, _title: string, text: string) =>
`![${text}](${href})`;
renderer['codespan'] = (text: string) =>
@@ -295,8 +305,7 @@ export class GrFormattedText extends LitElement {
private convertCodeToSuggestions() {
const marks = this.renderRoot.querySelectorAll('mark');
- if (marks.length > 0) {
- const userSuggestionMark = marks[0];
+ for (const userSuggestionMark of marks) {
const userSuggestion = document.createElement('gr-user-suggestion-fix');
// Temporary workaround for bug - tabs replacement
if (this.content.includes('\t')) {
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
index a287659d5f..23f1594103 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
@@ -510,6 +510,9 @@ suite('gr-formatted-text tests', () => {
element.content = `[myLink1](https://www.google.com)
[myLink2](/destiny)
[myLink3](${origin}/destiny)
+ [myLink4](google.com)
+ [myLink5](http://google.com)
+ [myLink6](mailto:google@google.com)
`;
await element.updateComplete;
@@ -529,6 +532,27 @@ suite('gr-formatted-text tests', () => {
<a href="/destiny">myLink2</a>
<br />
<a href="${origin}/destiny">myLink3</a>
+ <br />
+ <a
+ href="https://google.com"
+ rel="noopener noreferrer"
+ target="_blank"
+ >myLink4</a
+ >
+ <br />
+ <a
+ href="http://google.com"
+ rel="noopener noreferrer"
+ target="_blank"
+ >myLink5</a
+ >
+ <br />
+ <a
+ href="mailto:google@google.com"
+ rel="noopener noreferrer"
+ target="_blank"
+ >myLink6</a
+ >
</p>
</div>
</marked-element>
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
index 01e8a87b0d..9ce435ff49 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
@@ -281,6 +281,12 @@ export class GrHovercardAccountContents extends LitElement {
}}
>Dashboard</a
>
+ <gr-endpoint-decorator name="hovercard-links">
+ <gr-endpoint-param
+ name="account"
+ .value=${this.account}
+ ></gr-endpoint-param>
+ </gr-endpoint-decorator>
</div>`;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
index 281d295ca6..5afd53b2d4 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
@@ -86,6 +86,9 @@ suite('gr-hovercard-account-contents tests', () => {
<a href="/q/owner:kermit@gmail.com">Changes</a>
·
<a href="/dashboard/31415926535">Dashboard</a>
+ <gr-endpoint-decorator name="hovercard-links">
+ <gr-endpoint-param name="account"></gr-endpoint-param>
+ </gr-endpoint-decorator>
</div>
`
);
@@ -125,6 +128,9 @@ suite('gr-hovercard-account-contents tests', () => {
<a href="/q/owner:kermit@gmail.com"> Changes </a>
·
<a href="/dashboard/31415926535"> Dashboard </a>
+ <gr-endpoint-decorator name="hovercard-links">
+ <gr-endpoint-param name="account"></gr-endpoint-param>
+ </gr-endpoint-decorator>
</div>
`
);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.ts
index 7b996601d5..a1d2dcbbbd 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.ts
@@ -7,6 +7,7 @@ import {getBaseUrl} from '../../../utils/url-util';
import {HttpMethod} from '../../../constants/constants';
import {RequestPayload} from '../../../types/common';
import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
+import {readJSONResponsePayload} from '../gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
export const PLUGIN_LOADING_TIMEOUT_MS = 10000;
@@ -75,7 +76,7 @@ export function send(
}
});
} else {
- return restApiService.getResponseObject(response);
+ return readJSONResponsePayload(response).then(obj => obj.parsed);
}
})
.then(response => {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts
index f0143a6c73..523cc59638 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts
@@ -9,10 +9,8 @@ import {getAppContext} from '../../../services/app-context';
import {
ActionPriority,
ActionType,
- ChangeActions,
ChangeActionsPluginApi,
PrimaryActionKey,
- RevisionActions,
} from '../../../api/change-actions';
import {PropertyDeclaration} from 'lit';
import {JsApiService} from './gr-js-api-types';
@@ -28,9 +26,6 @@ export interface UIActionInfo extends RequireProperties<ActionInfo, 'label'> {
// This interface is required to avoid circular dependencies between files;
export interface GrChangeActionsElement extends Element {
- RevisionActions?: Record<string, string>;
- ChangeActions: Record<string, string>;
- ActionType: Record<string, string>;
primaryActionKeys: string[];
hideQuickApproveAction(): void;
setActionOverflow(type: ActionType, key: string, overflow: boolean): void;
@@ -58,12 +53,6 @@ export interface GrChangeActionsElement extends Element {
export class GrChangeActionsInterface implements ChangeActionsPluginApi {
private el?: GrChangeActionsElement;
- RevisionActions = RevisionActions;
-
- ChangeActions = ChangeActions;
-
- ActionType = ActionType;
-
private readonly reporting = getAppContext().reportingService;
constructor(
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.ts
index e557ca8bea..d42dc7cf3d 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.ts
@@ -57,6 +57,7 @@ suite('gr-change-actions-js-api-interface tests', () => {
<gr-change-actions></gr-change-actions>
`);
element.change = {} as ChangeViewChangeInfo;
+ element.revisionActions = {};
window.Gerrit.install(
p => {
plugin = p;
@@ -69,14 +70,6 @@ suite('gr-change-actions-js-api-interface tests', () => {
testResolver(pluginLoaderToken).loadPlugins([]);
});
- test('property existence', () => {
- const properties = ['ActionType', 'ChangeActions', 'RevisionActions'];
- for (const p of properties) {
- // Have to type as any to prevent 'has no index signature.'
- assert.deepEqual((changeActions as any)[p], (element as any)[p]);
- }
- });
-
test('add/remove primary action keys', () => {
element.primaryActionKeys = [];
changeActions.addPrimaryActionKey('foo' as PrimaryActionKey);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
index 6b9c684a65..b09502a246 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
@@ -57,11 +57,7 @@ export class GrJsApiInterface implements JsApiService, Finalizable {
try {
return callback(change, revision) === false;
} catch (err: unknown) {
- this.reporting.error(
- 'GrJsApiInterface',
- new Error('canSubmitChange callback error'),
- err
- );
+ this.reportError(err, EventType.SUBMIT_CHANGE);
}
return false;
});
@@ -116,11 +112,18 @@ export class GrJsApiInterface implements JsApiService, Finalizable {
try {
cb(change, revision, info, baseRevision ?? PARENT);
} catch (err: unknown) {
- this.reporting.error(
- 'GrJsApiInterface',
- new Error('showChange callback error'),
- err
- );
+ this.reportError(err, EventType.SHOW_CHANGE);
+ }
+ }
+ }
+
+ async handleReplySent() {
+ await this.waitForPluginsToLoad();
+ for (const cb of this._getEventCallbacks(EventType.REPLY_SENT)) {
+ try {
+ cb();
+ } catch (err: unknown) {
+ this.reportError(err, EventType.REPLY_SENT);
}
}
}
@@ -134,11 +137,7 @@ export class GrJsApiInterface implements JsApiService, Finalizable {
try {
cb(detail.revisionActions, detail.change);
} catch (err: unknown) {
- this.reporting.error(
- 'GrJsApiInterface',
- new Error('showRevisionActions callback error'),
- err
- );
+ this.reportError(err, EventType.SHOW_REVISION_ACTIONS);
}
}
}
@@ -148,11 +147,7 @@ export class GrJsApiInterface implements JsApiService, Finalizable {
try {
cb(change, msg);
} catch (err: unknown) {
- this.reporting.error(
- 'GrJsApiInterface',
- new Error('commitMessage callback error'),
- err
- );
+ this.reportError(err, EventType.COMMIT_MSG_EDIT);
}
}
}
@@ -163,11 +158,7 @@ export class GrJsApiInterface implements JsApiService, Finalizable {
try {
cb(detail.change);
} catch (err: unknown) {
- this.reporting.error(
- 'GrJsApiInterface',
- new Error('labelChange callback error'),
- err
- );
+ this.reportError(err, EventType.LABEL_CHANGE);
}
}
}
@@ -177,11 +168,7 @@ export class GrJsApiInterface implements JsApiService, Finalizable {
try {
revertMsg = cb(change, revertMsg, origMsg) as string;
} catch (err: unknown) {
- this.reporting.error(
- 'GrJsApiInterface',
- new Error('modifyRevertMsg callback error'),
- err
- );
+ this.reportError(err, EventType.REVERT);
}
}
return revertMsg;
@@ -200,11 +187,7 @@ export class GrJsApiInterface implements JsApiService, Finalizable {
origMsg
) as string;
} catch (err: unknown) {
- this.reporting.error(
- 'GrJsApiInterface',
- new Error('modifyRevertSubmissionMsg callback error'),
- err
- );
+ this.reportError(err, EventType.REVERT_SUBMISSION);
}
}
return revertSubmissionMsg;
@@ -230,11 +213,7 @@ export class GrJsApiInterface implements JsApiService, Finalizable {
review = {labels: r as LabelNameToValueMap};
}
} catch (err: unknown) {
- this.reporting.error(
- 'GrJsApiInterface',
- new Error('getReviewPostRevert callback error'),
- err
- );
+ this.reportError(err, EventType.POST_REVERT);
}
}
return review;
@@ -246,15 +225,19 @@ export class GrJsApiInterface implements JsApiService, Finalizable {
try {
cb(detail.change, detail.patchRange, detail.fileRange);
} catch (err: unknown) {
- this.reporting.error(
- 'GrJsApiInterface',
- new Error('showDiff callback error'),
- err
- );
+ this.reportError(err, EventType.SHOW_DIFF);
}
}
}
+ reportError(err: unknown, type: EventType) {
+ this.reporting.error(
+ 'GrJsApiInterface',
+ new Error(`plugin event callback error for type "${type}"`),
+ err
+ );
+ }
+
_getEventCallbacks(type: EventType) {
return eventCallbacks[type] || [];
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
index 6c180d7a06..dafa434a46 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
@@ -45,7 +45,7 @@ export interface JsApiService extends Finalizable {
revertSubmissionMsg: string,
origMsg: string
): string;
- handleShowChange(detail: ShowChangeDetail): void;
+ handleShowChange(detail: ShowChangeDetail): Promise<void>;
handleShowRevisionActions(detail: ShowRevisionActionsDetail): void;
handleLabelChange(detail: {change?: ParsedChangeInfo}): void;
modifyRevertMsg(
@@ -59,4 +59,5 @@ export interface JsApiService extends Finalizable {
canSubmitChange(change: ChangeInfo, revision?: RevisionInfo | null): boolean;
getReviewPostRevert(change?: ChangeInfo): ReviewInput;
handleShowDiff(detail: ShowDiffDetail): void;
+ handleReplySent(): Promise<void>;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
index b78af2af9d..7082ec5e77 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
@@ -119,6 +119,9 @@ export class PluginLoader implements Gerrit, Finalizable {
this.reportingService
);
this.pluginsModel = new PluginsModel();
+ this.awaitPluginsLoaded().finally(() => {
+ this.pluginsModel.updateState({pluginsLoaded: true});
+ });
this.pluginEndPoints = new GrPluginEndpoints();
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts
index 65e4960eb4..e9c132af22 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts
@@ -9,6 +9,7 @@ import {ErrorCallback, RestPluginApi} from '../../../api/rest';
import {PluginApi} from '../../../api/plugin';
import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
+import {readJSONResponsePayload} from '../gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
async function getErrorMessage(response: Response): Promise<string> {
const text = await response.text();
@@ -153,7 +154,9 @@ export class GrPluginRestApi implements RestPluginApi {
Promise.reject(new Error(msg))
);
} else {
- return this.restApi.getResponseObject(response) as Promise<T>;
+ return readJSONResponsePayload(response).then(
+ obj => obj.parsed
+ ) as Promise<T>;
}
})
.catch(err => {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.ts
index c5bef85843..472093ea2f 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.ts
@@ -6,7 +6,11 @@
import '../../../test/common-test-setup';
import './gr-js-api-interface';
import {GrPluginRestApi} from './gr-plugin-rest-api';
-import {assertFails, stubRestApi} from '../../../test/test-utils';
+import {
+ assertFails,
+ makePrefixedJSON,
+ stubRestApi,
+} from '../../../test/test-utils';
import {assert} from '@open-wc/testing';
import {PluginApi} from '../../../api/plugin';
import {
@@ -18,12 +22,10 @@ import {getAppContext} from '../../../services/app-context';
suite('gr-plugin-rest-api tests', () => {
let instance: GrPluginRestApi;
- let getResponseObjectStub: sinon.SinonStub;
let sendStub: sinon.SinonStub;
setup(() => {
stubRestApi('getAccount').resolves(createAccountDetailWithId());
- getResponseObjectStub = stubRestApi('getResponseObject').resolves();
sendStub = stubRestApi('send').resolves({...new Response(), status: 200});
let pluginApi: PluginApi;
window.Gerrit.install(
@@ -45,42 +47,41 @@ suite('gr-plugin-rest-api tests', () => {
const r = await instance.fetch(HttpMethod.POST, '/url', payload);
assert.isTrue(sendStub.calledWith(HttpMethod.POST, '/url', payload));
assert.equal(r.status, 200);
- assert.isFalse(getResponseObjectStub.called);
});
test('send', async () => {
const payload = {foo: 'foo'};
const response = {bar: 'bar'};
- getResponseObjectStub.resolves(response);
+ sendStub.resolves(new Response(makePrefixedJSON(response)));
const r = await instance.send(HttpMethod.POST, '/url', payload);
assert.isTrue(sendStub.calledWith(HttpMethod.POST, '/url', payload));
- assert.strictEqual(r, response);
+ assert.deepEqual(r, response);
});
test('get', async () => {
const response = {foo: 'foo'};
- getResponseObjectStub.resolves(response);
+ sendStub.resolves(new Response(makePrefixedJSON(response)));
const r = await instance.get('/url');
assert.isTrue(sendStub.calledWith('GET', '/url'));
- assert.strictEqual(r, response);
+ assert.deepEqual(r, response);
});
test('post', async () => {
const payload = {foo: 'foo'};
const response = {bar: 'bar'};
- getResponseObjectStub.resolves(response);
+ sendStub.resolves(new Response(makePrefixedJSON(response)));
const r = await instance.post('/url', payload);
assert.isTrue(sendStub.calledWith('POST', '/url', payload));
- assert.strictEqual(r, response);
+ assert.deepEqual(r, response);
});
test('put', async () => {
const payload = {foo: 'foo'};
const response = {bar: 'bar'};
- getResponseObjectStub.resolves(response);
+ sendStub.resolves(new Response(makePrefixedJSON(response)));
const r = await instance.put('/url', payload);
assert.isTrue(sendStub.calledWith('PUT', '/url', payload));
- assert.strictEqual(r, response);
+ assert.deepEqual(r, response);
});
test('delete works', async () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
index 108451217a..85f2c062b9 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
@@ -1,17 +1,11 @@
/**
* @license
- * Copyright 2019 Google LLC
+ * Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {getBaseUrl} from '../../../../utils/url-util';
-import {CancelConditionCallback} from '../../../../services/gr-rest-api/gr-rest-api';
import {AuthService} from '../../../../services/gr-auth/gr-auth';
-import {
- AccountDetailInfo,
- EmailInfo,
- ParsedJSON,
- RequestPayload,
-} from '../../../../types/common';
+import {ParsedJSON, RequestPayload} from '../../../../types/common';
import {HttpMethod} from '../../../../constants/constants';
import {RpcLogEventDetail} from '../../../../types/events';
import {
@@ -19,7 +13,10 @@ import {
fireNetworkError,
fireServerError,
} from '../../../../utils/event-util';
-import {AuthRequestInit, FetchRequest} from '../../../../types/types';
+import {
+ AuthRequestInit,
+ FetchRequest as FetchRequestBase,
+} from '../../../../types/types';
import {ErrorCallback} from '../../../../api/rest';
import {Scheduler, Task} from '../../../../services/scheduler/scheduler';
import {RetryError} from '../../../../services/scheduler/retry-scheduler';
@@ -27,32 +24,21 @@ import {RetryError} from '../../../../services/scheduler/retry-scheduler';
export const JSON_PREFIX = ")]}'";
export interface ResponsePayload {
- // TODO(TS): readResponsePayload can assign null to the parsed property if
- // it can't parse input data. However polygerrit assumes in many places
- // that the parsed property can't be null. We should update
- // readResponsePayload method and reject a promise instead of assigning
- // null to the parsed property
- parsed: ParsedJSON; // Can be null!!! See comment above
+ parsed: ParsedJSON;
raw: string;
}
-export function readResponsePayload(
+export async function readJSONResponsePayload(
response: Response
): Promise<ResponsePayload> {
- return response.text().then(text => {
- let result;
- try {
- result = parsePrefixedJSON(text);
- } catch (_) {
- result = null;
- }
- // TODO(TS): readResponsePayload can assign null to the parsed property if
- // it can't parse input data. However polygerrit assumes in many places
- // that the parsed property can't be null. We should update
- // readResponsePayload method and reject a promise instead of assigning
- // null to the parsed property
- return {parsed: result!, raw: text};
- });
+ const text = await response.text();
+ let result: ParsedJSON;
+ try {
+ result = parsePrefixedJSON(text);
+ } catch (_) {
+ throw new Error(`Response payload is not prefixed json. Payload: ${text}`);
+ }
+ return {parsed: result!, raw: text};
}
export function parsePrefixedJSON(jsonWithPrefix: string): ParsedJSON {
@@ -63,20 +49,22 @@ export function parsePrefixedJSON(jsonWithPrefix: string): ParsedJSON {
* Wrapper around Map for caching server responses. Site-based so that
* changes to CANONICAL_PATH will result in a different cache going into
* effect.
+ *
+ * All methods operate on the cache for the current CANONICAL_PATH.
+ * Accessing cache entries for older CANONICAL_PATH not supported.
*/
+// TODO(kamilm): Seems redundant to have both this and FetchPromisesCache
+// consider joining their functionality into a single cache.
export class SiteBasedCache {
- // TODO(TS): Type looks unusual. Fix it.
- // Container of per-canonical-path caches.
- private readonly data = new Map<
- string | undefined,
- unknown | Map<string, ParsedJSON | null>
- >();
+ private readonly data = new Map<string, Map<string, ParsedJSON>>();
constructor() {
if (window.INITIAL_DATA) {
// Put all data shipped with index.html into the cache. This makes it
// so that we spare more round trips to the server when the app loads
// initially.
+ // TODO(kamilm): This implies very strict format of what is stored in
+ // INITIAL_DATA which is not clear from the name, consider renaming.
Object.entries(window.INITIAL_DATA).forEach(e =>
this._cache().set(e[0], e[1] as unknown as ParsedJSON)
);
@@ -84,40 +72,23 @@ export class SiteBasedCache {
}
// Returns the cache for the current canonical path.
- _cache(): Map<string, unknown> {
- if (!this.data.has(window.CANONICAL_PATH)) {
- this.data.set(
- window.CANONICAL_PATH,
- new Map<string, ParsedJSON | null>()
- );
+ _cache(): Map<string, ParsedJSON> {
+ const canonical_path = window.CANONICAL_PATH ?? '';
+ if (!this.data.has(canonical_path)) {
+ this.data.set(canonical_path, new Map<string, ParsedJSON>());
}
- return this.data.get(window.CANONICAL_PATH) as Map<
- string,
- ParsedJSON | null
- >;
+ return this.data.get(canonical_path)!;
}
has(key: string) {
return this._cache().has(key);
}
- get(key: '/accounts/self/emails'): EmailInfo[] | null;
-
- get(key: '/accounts/self/detail'): AccountDetailInfo | null;
-
- get(key: string): ParsedJSON | null;
-
- get(key: string): unknown {
+ get(key: string): ParsedJSON | undefined {
return this._cache().get(key);
}
- set(key: '/accounts/self/emails', value: EmailInfo[]): void;
-
- set(key: '/accounts/self/detail', value: AccountDetailInfo): void;
-
- set(key: string, value: ParsedJSON | null): void;
-
- set(key: string, value: unknown) {
+ set(key: string, value: ParsedJSON) {
this._cache().set(key, value);
}
@@ -126,13 +97,13 @@ export class SiteBasedCache {
}
invalidatePrefix(prefix: string) {
- const newMap = new Map<string, unknown>();
+ const newMap = new Map<string, ParsedJSON>();
for (const [key, value] of this._cache().entries()) {
if (!key.startsWith(prefix)) {
newMap.set(key, value);
}
}
- this.data.set(window.CANONICAL_PATH, newMap);
+ this.data.set(window.CANONICAL_PATH ?? '', newMap);
}
}
@@ -140,6 +111,9 @@ type FetchPromisesCacheData = {
[url: string]: Promise<ParsedJSON | undefined> | undefined;
};
+/**
+ * Stores promises for inflight requests, by url.
+ */
export class FetchPromisesCache {
private data: FetchPromisesCacheData;
@@ -180,6 +154,7 @@ export class FetchPromisesCache {
this.data = newData;
}
}
+
export type FetchParams = {
[name: string]: string[] | string | number | boolean | undefined | null;
};
@@ -213,46 +188,57 @@ export function throwingErrorCallback(
});
}
-interface SendRequestBase {
- method: HttpMethod | undefined;
- body?: RequestPayload;
- contentType?: string;
- headers?: Record<string, string>;
- url: string;
+export interface FetchRequest extends FetchRequestBase {
+ /**
+ * If neither this or anonymizedUrl specified no 'gr-rpc-log' event is fired.
+ */
reportUrlAsIs?: boolean;
- anonymizedUrl?: string;
+ /** Extra url params to be encoded and added to the url. */
+ params?: FetchParams;
+ /**
+ * Callback that is called, if an error was caught during fetch or if the
+ * response was returned with a non-2xx status.
+ */
errFn?: ErrorCallback;
+ /**
+ * If true, response with non-200 status will cause an error to be reported
+ * via server-error event or errFn, if provided.
+ */
+ // TODO(kamilm): Consider changing the default to true. It makes more sense to
+ // only skip the check if the caller wants to prosess status themselves.
+ reportServerError?: boolean;
}
-export interface SendRawRequest extends SendRequestBase {
- parseResponse?: false | null;
-}
-
-export interface SendJSONRequest extends SendRequestBase {
- parseResponse: true;
+export interface FetchOptionsInit {
+ method?: HttpMethod;
+ body?: RequestPayload;
+ contentType?: string;
+ headers?: Record<string, string>;
}
-export type SendRequest = SendRawRequest | SendJSONRequest;
-
-export interface FetchJSONRequest extends FetchRequest {
- reportUrlAsIs?: boolean;
- params?: FetchParams;
- cancelCondition?: CancelConditionCallback;
- errFn?: ErrorCallback;
+export function getFetchOptions(init: FetchOptionsInit): AuthRequestInit {
+ const options: AuthRequestInit = {
+ method: init.method,
+ };
+ if (init.body) {
+ options.headers = new Headers();
+ options.headers.set('Content-Type', init.contentType || 'application/json');
+ options.body =
+ typeof init.body === 'string' ? init.body : JSON.stringify(init.body);
+ }
+ // Copy headers after processing body, so that explicit headers can override
+ // if necessary.
+ if (init.headers) {
+ if (!options.headers) {
+ options.headers = new Headers();
+ }
+ for (const [name, value] of Object.entries(init.headers)) {
+ options.headers.set(name, value);
+ }
+ }
+ return options;
}
-// export function isRequestWithCancel<T extends FetchJSONRequest>(
-// x: T
-// ): x is T & RequestWithCancel {
-// return !!(x as RequestWithCancel).cancelCondition;
-// }
-//
-// export function isRequestWithErrFn<T extends FetchJSONRequest>(
-// x: T
-// ): x is T & RequestWithErrFn {
-// return !!(x as RequestWithErrFn).errFn;
-// }
-
export class GrRestApiHelper {
constructor(
private readonly _cache: SiteBasedCache,
@@ -262,7 +248,7 @@ export class GrRestApiHelper {
private readonly writeScheduler: Scheduler<Response>
) {}
- private schedule(method: string, task: Task<Response>) {
+ private schedule(method: string, task: Task<Response>): Promise<Response> {
if (method === 'PUT' || method === 'POST' || method === 'DELETE') {
return this.writeScheduler.schedule(task);
} else {
@@ -273,20 +259,19 @@ export class GrRestApiHelper {
/**
* Wraps calls to the underlying authenticated fetch function (_auth.fetch)
* with timing and logging.
-s */
- fetch(req: FetchRequest): Promise<Response> {
- const method =
- req.fetchOptions && req.fetchOptions.method
- ? req.fetchOptions.method
- : 'GET';
- const start = Date.now();
+ */
+ private fetchImpl(req: FetchRequest): Promise<Response> {
+ const method = req.fetchOptions?.method ?? HttpMethod.GET;
+ const startTime = Date.now();
const task = async () => {
const res = await this._auth.fetch(req.url, req.fetchOptions);
+ // Check for "too many requests" error and throw RetryError to cause a
+ // retry in this case, if the scheduler attempts retries.
if (!res.ok && res.status === 429) throw new RetryError<Response>(res);
return res;
};
- const xhr = this.schedule(method, task).catch((err: unknown) => {
+ const resPromise = this.schedule(method, task).catch((err: unknown) => {
if (err instanceof RetryError) {
return err.payload;
} else {
@@ -295,9 +280,9 @@ s */
});
// Log the call after it completes.
- xhr.then(res => this._logCall(req, start, res ? res.status : null));
- // Return the XHR directly (without the log).
- return xhr;
+ resPromise.then(res => this.logCall(req, startTime, res.status));
+ // Return the response directly (without the log).
+ return resPromise;
}
/**
@@ -312,7 +297,7 @@ s */
* is used here rather than the response object so there is no way this
* method can read the body stream.
*/
- _logCall(req: FetchRequest, startTime: number, status: number | null) {
+ logCall(req: FetchRequest, startTime: number, status: number) {
const method =
req.fetchOptions && req.fetchOptions.method
? req.fetchOptions.method
@@ -343,89 +328,104 @@ s */
}
/**
- * Fetch JSON from url provided.
- * Returns a Promise that resolves to a native Response.
- * Doesn't do error checking. Supports cancel condition. Performs auth.
- * Validates auth expiry errors.
+ * Fetch from url provided.
+ *
+ * Performs auth. Validates auth expiry errors.
+ * Will report any errors (by firing a corresponding event or calling errFn)
+ * that happen during the request, but doesn't inspect the status of the
+ * received response unless req.reportServerError = true.
*
- * @return Promise which resolves to undefined if cancelCondition returns true
- * and resolves to Response otherwise
+ * @return Promise resolves to a native Response.
+ * If an error occurs when performing a request, promise rejects.
*/
- fetchRawJSON(req: FetchJSONRequest): Promise<Response | undefined> {
+ async fetch(req: FetchRequest): Promise<Response> {
const urlWithParams = this.urlWithParams(req.url, req.params);
const fetchReq: FetchRequest = {
url: urlWithParams,
fetchOptions: req.fetchOptions,
anonymizedUrl: req.reportUrlAsIs ? urlWithParams : req.anonymizedUrl,
};
- return this.fetch(fetchReq)
- .then((res: Response) => {
- if (req.cancelCondition && req.cancelCondition()) {
- if (res.body) {
- res.body.cancel();
- }
- return;
- }
- return res;
- })
- .catch(err => {
- if (req.errFn) {
- req.errFn.call(undefined, null, err);
- } else {
- fireNetworkError(err);
- }
- throw err;
- });
+ let resp: Response;
+ try {
+ resp = await this.fetchImpl(fetchReq);
+ } catch (err) {
+ if (req.errFn) {
+ await req.errFn.call(undefined, null, err as Error);
+ } else {
+ fireNetworkError(err as Error);
+ }
+ throw err;
+ }
+ if (req.reportServerError && !resp.ok) {
+ if (req.errFn) {
+ await req.errFn.call(undefined, resp);
+ } else {
+ fireServerError(resp, req);
+ }
+ }
+ return resp;
}
/**
* Fetch JSON from url provided.
- * Returns a Promise that resolves to a parsed response.
- * Same as {@link fetchRawJSON}, plus error handling.
+ *
+ * Returned promise rejects if an error occurs when performing a request or
+ * if the response payload doesn't contain a valid prefixed JSON.
+ *
+ * If response status is not 2xx, promise resolves to undefined and error is
+ * reported, through errFn callback or via 'sever-error' event. The error can
+ * be suppressed with req.reportServerError = false.
+ *
+ * If JSON parsing fails the promise rejects.
*
* @param noAcceptHeader - don't add default accept json header
+ * @return Promise that resolves to a parsed response.
*/
async fetchJSON(
- req: FetchJSONRequest,
+ req: FetchRequest,
noAcceptHeader?: boolean
): Promise<ParsedJSON | undefined> {
if (!noAcceptHeader) {
req = this.addAcceptJsonHeader(req);
}
- const response = await this.fetchRawJSON(req);
- if (!response) {
- return;
- }
+ req.reportServerError ??= true;
+ const response = await this.fetch(req);
if (!response.ok) {
- if (req.errFn) {
- await req.errFn.call(undefined, response);
- return;
- }
- fireServerError(response, req);
- return;
+ return undefined;
}
- return this.getResponseObject(response);
+ // TODO(kamilm): The parsing error should likely be reported via errFn or
+ // gr-error-manager as well.
+ return (await readJSONResponsePayload(response)).parsed;
}
+ /**
+ * Add extra url params to the url.
+ *
+ * Params with values (not undefined) added as <key>=<value>. If value is an
+ * array a separate <key>=<value> param is added for every value.
+ */
urlWithParams(url: string, fetchParams?: FetchParams): string {
if (!fetchParams) {
return getBaseUrl() + url;
}
const params: Array<string | number | boolean> = [];
- for (const [p, paramValue] of Object.entries(fetchParams)) {
+ for (const [paramKey, paramValue] of Object.entries(fetchParams)) {
if (paramValue === null || paramValue === undefined) {
- params.push(this.encodeRFC5987(p));
+ params.push(this.encodeRFC5987(paramKey));
continue;
}
- // TODO(TS): Unclear, why do we need the following code.
- // If paramValue can be array - we should either fix FetchParams type
- // or convert the array to a string before calling urlWithParams method.
- const paramValueAsArray = ([] as Array<string | number | boolean>).concat(
- paramValue
- );
- for (const value of paramValueAsArray) {
- params.push(`${this.encodeRFC5987(p)}=${this.encodeRFC5987(value)}`);
+
+ if (Array.isArray(paramValue)) {
+ for (const value of paramValue) {
+ params.push(
+ `${this.encodeRFC5987(paramKey)}=${this.encodeRFC5987(value)}`
+ );
+ }
+ } else {
+ params.push(
+ `${this.encodeRFC5987(paramKey)}=${this.encodeRFC5987(paramValue)}`
+ );
}
}
return getBaseUrl() + url + '?' + params.join('&');
@@ -440,11 +440,7 @@ s */
);
}
- getResponseObject(response: Response): Promise<ParsedJSON> {
- return readResponsePayload(response).then(payload => payload.parsed);
- }
-
- addAcceptJsonHeader(req: FetchJSONRequest) {
+ addAcceptJsonHeader(req: FetchRequest) {
if (!req.fetchOptions) req.fetchOptions = {};
if (!req.fetchOptions.headers) req.fetchOptions.headers = new Headers();
if (!req.fetchOptions.headers.has('Accept')) {
@@ -453,97 +449,40 @@ s */
return req;
}
- fetchCacheURL(req: FetchJSONRequest): Promise<ParsedJSON | undefined> {
- if (this._fetchPromisesCache.has(req.url)) {
- return this._fetchPromisesCache.get(req.url)!;
+ /**
+ * Fetch JSON using cached value if available.
+ *
+ * If there is an in-flight request with the same url returns the promise for
+ * the in-flight request. If previous call for the same url resulted in the
+ * successful response it is returned. Otherwise a new request is sent.
+ *
+ * Only req.url with req.params is considered for the caching key;
+ * headers or request body are not included in cache key.
+ */
+ fetchCacheJSON(req: FetchRequest): Promise<ParsedJSON | undefined> {
+ const urlWithParams = this.urlWithParams(req.url, req.params);
+ if (this._fetchPromisesCache.has(urlWithParams)) {
+ return this._fetchPromisesCache.get(urlWithParams)!;
}
- // TODO(andybons): Periodic cache invalidation.
- if (this._cache.has(req.url)) {
- return Promise.resolve(this._cache.get(req.url)!);
+ if (this._cache.has(urlWithParams)) {
+ return Promise.resolve(this._cache.get(urlWithParams)!);
}
this._fetchPromisesCache.set(
- req.url,
+ urlWithParams,
this.fetchJSON(req)
.then(response => {
if (response !== undefined) {
- this._cache.set(req.url, response);
+ this._cache.set(urlWithParams, response);
}
- this._fetchPromisesCache.set(req.url, undefined);
+ this._fetchPromisesCache.set(urlWithParams, undefined);
return response;
})
.catch(err => {
- this._fetchPromisesCache.set(req.url, undefined);
+ this._fetchPromisesCache.set(urlWithParams, undefined);
throw err;
})
);
- return this._fetchPromisesCache.get(req.url)!;
- }
-
- // if errFn is not set, then only Response possible
- send(req: SendRawRequest & {errFn?: undefined}): Promise<Response>;
-
- send(req: SendRawRequest): Promise<Response | undefined>;
-
- send(req: SendJSONRequest): Promise<ParsedJSON>;
-
- send(req: SendRequest): Promise<Response | ParsedJSON | undefined>;
-
- /**
- * Send an XHR.
- *
- * @return Promise resolves to Response/ParsedJSON only if the request is successful
- * (i.e. no exception and response.ok is true). If response fails then
- * promise resolves either to void if errFn is set or rejects if errFn
- * is not set */
- async send(req: SendRequest): Promise<Response | ParsedJSON | undefined> {
- const options: AuthRequestInit = {method: req.method};
- if (req.body) {
- options.headers = new Headers();
- options.headers.set(
- 'Content-Type',
- req.contentType || 'application/json'
- );
- options.body =
- typeof req.body === 'string' ? req.body : JSON.stringify(req.body);
- }
- if (req.headers) {
- if (!options.headers) {
- options.headers = new Headers();
- }
- for (const [name, value] of Object.entries(req.headers)) {
- options.headers.set(name, value);
- }
- }
- const url = req.url.startsWith('http') ? req.url : getBaseUrl() + req.url;
- const fetchReq: FetchRequest = {
- url,
- fetchOptions: options,
- anonymizedUrl: req.reportUrlAsIs ? url : req.anonymizedUrl,
- };
- let xhr;
- try {
- xhr = await this.fetch(fetchReq);
- } catch (err) {
- fireNetworkError(err as Error);
- if (req.errFn) {
- await req.errFn.call(undefined, null, err as Error);
- xhr = undefined;
- } else {
- throw err;
- }
- }
- if (xhr && !xhr.ok) {
- if (req.errFn) {
- await req.errFn.call(undefined, xhr);
- } else {
- fireServerError(xhr, fetchReq);
- }
- }
-
- if (req.parseResponse) {
- xhr = xhr && this.getResponseObject(xhr);
- }
- return xhr;
+ return this._fetchPromisesCache.get(urlWithParams)!;
}
invalidateFetchPromisesPrefix(prefix: string) {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts
index 9f0319eba3..0f94a4bccf 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright 2019 Google LLC
+ * Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../../../../test/common-test-setup';
@@ -8,16 +8,25 @@ import {
SiteBasedCache,
FetchPromisesCache,
GrRestApiHelper,
+ JSON_PREFIX,
+ readJSONResponsePayload,
+ parsePrefixedJSON,
} from './gr-rest-api-helper';
-import {assertFails, waitEventLoop} from '../../../../test/test-utils';
+import {
+ addListenerForTest,
+ assertFails,
+ makePrefixedJSON,
+ waitEventLoop,
+} from '../../../../test/test-utils';
import {FakeScheduler} from '../../../../services/scheduler/fake-scheduler';
import {RetryScheduler} from '../../../../services/scheduler/retry-scheduler';
-import {ParsedJSON} from '../../../../types/common';
import {HttpMethod} from '../../../../api/rest-api';
import {SinonFakeTimers} from 'sinon';
import {assert} from '@open-wc/testing';
import {AuthService} from '../../../../services/gr-auth/gr-auth';
import {GrAuthMock} from '../../../../services/gr-auth/gr-auth_mock';
+import {ParsedJSON} from '../../../../types/common';
+import {getBaseUrl} from '../../../../utils/url-util';
function makeParsedJSON<T>(val: T): ParsedJSON {
return val as unknown as ParsedJSON;
@@ -83,7 +92,7 @@ suite('gr-rest-api-helper tests', () => {
await waitEventLoop();
}
- suite('send()', () => {
+ suite('fetch()', () => {
setup(() => {
authFetchStub.returns(
Promise.resolve({
@@ -97,10 +106,11 @@ suite('gr-rest-api-helper tests', () => {
});
test('GET are sent to readScheduler', async () => {
- const promise = helper.send({
- method: HttpMethod.GET,
+ const promise = helper.fetch({
+ fetchOptions: {
+ method: HttpMethod.GET,
+ },
url: '/dummy/url',
- parseResponse: false,
});
assert.equal(writeScheduler.scheduled.length, 0);
await assertReadRequest();
@@ -109,16 +119,41 @@ suite('gr-rest-api-helper tests', () => {
});
test('PUT are sent to writeScheduler', async () => {
- const promise = helper.send({
- method: HttpMethod.PUT,
+ const promise = helper.fetch({
+ fetchOptions: {
+ method: HttpMethod.PUT,
+ },
url: '/dummy/url',
- parseResponse: false,
});
assert.equal(readScheduler.scheduled.length, 0);
await assertWriteRequest();
const res: Response = await promise;
assert.equal(await res.text(), 'Yay');
});
+
+ test('fetch calls auth fetch and logs', async () => {
+ const logStub = sinon.stub(helper, 'logCall');
+ const response = new Response(undefined, {status: 404});
+ const url = '/my/url';
+ const fetchOptions = {method: 'DELETE'};
+ authFetchStub.resolves(response);
+ const startTime = 123;
+ sinon.stub(Date, 'now').returns(startTime);
+ helper.fetch({url, fetchOptions, anonymizedUrl: url});
+
+ await assertWriteRequest();
+ assert.isTrue(logStub.calledOnce);
+ const expectedReq = {
+ url: getBaseUrl() + url,
+ fetchOptions,
+ anonymizedUrl: url,
+ };
+ assert.deepEqual(logStub.lastCall.args, [
+ expectedReq,
+ startTime,
+ response.status,
+ ]);
+ });
});
suite('fetchJSON()', () => {
@@ -150,6 +185,111 @@ suite('gr-rest-api-helper tests', () => {
const obj = await promise;
assert.deepEqual(obj, makeParsedJSON({hello: 'bonjour'}));
});
+
+ suite('error handling', () => {
+ let serverErrorCalled: boolean;
+ let networkErrorCalled: boolean;
+
+ setup(() => {
+ serverErrorCalled = false;
+ networkErrorCalled = false;
+ addListenerForTest(document, 'server-error', () => {
+ serverErrorCalled = true;
+ });
+ addListenerForTest(document, 'network-error', () => {
+ networkErrorCalled = true;
+ });
+ });
+
+ test('network error, promise rejects, event thrown', async () => {
+ authFetchStub.rejects(new Error('No response'));
+ const promise = helper.fetchJSON({url: '/dummy/url'});
+ await assertReadRequest();
+ const err = await assertFails(promise);
+ assert.equal((err as Error).message, 'No response');
+ await waitEventLoop();
+ assert.isTrue(networkErrorCalled);
+ assert.isFalse(serverErrorCalled);
+ });
+
+ test('network error, promise rejects, errFn called, no event', async () => {
+ const errFn = sinon.stub();
+ authFetchStub.rejects(new Error('No response'));
+ const promise = helper.fetchJSON({
+ url: '/dummy/url',
+ errFn,
+ });
+ await assertReadRequest();
+ const err = await assertFails(promise);
+ assert.equal((err as Error).message, 'No response');
+ await waitEventLoop();
+ assert.isTrue(errFn.called);
+ assert.isFalse(networkErrorCalled);
+ assert.isFalse(serverErrorCalled);
+ });
+
+ test('server error, promise resolves undefined, event thrown', async () => {
+ authFetchStub.returns(
+ Promise.resolve({
+ ...new Response(),
+ status: 400,
+ ok: false,
+ text() {
+ return Promise.resolve('Nope');
+ },
+ })
+ );
+ const promise = helper.fetchJSON({url: '/dummy/url'});
+ await assertReadRequest();
+ const resp = await promise;
+ assert.isUndefined(resp);
+ await waitEventLoop();
+ assert.isFalse(networkErrorCalled);
+ assert.isTrue(serverErrorCalled);
+ });
+
+ test('server error, promise resolves undefined, errFn called, no event', async () => {
+ authFetchStub.returns(
+ Promise.resolve({
+ ...new Response(),
+ status: 400,
+ ok: false,
+ text() {
+ return Promise.resolve('Nope');
+ },
+ })
+ );
+ const errFn = sinon.stub();
+ const promise = helper.fetchJSON({url: '/dummy/url', errFn});
+ await assertReadRequest();
+ const resp = await promise;
+ assert.isUndefined(resp);
+ await waitEventLoop();
+ assert.isTrue(errFn.called);
+ assert.isFalse(networkErrorCalled);
+ assert.isFalse(serverErrorCalled);
+ });
+
+ test('parsing error, promise rejects', async () => {
+ authFetchStub.returns(
+ Promise.resolve({
+ ...new Response(),
+ ok: true,
+ text() {
+ return Promise.resolve('not a prefixed json');
+ },
+ })
+ );
+ const errFn = sinon.stub();
+ const promise = helper.fetchJSON({url: '/dummy/url', errFn});
+ await assertReadRequest();
+ await assertFails(promise);
+ await waitEventLoop();
+ assert.isFalse(errFn.called);
+ assert.isFalse(networkErrorCalled);
+ assert.isFalse(serverErrorCalled);
+ });
+ });
});
test('cached results', () => {
@@ -158,9 +298,9 @@ suite('gr-rest-api-helper tests', () => {
.stub(helper, 'fetchJSON')
.callsFake(() => Promise.resolve(makeParsedJSON(++n)));
const promises = [];
- promises.push(helper.fetchCacheURL({url: '/foo'}));
- promises.push(helper.fetchCacheURL({url: '/foo'}));
- promises.push(helper.fetchCacheURL({url: '/foo'}));
+ promises.push(helper.fetchCacheJSON({url: '/foo'}));
+ promises.push(helper.fetchCacheJSON({url: '/foo'}));
+ promises.push(helper.fetchCacheJSON({url: '/foo'}));
return Promise.all(promises).then(results => {
assert.deepEqual(results, [
@@ -168,12 +308,41 @@ suite('gr-rest-api-helper tests', () => {
makeParsedJSON(1),
makeParsedJSON(1),
]);
- return helper.fetchCacheURL({url: '/foo'}).then(foo => {
+ return helper.fetchCacheJSON({url: '/foo'}).then(foo => {
assert.equal(foo, makeParsedJSON(1));
});
});
});
+ test('cached results with param', () => {
+ let n = 0;
+ sinon
+ .stub(helper, 'fetchJSON')
+ .callsFake(() => Promise.resolve(makeParsedJSON(++n)));
+ const promises = [];
+ promises.push(
+ helper.fetchCacheJSON({url: '/foo', params: {hello: 'world'}})
+ );
+ promises.push(helper.fetchCacheJSON({url: '/foo'}));
+ promises.push(
+ helper.fetchCacheJSON({url: '/foo', params: {hello: 'world'}})
+ );
+
+ return Promise.all(promises).then(results => {
+ assert.deepEqual(results, [
+ makeParsedJSON(1),
+ // The url without params is queried again, since it has different url.
+ makeParsedJSON(2),
+ makeParsedJSON(1),
+ ]);
+ return helper
+ .fetchCacheJSON({url: '/foo', params: {hello: 'world'}})
+ .then(foo => {
+ assert.equal(foo, makeParsedJSON(1));
+ });
+ });
+ });
+
test('cache invalidation', async () => {
cache.set('/foo/bar', makeParsedJSON(1));
cache.set('/bar', makeParsedJSON(2));
@@ -191,10 +360,11 @@ suite('gr-rest-api-helper tests', () => {
sp: 'hola',
gr: 'guten tag',
noval: null,
+ novaltoo: undefined,
});
assert.equal(
url,
- `${window.CANONICAL_PATH}/path/?sp=hola&gr=guten%20tag&noval`
+ `${window.CANONICAL_PATH}/path/?sp=hola&gr=guten%20tag&noval&novaltoo`
);
url = helper.urlWithParams('/path/', {
@@ -210,25 +380,6 @@ suite('gr-rest-api-helper tests', () => {
assert.equal(url, `${window.CANONICAL_PATH}/path/?l=c&l=b&l=a`);
});
- test('request callbacks can be canceled', async () => {
- let cancelCalled = false;
- authFetchStub.returns(
- Promise.resolve({
- body: {
- cancel() {
- cancelCalled = true;
- },
- },
- })
- );
- const cancelCondition = () => true;
- const promise = helper.fetchJSON({url: '/dummy/url', cancelCondition});
- await assertReadRequest();
- const obj = await promise;
- assert.isUndefined(obj);
- assert.isTrue(cancelCalled);
- });
-
suite('throwing in errFn', () => {
function throwInPromise(response?: Response | null, _?: Error) {
return response?.text().then(text => {
@@ -253,19 +404,6 @@ suite('gr-rest-api-helper tests', () => {
);
});
- test('errFn with Promise throw cause send to reject on error', async () => {
- const promise = helper.send({
- method: HttpMethod.GET,
- url: '/dummy/url',
- parseResponse: false,
- errFn: throwInPromise,
- });
- await assertReadRequest();
-
- const err = await assertFails(promise);
- assert.equal((err as Error).message, 'Nope');
- });
-
test('errFn with Promise throw cause fetchJSON to reject on error', async () => {
const promise = helper.fetchJSON({
url: '/dummy/url',
@@ -277,20 +415,7 @@ suite('gr-rest-api-helper tests', () => {
assert.equal((err as Error).message, 'Nope');
});
- test('errFn with immediate throw cause send to reject on error', async () => {
- const promise = helper.send({
- method: HttpMethod.GET,
- url: '/dummy/url',
- parseResponse: false,
- errFn: throwImmediately,
- });
- await assertReadRequest();
-
- const err = await assertFails(promise);
- assert.equal((err as Error).message, 'Error Callback error');
- });
-
- test('errFn with immediate Promise cause fetchJSON to reject on error', async () => {
+ test('errFn with immediate throw cause fetchJSON to reject on error', async () => {
const promise = helper.fetchJSON({
url: '/dummy/url',
errFn: throwImmediately,
@@ -313,12 +438,10 @@ suite('gr-rest-api-helper tests', () => {
);
});
- test('still call errFn when not retried', async () => {
+ test('non-retry scheduler errFn is called on 429 error', async () => {
const errFn = sinon.stub();
- const promise = helper.send({
- method: HttpMethod.GET,
+ const promise = helper.fetchJSON({
url: '/dummy/url',
- parseResponse: false,
errFn,
});
await assertReadRequest();
@@ -329,21 +452,21 @@ suite('gr-rest-api-helper tests', () => {
assert.isTrue(errFn.called);
});
- test('still pass through correctly when not retried', async () => {
- const promise = helper.send({
- method: HttpMethod.GET,
+ test('non-retry scheduler 429 error is returned without retrying', async () => {
+ const promise = helper.fetch({
url: '/dummy/url',
- parseResponse: false,
});
await assertReadRequest();
- // But we expect the result from the network to return a 429 error when
- // it's no longer being retried.
+ // With RetryScheduler we retry if the server returns response with 429
+ // status.
+ // If we are not using RetryScheduler the response with 429 should simply
+ // be returned from fetch without retrying.
const res: Response = await promise;
assert.equal(res.status, 429);
});
- test('are retried', async () => {
+ test('With RetryScheduler 429 errors are retried', async () => {
helper = new GrRestApiHelper(
cache,
authService,
@@ -351,10 +474,8 @@ suite('gr-rest-api-helper tests', () => {
new RetryScheduler<Response>(readScheduler, 1, 50),
writeScheduler
);
- const promise = helper.send({
- method: HttpMethod.GET,
+ const promise = helper.fetch({
url: '/dummy/url',
- parseResponse: false,
});
await assertReadRequest();
authFetchStub.returns(
@@ -375,4 +496,44 @@ suite('gr-rest-api-helper tests', () => {
assert.equal(await res.text(), 'Yay');
});
});
+
+ suite('reading responses', () => {
+ test('readResponsePayload', async () => {
+ const mockObject = {foo: 'bar', baz: 'foo'} as unknown as ParsedJSON;
+ const serial = makePrefixedJSON(mockObject);
+ const response = new Response(serial);
+ const payload = await readJSONResponsePayload(response);
+ assert.deepEqual(payload.parsed, mockObject);
+ assert.equal(payload.raw, serial);
+ });
+
+ test('parsePrefixedJSON', () => {
+ const obj = {x: 3, y: {z: 4}, w: 23} as unknown as ParsedJSON;
+ const serial = JSON_PREFIX + JSON.stringify(obj);
+ const result = parsePrefixedJSON(serial);
+ assert.deepEqual(result, obj);
+ });
+
+ test('parsing error', async () => {
+ const response = new Response('[');
+ const err: Error = await assertFails(readJSONResponsePayload(response));
+ assert.equal(
+ err.message,
+ 'Response payload is not prefixed json. Payload: ['
+ );
+ });
+ });
+
+ test('logCall only reports requests with anonymized URLs', async () => {
+ sinon.stub(Date, 'now').returns(200);
+ const handler = sinon.stub();
+ addListenerForTest(document, 'gr-rpc-log', handler);
+
+ helper.logCall({url: 'url'}, 100, 200);
+ assert.isFalse(handler.called);
+
+ helper.logCall({url: 'url', anonymizedUrl: 'not url'}, 100, 200);
+ await waitEventLoop();
+ assert.isTrue(handler.calledOnce);
+ });
});
diff --git a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts
index 69cdedde62..923a00ec3c 100644
--- a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts
+++ b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts
@@ -7,7 +7,7 @@ import '../../../embed/diff/gr-diff/gr-diff';
import {css, html, LitElement, nothing, PropertyValues} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {getAppContext} from '../../../services/app-context';
-import {Comment} from '../../../types/common';
+import {Comment, EDIT, BasePatchSetNum, RepoName} from '../../../types/common';
import {anyLineTooLong} from '../../../utils/diff-util';
import {
DiffLayer,
@@ -19,15 +19,17 @@ import {when} from 'lit/directives/when.js';
import {GrSyntaxLayerWorker} from '../../../embed/diff/gr-syntax-layer/gr-syntax-layer-worker';
import {resolve} from '../../../models/dependency';
import {highlightServiceToken} from '../../../services/highlight/highlight-service';
-import {NumericChangeId} from '../../../api/rest-api';
+import {FixSuggestionInfo, NumericChangeId} from '../../../api/rest-api';
import {changeModelToken} from '../../../models/change/change-model';
import {subscribe} from '../../lit/subscription-controller';
import {FilePreview} from '../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog';
import {userModelToken} from '../../../models/user/user-model';
import {createUserFixSuggestion} from '../../../utils/comment-util';
import {commentModelToken} from '../gr-comment-model/gr-comment-model';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {fire} from '../../../utils/event-util';
import {Interaction, Timing} from '../../../constants/reporting';
+import {createChangeUrl} from '../../../models/views/change';
declare global {
interface HTMLElementEventMap {
@@ -41,11 +43,22 @@ export interface OpenUserSuggestionPreviewEventDetail {
code: string;
}
+/**
+ * Diff preview for
+ * 1. code block suggestion vs commented Text
+ * or 2. fixSuggestionInfo that are attached to a comment.
+ *
+ * It shouldn't be created with both 1. and 2. but if it is
+ * it shows just for 1. (code block suggestion)
+ */
@customElement('gr-suggestion-diff-preview')
export class GrSuggestionDiffPreview extends LitElement {
@property({type: String})
suggestion?: string;
+ @property({type: Object})
+ fixSuggestionInfo?: FixSuggestionInfo;
+
@property({type: Boolean})
showAddSuggestionButton = false;
@@ -62,7 +75,9 @@ export class GrSuggestionDiffPreview extends LitElement {
layers: DiffLayer[] = [];
@state()
- previewLoadedFor?: string;
+ previewLoadedFor?: string | FixSuggestionInfo;
+
+ @state() repo?: RepoName;
@state()
changeNum?: NumericChangeId;
@@ -90,6 +105,8 @@ export class GrSuggestionDiffPreview extends LitElement {
private readonly getCommentModel = resolve(this, commentModelToken);
+ private readonly getNavigation = resolve(this, navigationToken);
+
private readonly syntaxLayer = new GrSyntaxLayerWorker(
resolve(this, highlightServiceToken),
() => getAppContext().reportingService
@@ -121,6 +138,11 @@ export class GrSuggestionDiffPreview extends LitElement {
() => this.getCommentModel().commentedText$,
commentedText => (this.commentedText = commentedText)
);
+ subscribe(
+ this,
+ () => this.getChangeModel().repo$,
+ x => (this.repo = x)
+ );
}
static override get styles() {
@@ -156,10 +178,16 @@ export class GrSuggestionDiffPreview extends LitElement {
this.fetchFixPreview();
}
}
+
+ if (changed.has('changeNum') || changed.has('comment')) {
+ if (this.previewLoadedFor !== this.fixSuggestionInfo) {
+ this.fetchfixSuggestionInfoPreview();
+ }
+ }
}
override render() {
- if (!this.suggestion) return nothing;
+ if (!this.suggestion && !this.fixSuggestionInfo) return nothing;
const code = this.suggestion;
return html`
${when(
@@ -233,6 +261,97 @@ export class GrSuggestionDiffPreview extends LitElement {
return res;
}
+ private async fetchfixSuggestionInfoPreview() {
+ if (
+ this.suggestion ||
+ !this.changeNum ||
+ !this.comment?.patch_set ||
+ !this.fixSuggestionInfo
+ )
+ return;
+
+ this.reporting.time(Timing.PREVIEW_FIX_LOAD);
+ const res = await this.restApiService.getFixPreview(
+ this.changeNum,
+ this.comment?.patch_set,
+ this.fixSuggestionInfo.replacements
+ );
+
+ if (!res) return;
+ const currentPreviews = Object.keys(res).map(key => {
+ return {filepath: key, preview: res[key]};
+ });
+ this.reporting.timeEnd(Timing.PREVIEW_FIX_LOAD, {
+ uuid: this.uuid,
+ });
+ if (currentPreviews.length > 0) {
+ this.preview = currentPreviews[0];
+ this.previewLoadedFor = this.fixSuggestionInfo;
+ }
+
+ return res;
+ }
+
+ /**
+ * Applies a fix (fix_suggestion in comment) previewed in
+ * `suggestion-diff-preview`, navigating to the new change URL with the EDIT
+ * patchset.
+ *
+ * Similar code flow is in gr-apply-fix-dialog.handleApplyFix
+ * Used in gr-fix-suggestions
+ */
+ public applyFixSuggestion() {
+ if (this.suggestion || !this.fixSuggestionInfo) return;
+ this.applyFix(this.fixSuggestionInfo);
+ }
+
+ /**
+ * Applies a fix (codeblock in comment message) previewed in
+ * `suggestion-diff-preview`, navigating to the new change URL with the EDIT
+ * patchset.
+ *
+ * Similar code flow is in gr-apply-fix-dialog.handleApplyFix
+ * Used in gr-user-suggestion-fix
+ */
+ public applyUserSuggestedFix() {
+ if (!this.comment || !this.suggestion || !this.commentedText) return;
+
+ const fixSuggestions = createUserFixSuggestion(
+ this.comment,
+ this.commentedText,
+ this.suggestion
+ );
+ this.applyFix(fixSuggestions[0]);
+ }
+
+ private async applyFix(fixSuggestion: FixSuggestionInfo) {
+ const changeNum = this.changeNum;
+ const basePatchNum = this.comment?.patch_set as BasePatchSetNum;
+ if (!changeNum || !basePatchNum || !fixSuggestion) return;
+
+ this.reporting.time(Timing.APPLY_FIX_LOAD);
+ const res = await this.restApiService.applyFixSuggestion(
+ changeNum,
+ basePatchNum,
+ fixSuggestion.replacements
+ );
+ this.reporting.timeEnd(Timing.APPLY_FIX_LOAD, {
+ method: '1-click',
+ description: fixSuggestion.description,
+ });
+ if (res?.ok) {
+ this.getNavigation().setUrl(
+ createChangeUrl({
+ changeNum,
+ repo: this.repo!,
+ patchNum: EDIT,
+ basePatchNum,
+ })
+ );
+ fire(this, 'apply-user-suggestion', {});
+ }
+ }
+
private overridePartialDiffPrefs() {
if (!this.diffPrefs) return undefined;
return {
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
index 15012056d0..7f70911a75 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -329,7 +329,12 @@ export class GrTextarea extends LitElement {
}
private handleEscKey(e: KeyboardEvent) {
- if (!this.isDropdownVisible()) {
+ // Esc should have normal behavior if the picker is closed, or "open" but
+ // with no results.
+ if (
+ !this.isDropdownVisible() ||
+ this.getVisibleDropdown().getCurrentText() === ''
+ ) {
return;
}
e.preventDefault();
@@ -338,7 +343,12 @@ export class GrTextarea extends LitElement {
}
private handleUpKey(e: KeyboardEvent) {
- if (!this.isDropdownVisible()) {
+ // Up should have normal behavior if the picker is closed, or "open" but
+ // with no results.
+ if (
+ !this.isDropdownVisible() ||
+ this.getVisibleDropdown().getCurrentText() === ''
+ ) {
return;
}
e.preventDefault();
@@ -348,7 +358,12 @@ export class GrTextarea extends LitElement {
}
private handleDownKey(e: KeyboardEvent) {
- if (!this.isDropdownVisible()) {
+ // Down should have normal behavior if the picker is closed, or "open" but
+ // with no results.
+ if (
+ !this.isDropdownVisible() ||
+ this.getVisibleDropdown().getCurrentText() === ''
+ ) {
return;
}
e.preventDefault();
@@ -358,8 +373,12 @@ export class GrTextarea extends LitElement {
}
private handleTabKey(e: KeyboardEvent) {
- // Tab should have normal behavior if the picker is closed.
- if (!this.isDropdownVisible()) {
+ // Tab should have normal behavior if the picker is closed, or "open" but
+ // with no results.
+ if (
+ !this.isDropdownVisible() ||
+ this.getVisibleDropdown().getCurrentText() === ''
+ ) {
return;
}
e.preventDefault();
@@ -587,7 +606,7 @@ export class GrTextarea extends LitElement {
// TODO(dhruvsri): merge with getAccountSuggestions in account-util
async computeReviewerSuggestions(): Promise<Item[]> {
return (
- (await this.restApiService.getSuggestedAccounts(
+ (await this.restApiService.queryAccounts(
this.currentSearchString ?? '',
/* number= */ 15,
this.changeNum,
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
index 4aef66eb1b..d84f5a7614 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
@@ -59,7 +59,7 @@ suite('gr-textarea tests', () => {
// updated.
const listenerStub = sinon.stub();
element.addEventListener('text-changed', listenerStub);
- stubRestApi('getSuggestedAccounts').returns(
+ stubRestApi('queryAccounts').returns(
Promise.resolve([
createAccountWithEmail('abc@google.com'),
createAccountWithEmail('abcdef@google.com'),
@@ -93,7 +93,7 @@ suite('gr-textarea tests', () => {
});
test('mention selector opens when previous char is \n', async () => {
- stubRestApi('getSuggestedAccounts').returns(
+ stubRestApi('queryAccounts').returns(
Promise.resolve([
{
...createAccountWithEmail('abc@google.com'),
@@ -130,7 +130,7 @@ suite('gr-textarea tests', () => {
test('mention suggestions cleared before request returns', async () => {
const promise = mockPromise<Item[]>();
- stubRestApi('getSuggestedAccounts').returns(promise);
+ stubRestApi('queryAccounts').returns(promise);
element.textarea!.focus();
await waitUntil(() => element.textarea!.focused === true);
@@ -164,7 +164,7 @@ suite('gr-textarea tests', () => {
test('mention dropdown shows suggestion for latest text', async () => {
const promise1 = mockPromise<Item[]>();
const promise2 = mockPromise<Item[]>();
- const suggestionStub = stubRestApi('getSuggestedAccounts');
+ const suggestionStub = stubRestApi('queryAccounts');
suggestionStub.returns(promise1);
element.textarea!.focus();
await waitUntil(() => element.textarea!.focused === true);
@@ -221,7 +221,7 @@ suite('gr-textarea tests', () => {
});
test('selecting mentions from dropdown', async () => {
- stubRestApi('getSuggestedAccounts').returns(
+ stubRestApi('queryAccounts').returns(
Promise.resolve([
createAccountWithEmail('abc@google.com'),
createAccountWithEmail('abcdef@google.com'),
@@ -254,7 +254,7 @@ suite('gr-textarea tests', () => {
const listenerStub = sinon.stub();
element.addEventListener('text-changed', listenerStub);
const resetSpy = sinon.spy(element, 'resetDropdown');
- stubRestApi('getSuggestedAccounts').returns(
+ stubRestApi('queryAccounts').returns(
Promise.resolve([
createAccountWithEmail('abc@google.com'),
createAccountWithEmail('abcdef@google.com'),
@@ -348,7 +348,7 @@ suite('gr-textarea tests', () => {
});
test('mention dropdown is cleared if @ is deleted', async () => {
- stubRestApi('getSuggestedAccounts').returns(
+ stubRestApi('queryAccounts').returns(
Promise.resolve([
createAccountWithEmail('abc@google.com'),
createAccountWithEmail('abcdef@google.com'),
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
index 4ccc635f4d..343de2a5cb 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
@@ -9,7 +9,7 @@ import {GrTooltip} from '../gr-tooltip/gr-tooltip';
import {css, html, LitElement, PropertyValues} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
-const BOTTOM_OFFSET = 7.2; // Height of the arrow in tooltip.
+const ARROW_HEIGHT = 7.2; // Height of the arrow in tooltip.
declare global {
interface HTMLElementTagNameMap {
@@ -141,7 +141,8 @@ export class GrTooltipContent extends LitElement {
// Set visibility to hidden before appending to the DOM so that
// calculations can be made based on the element’s size.
tooltip.style.visibility = 'hidden';
- document.body.appendChild(tooltip);
+ const parent = this.getTooltipParent(this);
+ parent.appendChild(tooltip);
await tooltip.updateComplete;
this._positionTooltip(tooltip);
tooltip.style.visibility = 'initial';
@@ -154,6 +155,22 @@ export class GrTooltipContent extends LitElement {
}
}
+ getTooltipParent(el: Node): Node {
+ if (el === document.body) {
+ return el;
+ }
+ if (el instanceof HTMLDialogElement) {
+ return el;
+ }
+ if (el instanceof ShadowRoot) {
+ return this.getTooltipParent(el.host);
+ }
+ if (el.parentNode) {
+ return this.getTooltipParent(el.parentNode);
+ }
+ return document.body;
+ }
+
_handleHideTooltip(e?: Event) {
if (this.isTouchDevice) {
return;
@@ -197,27 +214,62 @@ export class GrTooltipContent extends LitElement {
// private but used in tests.
_positionTooltip(tooltip: GrTooltip | null) {
if (tooltip === null) return;
- const rect = this.getBoundingClientRect();
- const boxRect = tooltip.getBoundingClientRect();
+ const hoveredRect = this.getBoundingClientRect();
+ const tooltipRect = tooltip.getBoundingClientRect();
if (!tooltip.parentElement) {
return;
}
const parentRect = tooltip.parentElement.getBoundingClientRect();
- const top = rect.top - parentRect.top;
- const left = rect.left - parentRect.left + (rect.width - boxRect.width) / 2;
- const right = parentRect.width - left - boxRect.width;
- if (left < 0) {
- tooltip.arrowCenterOffset = `${left}px`;
- } else if (right < 0) {
- tooltip.arrowCenterOffset = `${-0.5 * right}px`;
- }
- tooltip.style.left = `${Math.max(0, left)}px`;
-
- if (!this.positionBelow) {
- tooltip.style.top = `${Math.max(0, top)}px`;
- tooltip.style.transform = `translateY(calc(-100% - ${BOTTOM_OFFSET}px))`;
- } else {
- tooltip.style.top = `${top + rect.height + BOTTOM_OFFSET}px`;
+ // Use clientWidth to not include the scrollbars
+ const parentWidth = tooltip.parentElement.clientWidth;
+
+ const hoveredCenter =
+ 0.5 * (hoveredRect.left + hoveredRect.right) - parentRect.left;
+ const left = this.computeLeft(tooltipRect, hoveredCenter, parentWidth);
+ const {isBelow, top} = this.computeTop(
+ tooltipRect,
+ hoveredRect,
+ parentRect
+ );
+ const tooltipCenter = left + 0.5 * tooltipRect.width;
+
+ tooltip.arrowCenterOffset = `${hoveredCenter - tooltipCenter}px`;
+ tooltip.positionBelow = isBelow;
+ tooltip.style.top = `${top}px`;
+ tooltip.style.left = `${left}px`;
+ }
+
+ private computeLeft(
+ tooltipRect: DOMRect,
+ hoveredCenter: number,
+ parentWidth: number
+ ) {
+ let left = hoveredCenter - 0.5 * tooltipRect.width;
+ if (left + tooltipRect.width > parentWidth - 1) {
+ // Add 1px of extra padding. Without it on some browser zoom levels
+ // the hovercard is still considered going out of bounds and gets
+ // reshaped.
+ left = parentWidth - tooltipRect.width - 1;
+ }
+ return Math.max(0, left);
+ }
+
+ private computeTop(
+ tooltipRect: DOMRect,
+ hoveredRect: DOMRect,
+ parentRect: DOMRect
+ ): {
+ isBelow: boolean;
+ top: number;
+ } {
+ const top =
+ hoveredRect.top - parentRect.top - tooltipRect.height - ARROW_HEIGHT;
+ if (this.positionBelow || top < 0) {
+ return {
+ isBelow: true,
+ top: hoveredRect.bottom - parentRect.top + ARROW_HEIGHT,
+ };
}
+ return {isBelow: false, top};
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.ts b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.ts
index f4dbc3e53c..a9080e8bba 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.ts
@@ -24,6 +24,7 @@ suite('gr-tooltip-content tests', () => {
getBoundingClientRect() {
return parentRect;
},
+ clientWidth: parentRect.width,
},
};
}
@@ -67,24 +68,41 @@ suite('gr-tooltip-content tests', () => {
});
test('normal position', () => {
- sinon
- .stub(element, 'getBoundingClientRect')
- .callsFake(() => ({top: 100, left: 100, width: 200} as DOMRect));
+ sinon.stub(element, 'getBoundingClientRect').callsFake(
+ () =>
+ ({
+ top: 100,
+ left: 100,
+ width: 200,
+ right: 300,
+ height: 50,
+ bottom: 150,
+ } as DOMRect)
+ );
const tooltip = makeTooltip(
{height: 30, width: 50} as DOMRect,
{top: 0, left: 0, width: 1000} as DOMRect
) as GrTooltip;
element._positionTooltip(tooltip);
- assert.equal(tooltip.arrowCenterOffset, '0');
+ assert.equal(tooltip.arrowCenterOffset, '0px');
assert.equal(tooltip.style.left, '175px');
- assert.equal(tooltip.style.top, '100px');
+ // 100 - tooltip height (30) - arrow height (7.2)
+ assert.equal(tooltip.style.top, '62.8px');
});
test('left side position', async () => {
- sinon
- .stub(element, 'getBoundingClientRect')
- .callsFake(() => ({top: 100, left: 10, width: 50} as DOMRect));
+ sinon.stub(element, 'getBoundingClientRect').callsFake(
+ () =>
+ ({
+ top: 100,
+ left: 10,
+ width: 50,
+ right: 60,
+ height: 50,
+ bottom: 150,
+ } as DOMRect)
+ );
const tooltip = makeTooltip(
{height: 30, width: 120} as DOMRect,
{top: 0, left: 0, width: 1000} as DOMRect
@@ -92,32 +110,52 @@ suite('gr-tooltip-content tests', () => {
element._positionTooltip(tooltip);
await element.updateComplete;
- assert.isBelow(parseFloat(tooltip.arrowCenterOffset.replace(/px$/, '')), 0);
+ // Aligned with left edge
assert.equal(tooltip.style.left, '0px');
- assert.equal(tooltip.style.top, '100px');
+ // element center (35) - tooltip center (60)
+ assert.equal(tooltip.arrowCenterOffset, '-25px');
+ // 100 - tooltip height (30) - arrow height (7.2)
+ assert.equal(tooltip.style.top, '62.8px');
});
test('right side position', () => {
- sinon
- .stub(element, 'getBoundingClientRect')
- .callsFake(() => ({top: 100, left: 950, width: 50} as DOMRect));
+ sinon.stub(element, 'getBoundingClientRect').callsFake(
+ () =>
+ ({
+ top: 100,
+ left: 950,
+ width: 50,
+ right: 1000,
+ height: 50,
+ bottom: 150,
+ } as DOMRect)
+ );
const tooltip = makeTooltip(
{height: 30, width: 120} as DOMRect,
{top: 0, left: 0, width: 1000} as DOMRect
) as GrTooltip;
element._positionTooltip(tooltip);
- assert.isAbove(parseFloat(tooltip.arrowCenterOffset.replace(/px$/, '')), 0);
- assert.equal(tooltip.style.left, '915px');
- assert.equal(tooltip.style.top, '100px');
+ // Aligned with right edge: 1000 - tooltip width (120) - 1px pad
+ assert.equal(tooltip.style.left, '879px');
+ // element center (975) - tooltip center (939)
+ assert.equal(tooltip.arrowCenterOffset, '36px');
+ // 100 - tooltip height (30) - arrow height (7.2)
+ assert.equal(tooltip.style.top, '62.8px');
});
test('position to bottom', () => {
- sinon
- .stub(element, 'getBoundingClientRect')
- .callsFake(
- () => ({top: 100, left: 950, width: 50, height: 50} as DOMRect)
- );
+ sinon.stub(element, 'getBoundingClientRect').callsFake(
+ () =>
+ ({
+ top: 100,
+ left: 950,
+ width: 50,
+ height: 50,
+ right: 1000,
+ bottom: 150,
+ } as DOMRect)
+ );
const tooltip = makeTooltip(
{height: 30, width: 120} as DOMRect,
{top: 0, left: 0, width: 1000} as DOMRect
@@ -125,11 +163,42 @@ suite('gr-tooltip-content tests', () => {
element.positionBelow = true;
element._positionTooltip(tooltip);
- assert.isAbove(parseFloat(tooltip.arrowCenterOffset.replace(/px$/, '')), 0);
- assert.equal(tooltip.style.left, '915px');
+ // Aligned with right edge: 1000 - tooltip width (120) - 1px pad
+ assert.equal(tooltip.style.left, '879px');
+ // element center (975) - tooltip center (939)
+ assert.equal(tooltip.arrowCenterOffset, '36px');
+ // 150 + arrow height (7.2)
assert.equal(tooltip.style.top, '157.2px');
});
+ test('automatic flip to bottom', () => {
+ sinon.stub(element, 'getBoundingClientRect').callsFake(
+ () =>
+ ({
+ // Not enough space for arrow
+ top: 30,
+ left: 950,
+ width: 50,
+ height: 50,
+ right: 1000,
+ bottom: 80,
+ } as DOMRect)
+ );
+ const tooltip = makeTooltip(
+ {height: 30, width: 120} as DOMRect,
+ {top: 0, left: 0, width: 1000} as DOMRect
+ ) as GrTooltip;
+
+ element.positionBelow = true;
+ element._positionTooltip(tooltip);
+ // Aligned with right edge: 1000 - tooltip width (120) - 1px pad
+ assert.equal(tooltip.style.left, '879px');
+ // element center (975) - tooltip center (939)
+ assert.equal(tooltip.arrowCenterOffset, '36px');
+ // 150 + arrow height (7.2)
+ assert.equal(tooltip.style.top, '87.2px');
+ });
+
test('hides tooltip when detached', async () => {
const handleHideTooltipStub = sinon.stub(element, '_handleHideTooltip');
element.remove();
@@ -181,4 +250,61 @@ suite('gr-tooltip-content tests', () => {
await element.updateComplete;
assert.isNotOk(element.tooltip);
});
+
+ suite('getTooltipParent', () => {
+ let divInDialog: HTMLDivElement;
+ let divInDialogShadow: ShadowRoot;
+ let div: HTMLDivElement;
+ let divShadow: ShadowRoot;
+ let dialog: HTMLDialogElement;
+
+ setup(() => {
+ divInDialog = document.createElement('div');
+ divInDialogShadow = divInDialog.attachShadow({mode: 'open'});
+ div = document.createElement('div');
+ divShadow = div.attachShadow({mode: 'open'});
+ dialog = document.createElement('dialog');
+ document.body.appendChild(div);
+ document.body.appendChild(dialog);
+ dialog.appendChild(divInDialog);
+ });
+
+ test('tooltip in the div', () => {
+ const el = document.createElement('gr-tooltip-content');
+ divShadow.appendChild(el);
+ const tooltipParent = el.getTooltipParent(el);
+ assert.strictEqual(
+ tooltipParent,
+ document.body,
+ 'Tooltip expected to be attached to body'
+ );
+ });
+
+ test('tooltip in the dialog', () => {
+ const el = document.createElement('gr-tooltip-content');
+ dialog.appendChild(el);
+ const tooltipParent = el.getTooltipParent(el);
+ assert.strictEqual(
+ tooltipParent,
+ dialog,
+ 'Tooltip expected to be attached to dialog'
+ );
+ });
+
+ test('tooltip in the div in the dialog', () => {
+ const el = document.createElement('gr-tooltip-content');
+ divInDialogShadow.appendChild(el);
+ const tooltipParent = el.getTooltipParent(el);
+ assert.strictEqual(
+ tooltipParent,
+ dialog,
+ 'Tooltip expected to be attached to dialog'
+ );
+ });
+
+ teardown(() => {
+ document.body.removeChild(div);
+ document.body.removeChild(dialog);
+ });
+ });
});
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts
index c1ba729a5e..74cc9f7e7c 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts
@@ -80,7 +80,7 @@ export class GrTooltip extends LitElement {
override render() {
this.style.maxWidth = this.maxWidth;
- return html` <div class="tooltip">
+ return html` <div class="tooltip" aria-live="polite" role="tooltip">
<i
class="arrowPositionBelow arrow"
style=${styleMap({marginLeft: this.arrowCenterOffset})}
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts
index bc0cfba324..187e375408 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts
@@ -24,7 +24,7 @@ suite('gr-tooltip tests', () => {
assert.shadowDom.equal(
element,
/* HTML */ `
- <div class="tooltip">
+ <div class="tooltip" aria-live="polite" role="tooltip">
<i class="arrow arrowPositionBelow" style="margin-left:0;"> </i>
<div class="text">tooltipText</div>
<i class="arrow arrowPositionAbove" style="margin-left:0;"> </i>
diff --git a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts
index 63221232cc..c8de209163 100644
--- a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts
+++ b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts
@@ -8,12 +8,16 @@ import '../../shared/gr-icon/gr-icon';
import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
import '../gr-suggestion-diff-preview/gr-suggestion-diff-preview';
import {css, html, LitElement, nothing} from 'lit';
-import {customElement, state} from 'lit/decorators.js';
+import {customElement, state, query} from 'lit/decorators.js';
import {fire} from '../../../utils/event-util';
import {getDocUrl} from '../../../utils/url-util';
import {subscribe} from '../../lit/subscription-controller';
import {resolve} from '../../../models/dependency';
import {configModelToken} from '../../../models/config/config-model';
+import {GrSuggestionDiffPreview} from '../gr-suggestion-diff-preview/gr-suggestion-diff-preview';
+import {changeModelToken} from '../../../models/change/change-model';
+import {Comment, isDraft, PatchSetNumber} from '../../../types/common';
+import {commentModelToken} from '../gr-comment-model/gr-comment-model';
declare global {
interface HTMLElementEventMap {
@@ -29,10 +33,23 @@ export interface OpenUserSuggestionPreviewEventDetail {
@customElement('gr-user-suggestion-fix')
export class GrUserSuggestionsFix extends LitElement {
+ @query('gr-suggestion-diff-preview')
+ suggestionDiffPreview?: GrSuggestionDiffPreview;
+
@state() private docsBaseUrl = '';
+ @state() private applyingFix = false;
+
+ @state() latestPatchNum?: PatchSetNumber;
+
+ @state() comment?: Comment;
+
private readonly getConfigModel = resolve(this, configModelToken);
+ private readonly getChangeModel = resolve(this, changeModelToken);
+
+ private readonly getCommentModel = resolve(this, commentModelToken);
+
constructor() {
super();
subscribe(
@@ -40,6 +57,16 @@ export class GrUserSuggestionsFix extends LitElement {
() => this.getConfigModel().docsBaseUrl$,
docsBaseUrl => (this.docsBaseUrl = docsBaseUrl)
);
+ subscribe(
+ this,
+ () => this.getChangeModel().latestPatchNum$,
+ x => (this.latestPatchNum = x)
+ );
+ subscribe(
+ this,
+ () => this.getCommentModel().comment$,
+ comment => (this.comment = comment)
+ );
}
static override get styles() {
@@ -94,6 +121,17 @@ export class GrUserSuggestionsFix extends LitElement {
>
Show edit
</gr-button>
+ <gr-button
+ secondary
+ flatten
+ .loading=${this.applyingFix}
+ .disabled=${this.isApplyEditDisabled()}
+ class="action show-fix"
+ @click=${this.handleApplyFix}
+ .title=${this.computeApplyEditTooltip()}
+ >
+ Apply edit
+ </gr-button>
</div>
</div>
<gr-suggestion-diff-preview
@@ -105,6 +143,26 @@ export class GrUserSuggestionsFix extends LitElement {
if (!this.textContent) return;
fire(this, 'open-user-suggest-preview', {code: this.textContent});
}
+
+ async handleApplyFix() {
+ if (!this.textContent) return;
+ this.applyingFix = true;
+ await this.suggestionDiffPreview?.applyUserSuggestedFix();
+ this.applyingFix = false;
+ }
+
+ private isApplyEditDisabled() {
+ if (this.comment?.patch_set === undefined) return true;
+ if (isDraft(this.comment)) return true;
+ return this.comment.patch_set !== this.latestPatchNum;
+ }
+
+ private computeApplyEditTooltip() {
+ if (this.comment?.patch_set === undefined) return '';
+ return this.comment.patch_set !== this.latestPatchNum
+ ? 'You cannot apply this fix because it is from a previous patchset'
+ : '';
+ }
}
declare global {
diff --git a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
index b7d73b3b3a..52fd687614 100644
--- a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
@@ -67,6 +67,16 @@ suite('gr-user-suggestion-fix tests', () => {
tabindex="0"
flatten=""
>Show edit</gr-button
+ ><gr-button
+ aria-disabled="true"
+ disabled=""
+ class="action show-fix"
+ secondary=""
+ role="button"
+ tabindex="-1"
+ flatten=""
+ title="You cannot apply this fix because it is from a previous patchset"
+ >Apply edit</gr-button
>
</div>
</div>
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section.ts
index cf507e8889..7a368db9f3 100644
--- a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section.ts
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section.ts
@@ -6,10 +6,9 @@
import '../../../elements/shared/gr-button/gr-button';
import {html, LitElement} from 'lit';
import {property, state} from 'lit/decorators.js';
-import {DiffInfo, DiffViewMode, RenderPreferences} from '../../../api/diff';
+import {DiffViewMode} from '../../../api/diff';
import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
-import {diffClasses} from '../gr-diff/gr-diff-utils';
-import {getShowConfig} from './gr-context-controls';
+import {getShowConfig, showAbove, showBelow} from './gr-context-controls';
import {ifDefined} from 'lit/directives/if-defined.js';
import {when} from 'lit/directives/when.js';
import {subscribe} from '../../../elements/lit/subscription-controller';
@@ -20,30 +19,24 @@ import {
NO_COLUMNS,
} from '../gr-diff-model/gr-diff-model';
+/**
+ * This is a <tbody> diff section corresponding to a diff group of type
+ * CONTEXT_CONTROL. It typically contains three table rows: One padding row
+ * each at the top and at the bottom. The middle row contains the table cell
+ * (spanning mostly the entire table) that renders the actual control buttons
+ * in the <gr-context-controls> child component.
+ */
export class GrContextControlsSection extends LitElement {
- /** Should context controls be rendered for expanding above the section? */
- @property({type: Boolean}) showAbove = false;
-
- /** Should context controls be rendered for expanding below the section? */
- @property({type: Boolean}) showBelow = false;
-
/** Must be of type GrDiffGroupType.CONTEXT_CONTROL. */
@property({type: Object})
group?: GrDiffGroup;
- @property({type: Object})
- diff?: DiffInfo;
-
- @property({type: Object})
- renderPrefs?: RenderPreferences;
-
/**
* Semantic DOM diff testing does not work with just table fragments, so when
* running such tests the render() method has to wrap the DOM in a proper
* <table> element.
*/
- @state()
- addTableWrapperForTesting = false;
+ @state() addTableWrapperForTesting = false;
@state() viewMode: DiffViewMode = DiffViewMode.SIDE_BY_SIDE;
@@ -51,6 +44,8 @@ export class GrContextControlsSection extends LitElement {
@state() columnCount = 0;
+ @state() lineCountLeft = 0;
+
private readonly getDiffModel = resolve(this, diffModelToken);
constructor() {
@@ -70,6 +65,11 @@ export class GrContextControlsSection extends LitElement {
() => this.getDiffModel().columnCount$,
columnCount => (this.columnCount = columnCount)
);
+ subscribe(
+ this,
+ () => this.getDiffModel().lineCountLeft$,
+ lineCountLeft => (this.lineCountLeft = lineCountLeft)
+ );
}
/**
@@ -78,53 +78,48 @@ export class GrContextControlsSection extends LitElement {
* into the light DOM instead of the shadow DOM by overriding this method,
* which was the recommended workaround by the lit team.
* See also https://github.com/WICG/webcomponents/issues/79.
+ *
+ * Note that the <gr-context-controls> child component *does* use the shadow
+ * DOM, because the user has no intention to select the content of the context
+ * control buttons.
+ * TODO: That argument probably also applies to this component, so maybe
+ * reconsider making this a shadow DOM component!
*/
override createRenderRoot() {
return this;
}
private renderPaddingRow(whereClass: 'above' | 'below') {
- if (!this.showAbove && whereClass === 'above') return;
- if (!this.showBelow && whereClass === 'below') return;
+ if (!showAbove(this.group, this.lineCountLeft) && whereClass === 'above')
+ return;
+ if (!showBelow(this.group, this.lineCountLeft) && whereClass === 'below')
+ return;
const modeClass = this.isSideBySide() ? 'side-by-side' : 'unified';
const type = this.isSideBySide()
? GrDiffGroupType.CONTEXT_CONTROL
: undefined;
return html`
<tr
- class=${diffClasses('contextBackground', modeClass, whereClass)}
+ class=${['contextBackground', modeClass, whereClass].join(' ')}
left-type=${ifDefined(type)}
right-type=${ifDefined(type)}
>
${when(
this.columns.blame,
- () =>
- html`<td class=${diffClasses('blame')} data-line-number="0"></td>`
+ () => html`<td class="blame" data-line-number="0"></td>`
)}
${when(
this.columns.leftNumber,
- () => html`<td class=${diffClasses('contextLineNum')}></td>`
- )}
- ${when(
- this.columns.leftSign,
- () => html`<td class=${diffClasses('sign')}></td>`
- )}
- ${when(
- this.columns.leftContent,
- () => html`<td class=${diffClasses()}></td>`
+ () => html`<td class="contextLineNum"></td>`
)}
+ ${when(this.columns.leftSign, () => html`<td class="sign"></td>`)}
+ ${when(this.columns.leftContent, () => html`<td></td>`)}
${when(
this.columns.rightNumber,
- () => html`<td class=${diffClasses('contextLineNum')}></td>`
- )}
- ${when(
- this.columns.rightSign,
- () => html`<td class=${diffClasses('sign')}></td>`
- )}
- ${when(
- this.columns.rightContent,
- () => html`<td class=${diffClasses()}></td>`
+ () => html`<td class="contextLineNum"></td>`
)}
+ ${when(this.columns.rightSign, () => html`<td class="sign"></td>`)}
+ ${when(this.columns.rightContent, () => html`<td></td>`)}
</tr>
`;
}
@@ -137,29 +132,22 @@ export class GrContextControlsSection extends LitElement {
// Span all columns, but not the blame column.
let colspan = this.columnCount;
if (this.columns.blame) colspan--;
- const showConfig = getShowConfig(this.showAbove, this.showBelow);
+ const showConfig = getShowConfig(this.group, this.lineCountLeft);
return html`
- <tr class=${diffClasses('dividerRow', `show-${showConfig}`)}>
+ <tr class=${['dividerRow', `show-${showConfig}`].join(' ')}>
${when(
this.columns.blame,
- () =>
- html`<td class=${diffClasses('blame')} data-line-number="0"></td>`
+ () => html`<td class="blame" data-line-number="0"></td>`
)}
- <td class=${diffClasses('dividerCell')} colspan=${colspan}>
- <gr-context-controls
- class=${diffClasses()}
- .diff=${this.diff}
- .renderPreferences=${this.renderPrefs}
- .group=${this.group}
- .showConfig=${showConfig}
- >
- </gr-context-controls>
+ <td class="dividerCell" colspan=${colspan}>
+ <gr-context-controls .group=${this.group}> </gr-context-controls>
</td>
</tr>
`;
}
override render() {
+ if (this.group?.type !== GrDiffGroupType.CONTEXT_CONTROL) return;
const rows = html`
${this.renderPaddingRow('above')} ${this.createContextControlRow()}
${this.renderPaddingRow('below')}
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section_test.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section_test.ts
index 93db66ec9f..be7553e572 100644
--- a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section_test.ts
@@ -8,6 +8,10 @@ import {
wrapInProvider,
} from '../../../models/di-provider-element';
import '../../../test/common-test-setup';
+import {
+ createContextGroup,
+ createDiff,
+} from '../../../test/test-data-generators';
import {DiffModel, diffModelToken} from '../gr-diff-model/gr-diff-model';
import './gr-context-controls-section';
import {GrContextControlsSection} from './gr-context-controls-section';
@@ -27,14 +31,58 @@ suite('gr-context-controls-section test', () => {
)
)
).querySelector<GrContextControlsSection>('gr-context-controls-section')!;
+ await element.updateComplete;
+ diffModel.updateState({diff: createDiff()});
element.addTableWrapperForTesting = true;
await element.updateComplete;
});
- test('render: normal with showAbove and showBelow', async () => {
- element.showAbove = true;
- element.showBelow = true;
+ test('render nothing, if group is not set', async () => {
+ assert.lightDom.equal(element, '');
+ });
+
+ test('render above and below', async () => {
+ element.group = createContextGroup({offset: 10, count: 10});
+ await element.updateComplete;
+ assert.lightDom.equal(
+ element,
+ /* HTML */ `
+ <table>
+ <tbody>
+ <tr
+ class="above contextBackground side-by-side"
+ left-type="contextControl"
+ right-type="contextControl"
+ >
+ <td class="contextLineNum"></td>
+ <td></td>
+ <td class="contextLineNum"></td>
+ <td></td>
+ </tr>
+ <tr class="dividerRow show-both">
+ <td class="dividerCell" colspan="4">
+ <gr-context-controls showconfig="both"> </gr-context-controls>
+ </td>
+ </tr>
+ <tr
+ class="below contextBackground side-by-side"
+ left-type="contextControl"
+ right-type="contextControl"
+ >
+ <td class="contextLineNum"></td>
+ <td></td>
+ <td class="contextLineNum"></td>
+ <td></td>
+ </tr>
+ </tbody>
+ </table>
+ `
+ );
+ });
+
+ test('render above only', async () => {
+ element.group = createContextGroup({offset: 35, count: 10});
await element.updateComplete;
assert.lightDom.equal(
element,
@@ -42,33 +90,48 @@ suite('gr-context-controls-section test', () => {
<table>
<tbody>
<tr
- class="above contextBackground gr-diff side-by-side"
+ class="above contextBackground side-by-side"
left-type="contextControl"
right-type="contextControl"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="contextLineNum gr-diff"></td>
- <td class="gr-diff"></td>
- <td class="contextLineNum gr-diff"></td>
- <td class="gr-diff"></td>
+ <td class="contextLineNum"></td>
+ <td></td>
+ <td class="contextLineNum"></td>
+ <td></td>
+ </tr>
+ <tr class="dividerRow show-above">
+ <td class="dividerCell" colspan="4">
+ <gr-context-controls showconfig="above"> </gr-context-controls>
+ </td>
</tr>
- <tr class="dividerRow gr-diff show-both">
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="dividerCell gr-diff" colspan="4">
- <gr-context-controls class="gr-diff" showconfig="both">
- </gr-context-controls>
+ </tbody>
+ </table>
+ `
+ );
+ });
+
+ test('render below only', async () => {
+ element.group = createContextGroup({offset: 0, count: 10});
+ await element.updateComplete;
+ assert.lightDom.equal(
+ element,
+ /* HTML */ `
+ <table>
+ <tbody>
+ <tr class="dividerRow show-below">
+ <td class="dividerCell" colspan="4">
+ <gr-context-controls showconfig="below"> </gr-context-controls>
</td>
</tr>
<tr
- class="below contextBackground gr-diff side-by-side"
+ class="below contextBackground side-by-side"
left-type="contextControl"
right-type="contextControl"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="contextLineNum gr-diff"></td>
- <td class="gr-diff"></td>
- <td class="contextLineNum gr-diff"></td>
- <td class="gr-diff"></td>
+ <td class="contextLineNum"></td>
+ <td></td>
+ <td class="contextLineNum"></td>
+ <td></td>
</tr>
</tbody>
</table>
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
index e889b9063a..e1fc2ed350 100644
--- a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
@@ -14,24 +14,33 @@ import '@polymer/paper-listbox/paper-listbox';
import '@polymer/paper-tooltip/paper-tooltip';
import {of, EMPTY, Subject} from 'rxjs';
import {switchMap, delay} from 'rxjs/operators';
-
import '../../../elements/shared/gr-button/gr-button';
import {pluralize} from '../../../utils/string-util';
import {fire} from '../../../utils/event-util';
-import {DiffInfo} from '../../../types/diff';
import {assertIsDefined} from '../../../utils/common-util';
-import {css, html, LitElement, TemplateResult} from 'lit';
-import {property} from 'lit/decorators.js';
+import {
+ css,
+ html,
+ LitElement,
+ nothing,
+ PropertyValues,
+ TemplateResult,
+} from 'lit';
+import {property, state} from 'lit/decorators.js';
import {subscribe} from '../../../elements/lit/subscription-controller';
-
import {
ContextButtonType,
DiffContextButtonHoveredDetail,
RenderPreferences,
SyntaxBlock,
} from '../../../api/diff';
-
-import {GrDiffGroup, hideInContextControl} from '../gr-diff/gr-diff-group';
+import {
+ GrDiffGroup,
+ GrDiffGroupType,
+ hideInContextControl,
+} from '../gr-diff/gr-diff-group';
+import {resolve} from '../../../models/dependency';
+import {diffModelToken} from '../gr-diff-model/gr-diff-model';
declare global {
interface HTMLElementEventMap {
@@ -67,14 +76,25 @@ function findBlockTreePathForLine(
return [containingBlock].concat(innerPathInChild);
}
+/**
+ * 'above': Typically only for the context controls at the end of a file. So
+ * only show buttons "above" the middle line of the context control
+ * section.
+ * 'below': Typically only for the context controls at the beginning of a file.
+ * So only show buttons "below" the middle line of the context control
+ * section.
+ * 'both': Typically for the context controls in the middle of a file. So show
+ * two buttons, one for expanding from the top and one for expanding
+ * from the bottom.
+ */
export type GrContextControlsShowConfig = 'above' | 'below' | 'both';
-export function getShowConfig(
- showAbove: boolean,
- showBelow: boolean
-): GrContextControlsShowConfig {
- if (showAbove && !showBelow) return 'above';
- if (!showAbove && showBelow) return 'below';
+export function getShowConfig(group?: GrDiffGroup, lineCountLeft = 0) {
+ const above = showAbove(group, lineCountLeft);
+ const below = showBelow(group, lineCountLeft);
+
+ if (above && !below) return 'above';
+ if (!above && below) return 'below';
// Note that !showAbove && !showBelow also intentionally returns 'both'.
// This means the file is completely collapsed, which is unusual, but at least
@@ -82,16 +102,55 @@ export function getShowConfig(
return 'both';
}
-export class GrContextControls extends LitElement {
- @property({type: Object}) renderPreferences?: RenderPreferences;
+/** See GrContextControlsShowConfig for explanation of "above". */
+export function showAbove(group?: GrDiffGroup, lineCountLeft = 0) {
+ if (group?.type !== GrDiffGroupType.CONTEXT_CONTROL) return false;
- @property({type: Object}) diff?: DiffInfo;
+ // Note that we could as well use `right.start_line` here. And below we only
+ // use `left`, because we are comparing with `lineCountLeft`. But that is
+ // just an arbitrary choice.
+ const leftStart = group.lineRange.left.start_line;
+ const firstGroupIsSkipped = !!group.contextGroups[0].skip;
+ if (leftStart > 1 && !firstGroupIsSkipped) return true;
+
+ const leftEnd = group.lineRange.left.end_line;
+ const containsWholeFile = lineCountLeft === leftEnd - leftStart + 1;
+ return containsWholeFile;
+}
+/** See GrContextControlsShowConfig for explanation of "below". */
+export function showBelow(group?: GrDiffGroup, lineCountLeft = 0) {
+ if (group?.type !== GrDiffGroupType.CONTEXT_CONTROL) return false;
+
+ // Note that we could as well use `right.start_line` here. But we would then
+ // require a `lineCountRight` parameter for making the comparison.
+ const leftEnd = group.lineRange.left.end_line;
+ const lastGroupIsSkipped =
+ !!group.contextGroups[group.contextGroups.length - 1].skip;
+
+ return leftEnd < lineCountLeft && !lastGroupIsSkipped;
+}
+
+/**
+ * Renders context control buttons such as "+23 lines" or "+Block". It is only
+ * meant to be used to be rendered into a diff table cell of its parent
+ * component <gr-context-controls-section>.
+ */
+export class GrContextControls extends LitElement {
@property({type: Object}) group?: GrDiffGroup;
+ // This is just a property (and not a state), because we want to "reflect".
@property({type: String, reflect: true})
showConfig: GrContextControlsShowConfig = 'both';
+ @state() syntaxTreeRight?: SyntaxBlock[];
+
+ @state() renderPreferences?: RenderPreferences;
+
+ @state() lineCountLeft = 0;
+
+ private readonly getDiffModel = resolve(this, diffModelToken);
+
private expandButtonsHover = new Subject<{
eventType: 'enter' | 'leave';
buttonType: ContextButtonType;
@@ -219,6 +278,32 @@ export class GrContextControls extends LitElement {
constructor() {
super();
this.setupButtonHoverHandler();
+ subscribe(
+ this,
+ () => this.getDiffModel().syntaxTreeRight$,
+ syntaxTree => (this.syntaxTreeRight = syntaxTree)
+ );
+ subscribe(
+ this,
+ () => this.getDiffModel().renderPrefs$,
+ renderPrefs => (this.renderPreferences = renderPrefs)
+ );
+ subscribe(
+ this,
+ () => this.getDiffModel().lineCountLeft$,
+ lineCountLeft => {
+ this.lineCountLeft = lineCountLeft;
+ this.updateShowConfig();
+ }
+ );
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('group')) this.updateShowConfig();
+ }
+
+ private updateShowConfig() {
+ this.showConfig = getShowConfig(this.group, this.lineCountLeft);
}
private showBoth() {
@@ -265,7 +350,7 @@ export class GrContextControls extends LitElement {
}
private createExpandAllButtonContainer() {
- return html` <div class="gr-diff aboveBelowButtons fullExpansion">
+ return html` <div class="aboveBelowButtons fullExpansion">
${this.createContextButton(ContextButtonType.ALL, this.numLines())}
</div>`;
}
@@ -439,14 +524,12 @@ export class GrContextControls extends LitElement {
if (this.showAbove()) {
aboveBlockButton = this.createBlockButton(
ContextButtonType.BLOCK_ABOVE,
- this.numLines(),
this.group.lineRange.right.start_line - 1
);
}
if (this.showBelow()) {
belowBlockButton = this.createBlockButton(
ContextButtonType.BLOCK_BELOW,
- this.numLines(),
this.group.lineRange.right.end_line + 1
);
}
@@ -476,26 +559,28 @@ export class GrContextControls extends LitElement {
>`;
}
+ /**
+ * Creates a "expand until end of block" button. This is based on syntax tree
+ * information for the *right* side of the diff.
+ */
private createBlockButton(
buttonType: ContextButtonType,
- numLines: number,
- referenceLine: number
+ referenceLineRight: number
) {
- if (!this.diff?.meta_b) return;
- const syntaxTree = this.diff.meta_b.syntax_tree;
+ if (this.syntaxTreeRight === undefined) return;
const outlineSyntaxPath = findBlockTreePathForLine(
- referenceLine,
- syntaxTree
+ referenceLineRight,
+ this.syntaxTreeRight
);
- let linesToExpand = numLines;
+ let linesToExpand = this.numLines();
if (outlineSyntaxPath.length) {
const {range} = outlineSyntaxPath[outlineSyntaxPath.length - 1];
const targetLine =
buttonType === ContextButtonType.BLOCK_ABOVE
? range.end_line
: range.start_line;
- const distanceToTargetLine = Math.abs(targetLine - referenceLine);
- if (distanceToTargetLine < numLines) {
+ const distanceToTargetLine = Math.abs(targetLine - referenceLineRight);
+ if (distanceToTargetLine < this.numLines()) {
linesToExpand = distanceToTargetLine;
}
}
@@ -507,15 +592,8 @@ export class GrContextControls extends LitElement {
return this.createContextButton(buttonType, linesToExpand, tooltip);
}
- private hasValidProperties() {
- return !!(this.diff && this.group?.contextGroups?.length);
- }
-
override render() {
- if (!this.hasValidProperties()) {
- console.error('Invalid properties for gr-context-controls!');
- return html`<p>invalid properties</p>`;
- }
+ if (!this.group) return nothing;
return html`
<div class="horizontalFlex">
${this.createExpandAllButtonContainer()}
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls_test.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls_test.ts
index 215dc88408..20fc9c4e03 100644
--- a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls_test.ts
@@ -7,47 +7,23 @@ import '../../../test/common-test-setup';
import '../gr-diff/gr-diff-group';
import './gr-context-controls';
import {GrContextControls} from './gr-context-controls';
-
-import {GrDiffLine} from '../gr-diff/gr-diff-line';
-import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
-import {
- DiffFileMetaInfo,
- DiffInfo,
- GrDiffLineType,
- SyntaxBlock,
-} from '../../../api/diff';
+import {SyntaxBlock} from '../../../api/diff';
import {fixture, html, assert} from '@open-wc/testing';
import {waitEventLoop} from '../../../test/test-utils';
+import {createContextGroup} from '../../../test/test-data-generators';
suite('gr-context-control tests', () => {
let element: GrContextControls;
setup(async () => {
element = document.createElement('gr-context-controls');
- element.diff = {content: []} as any as DiffInfo;
+ element.lineCountLeft = 50;
element.renderPreferences = {};
const div = await fixture(html`<div></div>`);
div.appendChild(element);
await waitEventLoop();
});
- function createContextGroup(options: {offset?: number; count?: number}) {
- const offset = options.offset || 0;
- const numLines = options.count || 10;
- const lines = [];
- for (let i = 0; i < numLines; i++) {
- const line = new GrDiffLine(GrDiffLineType.BOTH);
- line.beforeNumber = offset + i + 1;
- line.afterNumber = offset + i + 1;
- line.text = 'lorem upsum';
- lines.push(line);
- }
- return new GrDiffGroup({
- type: GrDiffGroupType.CONTEXT_CONTROL,
- contextGroups: [new GrDiffGroup({type: GrDiffGroupType.BOTH, lines})],
- });
- }
-
test('no +10 buttons for 10 or less lines', async () => {
element.group = createContextGroup({count: 10});
@@ -62,7 +38,6 @@ suite('gr-context-control tests', () => {
test('context control at the top', async () => {
element.group = createContextGroup({offset: 0, count: 20});
- element.showConfig = 'below';
await waitEventLoop();
@@ -80,7 +55,6 @@ suite('gr-context-control tests', () => {
test('context control in the middle', async () => {
element.group = createContextGroup({offset: 10, count: 20});
- element.showConfig = 'both';
await waitEventLoop();
@@ -100,7 +74,6 @@ suite('gr-context-control tests', () => {
test('context control at the bottom', async () => {
element.group = createContextGroup({offset: 30, count: 20});
- element.showConfig = 'above';
await waitEventLoop();
@@ -118,15 +91,12 @@ suite('gr-context-control tests', () => {
function prepareForBlockExpansion(syntaxTree: SyntaxBlock[]) {
element.renderPreferences!.use_block_expansion = true;
- element.diff!.meta_b = {
- syntax_tree: syntaxTree,
- } as any as DiffFileMetaInfo;
+ element.syntaxTreeRight = syntaxTree;
}
test('context control with block expansion at the top', async () => {
prepareForBlockExpansion([]);
element.group = createContextGroup({offset: 0, count: 20});
- element.showConfig = 'below';
await waitEventLoop();
@@ -155,7 +125,6 @@ suite('gr-context-control tests', () => {
test('context control with block expansion in the middle', async () => {
prepareForBlockExpansion([]);
element.group = createContextGroup({offset: 10, count: 20});
- element.showConfig = 'both';
await waitEventLoop();
@@ -192,7 +161,6 @@ suite('gr-context-control tests', () => {
test('context control with block expansion at the bottom', async () => {
prepareForBlockExpansion([]);
element.group = createContextGroup({offset: 30, count: 20});
- element.showConfig = 'above';
await waitEventLoop();
@@ -218,7 +186,7 @@ suite('gr-context-control tests', () => {
);
});
- test('+ Block tooltip tooltip shows syntax block containing the target lines above and below', async () => {
+ test('+Block tooltip tooltip shows syntax block containing the target lines above and below', async () => {
prepareForBlockExpansion([
{
name: 'aSpecificFunction',
@@ -232,7 +200,6 @@ suite('gr-context-control tests', () => {
},
]);
element.group = createContextGroup({offset: 10, count: 20});
- element.showConfig = 'both';
await waitEventLoop();
@@ -284,7 +251,6 @@ suite('gr-context-control tests', () => {
},
]);
element.group = createContextGroup({offset: 10, count: 20});
- element.showConfig = 'both';
await waitEventLoop();
@@ -330,7 +296,6 @@ suite('gr-context-control tests', () => {
},
]);
element.group = createContextGroup({offset: 10, count: 20});
- element.showConfig = 'both';
await waitEventLoop();
const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
@@ -348,7 +313,6 @@ suite('gr-context-control tests', () => {
prepareForBlockExpansion([]);
element.group = createContextGroup({offset: 10, count: 20});
- element.showConfig = 'both';
await waitEventLoop();
const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
index a0406be588..642610a98d 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
@@ -23,7 +23,7 @@ import {getBaseUrl} from '../../../utils/url-util';
import {otherSide} from '../../../utils/diff-util';
import './gr-diff-text';
import {
- diffClasses,
+ findBlame,
GrDiffCommentThread,
isLongCommentRange,
isResponsive,
@@ -71,9 +71,6 @@ export class GrDiffRow extends LitElement {
private right$ = new BehaviorSubject<GrDiffLine | undefined>(undefined);
@property({type: Object})
- blameInfo?: BlameInfo;
-
- @property({type: Object})
responsiveMode?: DiffResponsiveMode;
@property({type: Boolean})
@@ -104,6 +101,8 @@ export class GrDiffRow extends LitElement {
@state() columns: ColumnsToShow = NO_COLUMNS;
+ @state() blameInfo?: BlameInfo;
+
/**
* Keeps track of whether diff layers have already been applied to the diff
* row. That happens after the DOM has been created in the `updated()`
@@ -154,6 +153,14 @@ export class GrDiffRow extends LitElement {
() => this.getDiffModel().columnsToShow$,
columnsToShow => (this.columns = columnsToShow)
);
+ subscribe(
+ this,
+ () => this.getDiffModel().blameInfo$,
+ blameInfos => {
+ const line = this.left?.lineNumber(Side.LEFT);
+ this.blameInfo = findBlame(blameInfos, line);
+ }
+ );
}
override willUpdate(changedProperties: PropertyValues) {
@@ -220,7 +227,7 @@ export class GrDiffRow extends LitElement {
const row = html`
<tr
${ref(this.tableRowRef)}
- class=${diffClasses('diff-row', ...classes)}
+ class=${['diff-row', ...classes].join(' ')}
left-type=${ifDefined(this.getType(Side.LEFT))}
right-type=${ifDefined(this.getType(Side.RIGHT))}
tabindex="-1"
@@ -292,7 +299,7 @@ export class GrDiffRow extends LitElement {
return html`
<td
${ref(this.blameCellRef)}
- class=${diffClasses('blame')}
+ class="blame"
data-line-number=${this.left?.beforeNumber ?? 0}
>${this.renderBlameElement()}</td>
`;
@@ -312,11 +319,11 @@ export class GrDiffRow extends LitElement {
// td.blame has `white-space: pre`, so prettier must not add spaces.
// prettier-ignore
- return html`<span class=${diffClasses(...extras)}
- ><a href=${url} class=${diffClasses('blameDate')}>${date}</a
- ><span class=${diffClasses('blameAuthor')}> ${shortName}</span
- ><gr-hovercard class=${diffClasses()}>
- <span class=${diffClasses('blameHoverCard')}>
+ return html`<span class=${extras.join(' ')}
+ ><a href=${url} class="blameDate">${date}</a
+ ><span class="blameAuthor"> ${shortName}</span
+ ><gr-hovercard>
+ <span class="blameHoverCard">
Commit ${commit.id}<br />
Author: ${commit.author}<br />
Date: ${date}<br />
@@ -337,13 +344,13 @@ export class GrDiffRow extends LitElement {
const blankClass = isBlank ? 'blankLineNum' : '';
return html`<td
${ref(this.lineNumberRef(side))}
- class=${diffClasses(side, blankClass)}
+ class=${[side, blankClass].join(' ')}
></td>`;
}
return html`<td
${ref(this.lineNumberRef(side))}
- class=${diffClasses(side, 'lineNum')}
+ class=${[side, 'lineNum'].join(' ')}
data-value=${lineNumber}
>
${this.renderLineNumberButton(line, lineNumber, side)}
@@ -362,7 +369,7 @@ export class GrDiffRow extends LitElement {
return html`
<button
id=${this.lineNumberId(side)}
- class=${diffClasses('lineNumButton', side)}
+ class=${['lineNumButton', side].join(' ')}
tabindex="-1"
data-value=${lineNumber}
aria-label=${ifDefined(
@@ -425,7 +432,7 @@ export class GrDiffRow extends LitElement {
return html`
<td
${ref(this.contentCellRef(side))}
- class=${diffClasses(...extras)}
+ class=${extras.join(' ')}
@click=${() => {
if (lineNumber) {
this.getDiffModel().selectLine(lineNumber, side);
@@ -459,7 +466,7 @@ export class GrDiffRow extends LitElement {
if (!line.hasIntralineInfo) extras.push('no-intraline-info');
const sign = isAdd ? '+' : isRemove ? '-' : '';
- return html`<td class=${diffClasses(...extras)}>${sign}</td>`;
+ return html`<td class=${extras.join(' ')}>${sign}</td>`;
}
private renderLostMessage(side: Side) {
@@ -578,7 +585,7 @@ export class GrDiffRow extends LitElement {
// .content has `white-space: pre`, so prettier must not add spaces.
// prettier-ignore
return html`<div
- class=${diffClasses('contentText')}
+ class="contentText"
data-side=${ifDefined(side)}
id=${this.contentId(side)}
>${textElement}</div>`;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts
index 95b0357e0f..1526ce33de 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts
@@ -34,16 +34,15 @@ suite('gr-diff-row test', () => {
<tbody>
<tr
aria-labelledby="left-button-1 left-content-1 right-button-1 right-content-1"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="1"></td>
- <td class="gr-diff left lineNum" data-value="1">
+ <td class="left lineNum" data-value="1">
<button
aria-label="1 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="1"
id="left-button-1"
tabindex="-1"
@@ -51,9 +50,9 @@ suite('gr-diff-row test', () => {
1
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-1"
>
@@ -61,10 +60,10 @@ suite('gr-diff-row test', () => {
</div>
</div>
</td>
- <td class="gr-diff lineNum right" data-value="1">
+ <td class="lineNum right" data-value="1">
<button
aria-label="1 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="1"
id="right-button-1"
tabindex="-1"
@@ -72,9 +71,9 @@ suite('gr-diff-row test', () => {
1
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-1"
>
@@ -106,14 +105,13 @@ suite('gr-diff-row test', () => {
<tbody>
<tr
aria-labelledby="left-button-1 right-button-1 right-content-1"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="1"></td>
- <td class="gr-diff left lineNum" data-value="1">
+ <td class="left lineNum" data-value="1">
<button
aria-label="1 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="1"
id="left-button-1"
tabindex="-1"
@@ -121,10 +119,10 @@ suite('gr-diff-row test', () => {
1
</button>
</td>
- <td class="gr-diff lineNum right" data-value="1">
+ <td class="lineNum right" data-value="1">
<button
aria-label="1 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="1"
id="right-button-1"
tabindex="-1"
@@ -132,12 +130,8 @@ suite('gr-diff-row test', () => {
1
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
- <div
- class="contentText gr-diff"
- data-side="right"
- id="right-content-1"
- >
+ <td class="both content no-intraline-info right">
+ <div class="contentText" data-side="right" id="right-content-1">
<gr-diff-text data-side="right"> lorem ipsum </gr-diff-text>
</div>
</td>
@@ -163,20 +157,19 @@ suite('gr-diff-row test', () => {
<tbody>
<tr
aria-labelledby="right-button-1 right-content-1"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="blank"
right-type="add"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="blankLineNum gr-diff left"></td>
- <td class="blank gr-diff left no-intraline-info">
- <div class="contentText gr-diff" data-side="left"></div>
+ <td class="blankLineNum left"></td>
+ <td class="blank left no-intraline-info">
+ <div class="contentText" data-side="left"></div>
</td>
- <td class="gr-diff lineNum right" data-value="1">
+ <td class="lineNum right" data-value="1">
<button
aria-label="1 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="1"
id="right-button-1"
tabindex="-1"
@@ -184,12 +177,8 @@ suite('gr-diff-row test', () => {
1
</button>
</td>
- <td class="add content gr-diff no-intraline-info right">
- <div
- class="contentText gr-diff"
- data-side="right"
- id="right-content-1"
- >
+ <td class="add content no-intraline-info right">
+ <div class="contentText" data-side="right" id="right-content-1">
<gr-diff-text data-side="right"> lorem ipsum </gr-diff-text>
</div>
</td>
@@ -214,16 +203,15 @@ suite('gr-diff-row test', () => {
<tbody>
<tr
aria-labelledby="left-button-1 left-content-1"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="remove"
right-type="blank"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="1"></td>
- <td class="gr-diff left lineNum" data-value="1">
+ <td class="left lineNum" data-value="1">
<button
aria-label="1 removed"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="1"
id="left-button-1"
tabindex="-1"
@@ -231,18 +219,14 @@ suite('gr-diff-row test', () => {
1
</button>
</td>
- <td class="content gr-diff left no-intraline-info remove">
- <div
- class="contentText gr-diff"
- data-side="left"
- id="left-content-1"
- >
+ <td class="content left no-intraline-info remove">
+ <div class="contentText" data-side="left" id="left-content-1">
<gr-diff-text data-side="left"> lorem ipsum </gr-diff-text>
</div>
</td>
- <td class="blankLineNum gr-diff right"></td>
- <td class="blank gr-diff no-intraline-info right">
- <div class="contentText gr-diff" data-side="right"></div>
+ <td class="blankLineNum right"></td>
+ <td class="blank no-intraline-info right">
+ <div class="contentText" data-side="right"></div>
</td>
</tr>
<slot name="post-left-line-1"></slot>
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
index d2498019c2..a189e05804 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
@@ -15,7 +15,7 @@ import {
DiffPreferencesInfo,
} from '../../../api/diff';
import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
-import {diffClasses, getResponsiveMode} from '../gr-diff/gr-diff-utils';
+import {getResponsiveMode} from '../gr-diff/gr-diff-utils';
import {GrDiffRow} from './gr-diff-row';
import '../gr-context-controls/gr-context-controls-section';
import '../gr-context-controls/gr-context-controls';
@@ -23,7 +23,6 @@ import '../gr-range-header/gr-range-header';
import './gr-diff-row';
import {when} from 'lit/directives/when.js';
import {fire} from '../../../utils/event-util';
-import {countLines} from '../../../utils/diff-util';
import {resolve} from '../../../models/dependency';
import {
ColumnsToShow,
@@ -142,7 +141,7 @@ export class GrDiffSection extends LitElement {
this.diffPrefs?.show_file_comment_button === false ||
this.renderPrefs?.show_file_comment_button === false;
const body = html`
- <tbody class=${diffClasses(...extras)}>
+ <tbody class=${extras.join(' ')}>
${this.renderContextControls()} ${this.renderMoveControls()}
${pairs.map(pair => {
const leftClass = `left-${pair.left.lineNumber(Side.LEFT)}`;
@@ -191,26 +190,8 @@ export class GrDiffSection extends LitElement {
private renderContextControls() {
if (this.group?.type !== GrDiffGroupType.CONTEXT_CONTROL) return;
-
- const leftStart = this.group.lineRange.left.start_line;
- const leftEnd = this.group.lineRange.left.end_line;
- const firstGroupIsSkipped = !!this.group.contextGroups[0].skip;
- const lastGroupIsSkipped =
- !!this.group.contextGroups[this.group.contextGroups.length - 1].skip;
- const lineCountLeft = countLines(this.diff, Side.LEFT);
- const containsWholeFile = lineCountLeft === leftEnd - leftStart + 1;
- const showAbove =
- (leftStart > 1 && !firstGroupIsSkipped) || containsWholeFile;
- const showBelow = leftEnd < lineCountLeft && !lastGroupIsSkipped;
-
return html`
- <gr-context-controls-section
- .showAbove=${showAbove}
- .showBelow=${showBelow}
- .group=${this.group}
- .diff=${this.diff}
- .renderPrefs=${this.renderPrefs}
- >
+ <gr-context-controls-section .group=${this.group}>
</gr-context-controls-section>
`;
}
@@ -225,41 +206,30 @@ export class GrDiffSection extends LitElement {
private renderMoveControls() {
if (!this.group?.moveDetails) return;
const movedIn = this.group.adds.length > 0;
- const plainCell = html`<td class=${diffClasses()}></td>`;
+ const plainCell = html`<td></td>`;
const moveCell = html`
- <td class=${diffClasses('moveHeader')}>
- <gr-range-header class=${diffClasses()} icon="move_item">
+ <td class="moveHeader">
+ <gr-range-header icon="move_item">
${this.renderMoveDescription(movedIn)}
</gr-range-header>
</td>
`;
return html`
- <tr
- class=${diffClasses('moveControls', movedIn ? 'movedIn' : 'movedOut')}
- >
- ${when(
- this.columns.blame,
- () => html`<td class=${diffClasses('blame')}></td>`
- )}
+ <tr class=${['moveControls', movedIn ? 'movedIn' : 'movedOut'].join(' ')}>
+ ${when(this.columns.blame, () => html`<td class="blame"></td>`)}
${when(
this.columns.leftNumber,
- () => html`<td class=${diffClasses('moveControlsLineNumCol')}></td>`
- )}
- ${when(
- this.columns.leftSign,
- () => html`<td class=${diffClasses('sign')}></td>`
+ () => html`<td class="moveControlsLineNumCol"></td>`
)}
+ ${when(this.columns.leftSign, () => html`<td class="sign"></td>`)}
${when(this.columns.leftContent, () =>
movedIn ? plainCell : moveCell
)}
${when(
this.columns.rightNumber,
- () => html`<td class=${diffClasses('moveControlsLineNumCol')}></td>`
- )}
- ${when(
- this.columns.rightSign,
- () => html`<td class=${diffClasses('sign')}></td>`
+ () => html`<td class="moveControlsLineNumCol"></td>`
)}
+ ${when(this.columns.rightSign, () => html`<td class="sign"></td>`)}
${when(this.columns.rightContent, () =>
movedIn || this.isUnifiedDiff() ? moveCell : plainCell
)}
@@ -275,20 +245,18 @@ export class GrDiffSection extends LitElement {
const direction = movedIn ? 'from' : 'to';
const textLabel = `Moved ${andChangedLabel}${direction} lines `;
return html`
- <div class=${diffClasses()}>
- <span class=${diffClasses()}>${textLabel}</span>
+ <div>
+ <span>${textLabel}</span>
${this.renderMovedLineAnchor(range.start, otherSide)}
- <span class=${diffClasses()}> - </span>
+ <span> - </span>
${this.renderMovedLineAnchor(range.end, otherSide)}
</div>
`;
}
return html`
- <div class=${diffClasses()}>
- <span class=${diffClasses()}
- >${movedIn ? 'Moved in' : 'Moved out'}</span
- >
+ <div>
+ <span>${movedIn ? 'Moved in' : 'Moved out'}</span>
</div>
`;
}
@@ -299,11 +267,7 @@ export class GrDiffSection extends LitElement {
this.handleMovedLineAnchorClick(e.target, side, line);
};
// `href` is not actually used but important for Screen Readers
- return html`
- <a class=${diffClasses()} href=${`#${line}`} @click=${listener}
- >${line}</a
- >
- `;
+ return html`<a href=${`#${line}`} @click=${listener}>${line}</a>`;
}
private handleMovedLineAnchorClick(
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts
index e85e945924..d5e471b6f1 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts
@@ -50,21 +50,20 @@ suite('gr-diff-section test', () => {
/* HTML */ `
<table>
<tbody>
- <tr class="gr-diff moveControls movedOut">
- <td class="blame gr-diff"></td>
- <td class="gr-diff moveControlsLineNumCol"></td>
- <td class="gr-diff moveHeader">
- <gr-range-header class="gr-diff" icon="move_item">
- <div class="gr-diff">
- <span class="gr-diff"> Moved to lines </span>
- <a class="gr-diff" href="#1"> 1 </a>
- <span class="gr-diff"> - </span>
- <a class="gr-diff" href="#2"> 2 </a>
+ <tr class="moveControls movedOut">
+ <td class="moveControlsLineNumCol"></td>
+ <td class="moveHeader">
+ <gr-range-header icon="move_item">
+ <div>
+ <span> Moved to lines </span>
+ <a href="#1"> 1 </a>
+ <span> - </span>
+ <a href="#2"> 2 </a>
</div>
</gr-range-header>
</td>
- <td class="gr-diff moveControlsLineNumCol"></td>
- <td class="gr-diff"></td>
+ <td class="moveControlsLineNumCol"></td>
+ <td></td>
</tr>
</tbody>
</table>
@@ -87,17 +86,16 @@ suite('gr-diff-section test', () => {
/* HTML */ `
<table>
<tbody>
- <tr class="gr-diff moveControls movedOut">
- <td class="blame gr-diff"></td>
- <td class="gr-diff moveControlsLineNumCol"></td>
- <td class="gr-diff moveControlsLineNumCol"></td>
- <td class="gr-diff moveHeader">
- <gr-range-header class="gr-diff" icon="move_item">
- <div class="gr-diff">
- <span class="gr-diff"> Moved to lines </span>
- <a class="gr-diff" href="#1"> 1 </a>
- <span class="gr-diff"> - </span>
- <a class="gr-diff" href="#2"> 2 </a>
+ <tr class="moveControls movedOut">
+ <td class="moveControlsLineNumCol"></td>
+ <td class="moveControlsLineNumCol"></td>
+ <td class="moveHeader">
+ <gr-range-header icon="move_item">
+ <div>
+ <span> Moved to lines </span>
+ <a href="#1"> 1 </a>
+ <span> - </span>
+ <a href="#2"> 2 </a>
</div>
</gr-range-header>
</td>
@@ -135,19 +133,18 @@ suite('gr-diff-section test', () => {
<slot name="post-left-line-1"></slot>
<slot name="post-right-line-1"></slot>
<table>
- <tbody class="both gr-diff section">
+ <tbody class="both section">
<tr
aria-labelledby="left-button-1 left-content-1 right-button-1 right-content-1"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="1"></td>
- <td class="gr-diff left lineNum" data-value="1">
+ <td class="left lineNum" data-value="1">
<button
aria-label="1 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="1"
id="left-button-1"
tabindex="-1"
@@ -155,19 +152,15 @@ suite('gr-diff-section test', () => {
1
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
- <div
- class="contentText gr-diff"
- data-side="left"
- id="left-content-1"
- >
+ <td class="both content left no-intraline-info">
+ <div class="contentText" data-side="left" id="left-content-1">
<gr-diff-text data-side="left">asdf</gr-diff-text>
</div>
</td>
- <td class="gr-diff lineNum right" data-value="1">
+ <td class="lineNum right" data-value="1">
<button
aria-label="1 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="1"
id="right-button-1"
tabindex="-1"
@@ -175,28 +168,23 @@ suite('gr-diff-section test', () => {
1
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
- <div
- class="contentText gr-diff"
- data-side="right"
- id="right-content-1"
- >
+ <td class="both content no-intraline-info right">
+ <div class="contentText" data-side="right" id="right-content-1">
<gr-diff-text data-side="right">asdf </gr-diff-text>
</div>
</td>
</tr>
<tr
aria-labelledby="left-button-1 left-content-1 right-button-1 right-content-1"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="1"></td>
- <td class="gr-diff left lineNum" data-value="1">
+ <td class="left lineNum" data-value="1">
<button
aria-label="1 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="1"
id="left-button-1"
tabindex="-1"
@@ -204,19 +192,15 @@ suite('gr-diff-section test', () => {
1
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
- <div
- class="contentText gr-diff"
- data-side="left"
- id="left-content-1"
- >
+ <td class="both content left no-intraline-info">
+ <div class="contentText" data-side="left" id="left-content-1">
<gr-diff-text data-side="left"> qwer</gr-diff-text>
</div>
</td>
- <td class="gr-diff lineNum right" data-value="1">
+ <td class="lineNum right" data-value="1">
<button
aria-label="1 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="1"
id="right-button-1"
tabindex="-1"
@@ -224,28 +208,23 @@ suite('gr-diff-section test', () => {
1
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
- <div
- class="contentText gr-diff"
- data-side="right"
- id="right-content-1"
- >
+ <td class="both content no-intraline-info right">
+ <div class="contentText" data-side="right" id="right-content-1">
<gr-diff-text data-side="right">qwer </gr-diff-text>
</div>
</td>
</tr>
<tr
aria-labelledby="left-button-1 left-content-1 right-button-1 right-content-1"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="1"></td>
- <td class="gr-diff left lineNum" data-value="1">
+ <td class="left lineNum" data-value="1">
<button
aria-label="1 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="1"
id="left-button-1"
tabindex="-1"
@@ -253,19 +232,15 @@ suite('gr-diff-section test', () => {
1
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
- <div
- class="contentText gr-diff"
- data-side="left"
- id="left-content-1"
- >
+ <td class="both content left no-intraline-info">
+ <div class="contentText" data-side="left" id="left-content-1">
<gr-diff-text data-side="left">zxcv </gr-diff-text>
</div>
</td>
- <td class="gr-diff lineNum right" data-value="1">
+ <td class="lineNum right" data-value="1">
<button
aria-label="1 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="1"
id="right-button-1"
tabindex="-1"
@@ -273,12 +248,8 @@ suite('gr-diff-section test', () => {
1
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
- <div
- class="contentText gr-diff"
- data-side="right"
- id="right-content-1"
- >
+ <td class="both content no-intraline-info right">
+ <div class="contentText" data-side="right" id="right-content-1">
<gr-diff-text data-side="right">zxcv </gr-diff-text>
</div>
</td>
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts
index 5161b18173..2156a5b18f 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts
@@ -6,7 +6,6 @@
import {LitElement, html, TemplateResult} from 'lit';
import {property} from 'lit/decorators.js';
import {styleMap} from 'lit/directives/style-map.js';
-import {diffClasses} from '../gr-diff/gr-diff-utils';
const SURROGATE_PAIR = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
@@ -113,7 +112,7 @@ export class GrDiffText extends LitElement {
tabSize = this.tabSize;
}
const piece = html`<span
- class=${diffClasses('tab')}
+ class="tab"
style=${styleMap({'tab-size': `${tabSize}`})}
>${TAB}</span
>`;
@@ -135,9 +134,9 @@ export class GrDiffText extends LitElement {
/** Render a line break, don't advance text offset, reset col position. */
private renderLineBreak() {
if (this.isResponsive) {
- this.pieces.push(html`<wbr class=${diffClasses()}></wbr>`);
+ this.pieces.push(html`<wbr></wbr>`);
} else {
- this.pieces.push(html`<span class=${diffClasses('br')}></span>`);
+ this.pieces.push(html`<span class="br"></span>`);
}
// this.textOffset += 0;
this.columnPos = 0;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts
index 3858bedc5b..a458a61ef7 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts
@@ -8,9 +8,9 @@ import './gr-diff-text';
import {GrDiffText} from './gr-diff-text';
import {fixture, html, assert} from '@open-wc/testing';
-const LINE_BREAK = '<span class="gr-diff br"></span>';
+const LINE_BREAK = '<span class="br"></span>';
-const LINE_BREAK_WBR = '<wbr class="gr-diff"></wbr>';
+const LINE_BREAK_WBR = '<wbr></wbr>';
const TAB = '<span class="" style=""></span>';
@@ -86,21 +86,21 @@ suite('gr-diff-text test', () => {
element.tabSize = 4;
await check(
'\t',
- /* HTML */ '<span class="gr-diff tab" style="tab-size:4;"></span>'
+ /* HTML */ '<span class="tab" style="tab-size:4;"></span>'
);
await check(
'abc\t',
- /* HTML */ 'abc<span class="gr-diff tab" style="tab-size:1;"></span>'
+ /* HTML */ 'abc<span class="tab" style="tab-size:1;"></span>'
);
element.tabSize = 8;
await check(
'\t',
- /* HTML */ '<span class="gr-diff tab" style="tab-size:8;"></span>'
+ /* HTML */ '<span class="tab" style="tab-size:8;"></span>'
);
await check(
'abc\t',
- /* HTML */ 'abc<span class="gr-diff tab" style="tab-size:5;"></span>'
+ /* HTML */ 'abc<span class="tab" style="tab-size:5;"></span>'
);
});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-model/gr-diff-model.ts b/polygerrit-ui/app/embed/diff/gr-diff-model/gr-diff-model.ts
index 4a0778b970..88451f6c89 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-model/gr-diff-model.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-model/gr-diff-model.ts
@@ -3,8 +3,8 @@
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {Observable, combineLatest, from} from 'rxjs';
-import {debounceTime, filter, switchMap, withLatestFrom} from 'rxjs/operators';
+import {Observable, combineLatest} from 'rxjs';
+import {debounceTime, filter, map, withLatestFrom} from 'rxjs/operators';
import {
CreateCommentEventDetail,
DiffInfo,
@@ -17,6 +17,7 @@ import {
LineSelectedEventDetail,
RenderPreferences,
Side,
+ SyntaxBlock,
} from '../../../api/diff';
import {define} from '../../../models/dependency';
import {Model} from '../../../models/base/model';
@@ -37,8 +38,8 @@ import {
} from '../gr-diff-processor/gr-diff-processor';
import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
import {assert} from '../../../utils/common-util';
-import {isImageDiff} from '../../../utils/diff-util';
-import {ImageInfo} from '../../../types/common';
+import {countLines, isImageDiff} from '../../../utils/diff-util';
+import {BlameInfo, ImageInfo} from '../../../types/common';
import {fire} from '../../../utils/event-util';
import {CommentRange} from '../../../api/rest-api';
@@ -56,6 +57,7 @@ export interface DiffState {
showFullContext: FullContext;
errorMessage?: string;
layers: DiffLayer[];
+ blameInfo: BlameInfo[];
}
export interface ColumnsToShow {
@@ -86,6 +88,15 @@ export class DiffModel extends Model<DiffState> {
diffState => diffState.diff!
);
+ readonly syntaxTreeRight$: Observable<SyntaxBlock[] | undefined> = select(
+ this.diff$,
+ diff => diff.meta_b?.syntax_tree
+ );
+
+ readonly lineCountLeft$: Observable<number> = select(this.diff$, diff =>
+ countLines(diff, Side.LEFT)
+ );
+
readonly baseImage$: Observable<ImageInfo | undefined> = select(
this.state$,
diffState => diffState.baseImage
@@ -101,6 +112,11 @@ export class DiffModel extends Model<DiffState> {
diffState => diffState.path
);
+ readonly blameInfo$: Observable<BlameInfo[]> = select(
+ this.state$,
+ diffState => diffState.blameInfo
+ );
+
readonly renderPrefs$: Observable<RenderPreferences> = select(
this.state$,
diffState => diffState.renderPrefs
@@ -112,15 +128,14 @@ export class DiffModel extends Model<DiffState> {
);
readonly columnsToShow$: Observable<ColumnsToShow> = select(
- this.renderPrefs$,
- renderPrefs => {
+ combineLatest([this.blameInfo$, this.renderPrefs$]),
+ ([blameInfo, renderPrefs]) => {
const hideLeft = !!renderPrefs.hide_left_side;
const showSign = !!renderPrefs.show_sign_col;
const unified = renderPrefs.view_mode === DiffViewMode.UNIFIED;
return {
- // TODO: Do not always render the blame column. Move this into renderPrefs.
- blame: true,
+ blame: blameInfo.length > 0,
// Hiding the left side in unified diff mode does not make a lot of sense and is not supported.
leftNumber: !hideLeft || unified,
leftSign: !hideLeft && showSign && !unified,
@@ -211,6 +226,7 @@ export class DiffModel extends Model<DiffState> {
groups: [],
showFullContext: FullContext.UNDECIDED,
layers: [],
+ blameInfo: [],
});
this.subscriptions = [this.processDiff()];
}
@@ -220,7 +236,7 @@ export class DiffModel extends Model<DiffState> {
.pipe(
withLatestFrom(this.keyLocations$),
debounceTime(1),
- switchMap(([[diff, context, renderPrefs], keyLocations]) => {
+ map(([[diff, context, renderPrefs], keyLocations]) => {
const options: ProcessingOptions = {
context,
keyLocations,
@@ -229,8 +245,9 @@ export class DiffModel extends Model<DiffState> {
if (renderPrefs?.num_lines_rendered_at_once) {
options.asyncThreshold = renderPrefs.num_lines_rendered_at_once;
}
+
const processor = new GrDiffProcessor(options);
- return from(processor.process(diff.content));
+ return processor.process(diff.content);
})
)
.subscribe(groups => {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
index 5db6db99d0..6b7b45fe2b 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
@@ -11,8 +11,6 @@ import {
} from '../gr-diff/gr-diff-group';
import {DiffContent} from '../../../types/diff';
import {Side} from '../../../constants/constants';
-import {debounce, DelayedTask} from '../../../utils/async-util';
-import {assert} from '../../../utils/common-util';
import {getStringLength} from '../gr-diff-highlight/gr-annotation';
import {GrDiffLineType, LineNumber} from '../../../api/diff';
import {FULL_CONTEXT, KeyLocations} from '../gr-diff/gr-diff-utils';
@@ -31,19 +29,6 @@ interface ChunkEnd {
keyLocation: boolean;
}
-/**
- * The maximum size for an addition or removal chunk before it is broken down
- * into a series of chunks that are this size at most.
- *
- * Note: The value of 120 is chosen so that it is larger than the default
- * asyncThreshold of 64, but feel free to tune this constant to your
- * performance needs.
- */
-function calcMaxGroupSize(asyncThreshold?: number): number {
- if (!asyncThreshold) return 120;
- return asyncThreshold * 2;
-}
-
/** Interface for listening to the output of the processor. */
export interface GroupConsumer {
addGroup(group: GrDiffGroup): void;
@@ -90,114 +75,48 @@ export class GrDiffProcessor {
// visible for testing
keyLocations: KeyLocations;
- private asyncThreshold: number;
-
- private isBinary: boolean;
-
- // visible for testing
- isScrolling?: boolean;
-
- /** Just for making sure that process() is only called once. */
- private isStarted = false;
+ private isBinary = false;
- /** Indicates that processing should be stopped. */
- private isCancelled = false;
-
- private resetIsScrollingTask?: DelayedTask;
-
- private readonly groups: GrDiffGroup[] = [];
+ private groups: GrDiffGroup[] = [];
constructor(options: ProcessingOptions) {
this.context = options.context;
- this.asyncThreshold = options.asyncThreshold ?? 64;
this.keyLocations = options.keyLocations ?? {left: {}, right: {}};
this.isBinary = options.isBinary ?? false;
}
- private readonly handleWindowScroll = () => {
- this.isScrolling = true;
- this.resetIsScrollingTask = debounce(
- this.resetIsScrollingTask,
- () => (this.isScrolling = false),
- 50
- );
- };
-
/**
- * Asynchronously process the diff chunks into groups. As it processes, it
- * will splice groups into the `groups` property of the component.
+ * Process the diff chunks into GrDiffGroups.
*
- * @return A promise that resolves with an
- * array of GrDiffGroups when the diff is completely processed.
+ * @return an array of GrDiffGroups
*/
- async process(chunks: DiffContent[]): Promise<GrDiffGroup[]> {
- assert(this.isStarted === false, 'diff processor cannot be started twice');
-
- window.addEventListener('scroll', this.handleWindowScroll);
-
+ process(chunks: DiffContent[]): GrDiffGroup[] {
+ this.groups = [];
this.groups.push(this.makeGroup('LOST'));
this.groups.push(this.makeGroup('FILE'));
- if (this.isBinary) return this.groups;
- try {
- await this.processChunks(chunks);
- } finally {
- this.finish();
- }
+ this.processChunks(chunks);
return this.groups;
}
- finish() {
- window.removeEventListener('scroll', this.handleWindowScroll);
- }
-
- cancel() {
- this.isCancelled = true;
- this.finish();
- }
-
- async processChunks(chunks: DiffContent[]) {
- let completed = () => {};
- const promise = new Promise<void>(resolve => (completed = resolve));
+ processChunks(chunks: DiffContent[]) {
+ if (this.isBinary) return;
const state = {
lineNums: {left: 0, right: 0},
chunkIndex: 0,
};
-
- chunks = this.splitLargeChunks(chunks);
chunks = this.splitCommonChunksWithKeyLocations(chunks);
- let currentBatch = 0;
- const nextStep = () => {
- if (this.isCancelled || state.chunkIndex >= chunks.length) {
- completed();
- return;
- }
- if (this.isScrolling) {
- window.setTimeout(nextStep, 100);
- return;
- }
-
+ while (state.chunkIndex < chunks.length) {
const stateUpdate = this.processNext(state, chunks);
for (const group of stateUpdate.groups) {
this.groups.push(group);
- currentBatch += group.lines.length;
}
state.lineNums.left += stateUpdate.lineDelta.left;
state.lineNums.right += stateUpdate.lineDelta.right;
-
state.chunkIndex = stateUpdate.newChunkIndex;
- if (currentBatch >= this.asyncThreshold) {
- currentBatch = 0;
- window.setTimeout(nextStep, 1);
- } else {
- nextStep.call(this);
- }
- };
-
- nextStep.call(this);
- await promise;
+ }
}
/**
@@ -448,53 +367,6 @@ export class GrDiffProcessor {
}
/**
- * Split chunks into smaller chunks of the same kind.
- *
- * This is done to prevent doing too much work on the main thread in one
- * uninterrupted rendering step, which would make the browser unresponsive.
- *
- * Note that in the case of unmodified chunks, we only split chunks if the
- * context is set to file (because otherwise they are split up further down
- * the processing into the visible and hidden context), and only split it
- * into 2 chunks, one max sized one and the rest (for reasons that are
- * unclear to me).
- *
- * @param chunks Chunks as returned from the server
- * @return Finer grained chunks.
- */
- // visible for testing
- splitLargeChunks(chunks: DiffContent[]): DiffContent[] {
- const newChunks = [];
-
- for (const chunk of chunks) {
- if (!chunk.ab) {
- for (const subChunk of this.breakdownChunk(chunk)) {
- newChunks.push(subChunk);
- }
- continue;
- }
-
- // If the context is set to "whole file", then break down the shared
- // chunks so they can be rendered incrementally. Note: this is not
- // enabled for any other context preference because manipulating the
- // chunks in this way violates assumptions by the context grouper logic.
- const MAX_GROUP_SIZE = calcMaxGroupSize(this.asyncThreshold);
- if (
- this.context === FULL_CONTEXT &&
- chunk.ab.length > MAX_GROUP_SIZE * 2
- ) {
- // Split large shared chunks in two, where the first is the maximum
- // group size.
- newChunks.push({ab: chunk.ab.slice(0, MAX_GROUP_SIZE)});
- newChunks.push({ab: chunk.ab.slice(MAX_GROUP_SIZE)});
- } else {
- newChunks.push(chunk);
- }
- }
- return newChunks;
- }
-
- /**
* In order to show key locations, such as comments, out of the bounds of
* the selected context, treat them as separate chunks within the model so
* that the content (and context surrounding it) renders correctly.
@@ -675,60 +547,4 @@ export class GrDiffProcessor {
}
return normalized;
}
-
- /**
- * If a group is an addition or a removal, break it down into smaller groups
- * of that type using the MAX_GROUP_SIZE. If the group is a shared chunk
- * or a delta it is returned as the single element of the result array.
- */
- // visible for testing
- breakdownChunk(chunk: DiffContent): DiffContent[] {
- let key: 'a' | 'b' | 'ab' | null = null;
- const {a, b, ab, move_details} = chunk;
- if (a?.length && !b?.length) {
- key = 'a';
- } else if (b?.length && !a?.length) {
- key = 'b';
- } else if (ab?.length) {
- key = 'ab';
- }
-
- // Move chunks should not be divided because of move label
- // positioned in the top of the chunk
- if (!key || move_details) {
- return [chunk];
- }
-
- const MAX_GROUP_SIZE = calcMaxGroupSize(this.asyncThreshold);
- return this.breakdown(chunk[key]!, MAX_GROUP_SIZE).map(subChunkLines => {
- const subChunk: DiffContent = {};
- subChunk[key!] = subChunkLines;
- if (chunk.due_to_rebase) {
- subChunk.due_to_rebase = true;
- }
- if (chunk.move_details) {
- subChunk.move_details = chunk.move_details;
- }
- return subChunk;
- });
- }
-
- /**
- * Given an array and a size, return an array of arrays where no inner array
- * is larger than that size, preserving the original order.
- */
- // visible for testing
- breakdown<T>(array: T[], size: number): T[][] {
- if (!array.length) {
- return [];
- }
- if (array.length < size) {
- return [array];
- }
-
- const head = array.slice(0, array.length - size);
- const tail = array.slice(array.length - size);
-
- return this.breakdown(head, size).concat([tail]);
- }
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts
index 3485fe4d57..f6b9737b70 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts
@@ -578,71 +578,6 @@ suite('gr-diff-processor tests', () => {
]);
});
- test('breaks down shared chunks w/ whole-file', () => {
- const maxGroupSize = 128;
- const size = maxGroupSize * 2 + 5;
- const ab = Array(size)
- .fill(0)
- .map(() => `${Math.random()}`);
- const content = [{ab}];
- processor.context = FULL_CONTEXT;
- const result = processor.splitLargeChunks(content);
- assert.equal(result.length, 2);
- assert.deepEqual(result[0].ab, content[0].ab.slice(0, maxGroupSize));
- assert.deepEqual(result[1].ab, content[0].ab.slice(maxGroupSize));
- });
-
- test('breaks down added chunks', () => {
- const maxGroupSize = 128;
- const size = maxGroupSize * 2 + 5;
- const content = Array(size)
- .fill(0)
- .map(() => `${Math.random()}`);
- processor.context = 5;
- const splitContent = processor
- .splitLargeChunks([{a: [], b: content}])
- .map(r => r.b);
- assert.equal(splitContent.length, 3);
- assert.deepEqual(splitContent[0], content.slice(0, 5));
- assert.deepEqual(splitContent[1], content.slice(5, maxGroupSize + 5));
- assert.deepEqual(splitContent[2], content.slice(maxGroupSize + 5));
- });
-
- test('breaks down removed chunks', () => {
- const maxGroupSize = 128;
- const size = maxGroupSize * 2 + 5;
- const content = Array(size)
- .fill(0)
- .map(() => `${Math.random()}`);
- processor.context = 5;
- const splitContent = processor
- .splitLargeChunks([{a: content, b: []}])
- .map(r => r.a);
- assert.equal(splitContent.length, 3);
- assert.deepEqual(splitContent[0], content.slice(0, 5));
- assert.deepEqual(splitContent[1], content.slice(5, maxGroupSize + 5));
- assert.deepEqual(splitContent[2], content.slice(maxGroupSize + 5));
- });
-
- test('does not break down moved chunks', () => {
- const size = 120 * 2 + 5;
- const content = Array(size)
- .fill(0)
- .map(() => `${Math.random()}`);
- processor.context = 5;
- const splitContent = processor
- .splitLargeChunks([
- {
- a: content,
- b: [],
- move_details: {changed: false, range: {start: 1, end: 1}},
- },
- ])
- .map(r => r.a);
- assert.equal(splitContent.length, 1);
- assert.deepEqual(splitContent[0], content);
- });
-
test('does not break-down common chunks w/ context', () => {
const ab = Array(75)
.fill(0)
@@ -767,15 +702,6 @@ suite('gr-diff-processor tests', () => {
]);
});
- test('isScrolling paused', async () => {
- const content = Array(200).fill({ab: ['', '']});
- processor.isScrolling = true;
- const promise = processor.process(content);
- processor.isScrolling = false;
- const groups = await promise;
- assert.isAtLeast(groups.length, 3);
- });
-
test('image diffs', async () => {
const content = Array(200).fill({ab: ['', '']});
options.isBinary = true;
@@ -1053,61 +979,5 @@ suite('gr-diff-processor tests', () => {
assert.notOk(result[result.length - 1].afterNumber);
});
});
-
- suite('breakdown*', () => {
- test('breakdownChunk breaks down additions', () => {
- const breakdownSpy = sinon.spy(processor, 'breakdown');
- const chunk = {b: ['blah', 'blah', 'blah']};
- const result = processor.breakdownChunk(chunk);
- assert.deepEqual(result, [chunk]);
- assert.isTrue(breakdownSpy.called);
- });
-
- test('breakdownChunk keeps due_to_rebase for broken down additions', () => {
- sinon.spy(processor, 'breakdown');
- const chunk = {b: ['blah', 'blah', 'blah'], due_to_rebase: true};
- const result = processor.breakdownChunk(chunk);
- for (const subResult of result) {
- assert.isTrue(subResult.due_to_rebase);
- }
- });
-
- test('breakdown common case', () => {
- const array = 'Lorem ipsum dolor sit amet, suspendisse inceptos'.split(
- ' '
- );
- const size = 3;
-
- const result = processor.breakdown(array, size);
-
- for (const subResult of result) {
- assert.isAtMost(subResult.length, size);
- }
- const flattened = result.reduce((a, b) => a.concat(b), []);
- assert.deepEqual(flattened, array);
- });
-
- test('breakdown smaller than size', () => {
- const array = 'Lorem ipsum dolor sit amet, suspendisse inceptos'.split(
- ' '
- );
- const size = 10;
- const expected = [array];
-
- const result = processor.breakdown(array, size);
-
- assert.deepEqual(result, expected);
- });
-
- test('breakdown empty', () => {
- const array: string[] = [];
- const size = 10;
- const expected: string[][] = [];
-
- const result = processor.breakdown(array, size);
-
- assert.deepEqual(result, expected);
- });
- });
});
});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element.ts
index b5a79cdc02..e158bd71ce 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element.ts
@@ -14,12 +14,7 @@ import '../gr-ranged-comment-hint/gr-ranged-comment-hint';
import '../gr-diff-builder/gr-diff-builder-image';
import '../gr-diff-builder/gr-diff-section';
import '../gr-diff-builder/gr-diff-row';
-import {
- isResponsive,
- FullContext,
- diffClasses,
- FULL_CONTEXT,
-} from './gr-diff-utils';
+import {isResponsive, FullContext, FULL_CONTEXT} from './gr-diff-utils';
import {ImageInfo} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {
@@ -364,9 +359,9 @@ export class GrDiffElement extends LitElement {
public renderBinaryDiff() {
return html`
- <tbody class="gr-diff binary-diff">
- <tr class="gr-diff">
- <td colspan=${this.columnCount} class="gr-diff">
+ <tbody class="binary-diff">
+ <tr>
+ <td colspan=${this.columnCount}>
<span>Difference in binary files</span>
</td>
</tr>
@@ -394,42 +389,19 @@ export class GrDiffElement extends LitElement {
);
return html`
<colgroup>
- ${when(
- this.columns.blame,
- () => html`<col class=${diffClasses('blame')} />`
- )}
+ ${when(this.columns.blame, () => html`<col class="blame" />`)}
${when(
this.columns.leftNumber,
- () =>
- html`<col
- class=${diffClasses(Side.LEFT)}
- width=${lineNumberWidth}
- />`
- )}
- ${when(
- this.columns.leftSign,
- () => html`<col class=${diffClasses(Side.LEFT, 'sign')} />`
- )}
- ${when(
- this.columns.leftContent,
- () => html`<col class=${diffClasses(Side.LEFT)} />`
+ () => html`<col class="left" width=${lineNumberWidth} />`
)}
+ ${when(this.columns.leftSign, () => html`<col class="left sign" />`)}
+ ${when(this.columns.leftContent, () => html`<col class="left" />`)}
${when(
this.columns.rightNumber,
- () =>
- html`<col
- class=${diffClasses(Side.RIGHT)}
- width=${lineNumberWidth}
- />`
- )}
- ${when(
- this.columns.rightSign,
- () => html`<col class=${diffClasses(Side.RIGHT, 'sign')} />`
- )}
- ${when(
- this.columns.rightContent,
- () => html`<col class=${diffClasses(Side.RIGHT)} />`
+ () => html`<col class="right" width=${lineNumberWidth} />`
)}
+ ${when(this.columns.rightSign, () => html`<col class="right sign" />`)}
+ ${when(this.columns.rightContent, () => html`<col class="right" />`)}
</colgroup>
`;
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element_test.ts
index be6b72e28c..1a437197af 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element_test.ts
@@ -60,11 +60,10 @@ suite('gr-diff-element tests', () => {
<div class="diffContainer sideBySide">
<table id="diffTable">
<colgroup>
- <col class="blame gr-diff" />
- <col class="gr-diff left" width="48" />
- <col class="gr-diff left" />
- <col class="gr-diff right" width="48" />
- <col class="gr-diff right" />
+ <col class="left" width="48" />
+ <col class="left" />
+ <col class="right" width="48" />
+ <col class="right" />
</colgroup>
</table>
</div>
@@ -86,36 +85,31 @@ suite('gr-diff-element tests', () => {
<div class="diffContainer unified">
<table id="diffTable">
<colgroup>
- <col class="blame gr-diff" />
- <col class="gr-diff left" width="48" />
- <col class="gr-diff right" width="48" />
- <col class="gr-diff right" />
+ <col class="left" width="48" />
+ <col class="right" width="48" />
+ <col class="right" />
</colgroup>
- <tbody class="both gr-diff section">
+ <tbody class="both section">
<tr
aria-labelledby="left-button-LOST right-button-LOST right-content-LOST"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="LOST"></td>
- <td class="gr-diff left lineNum" data-value="LOST"></td>
- <td class="gr-diff lineNum right" data-value="LOST"></td>
- <td
- class="both content gr-diff lost no-intraline-info right"
- ></td>
+ <td class="left lineNum" data-value="LOST"></td>
+ <td class="lineNum right" data-value="LOST"></td>
+ <td class="both content lost no-intraline-info right"></td>
</tr>
</tbody>
- <tbody class="both gr-diff section">
+ <tbody class="both section">
<tr
aria-labelledby="left-button-FILE right-button-FILE right-content-FILE"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="FILE"></td>
- <td class="gr-diff left lineNum" data-value="FILE">
+ <td class="left lineNum" data-value="FILE">
<button
aria-label="Add file comment"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="FILE"
id="left-button-FILE"
tabindex="-1"
@@ -123,10 +117,10 @@ suite('gr-diff-element tests', () => {
FILE
</button>
</td>
- <td class="gr-diff lineNum right" data-value="FILE">
+ <td class="lineNum right" data-value="FILE">
<button
aria-label="Add file comment"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="FILE"
id="right-button-FILE"
tabindex="-1"
@@ -134,22 +128,19 @@ suite('gr-diff-element tests', () => {
FILE
</button>
</td>
- <td
- class="both content file gr-diff no-intraline-info right"
- ></td>
+ <td class="both content file no-intraline-info right"></td>
</tr>
</tbody>
- <tbody class="both gr-diff section">
+ <tbody class="both section">
<tr
aria-labelledby="left-button-1 right-button-1 right-content-1"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="1"></td>
- <td class="gr-diff left lineNum" data-value="1">
+ <td class="left lineNum" data-value="1">
<button
aria-label="1 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="1"
id="left-button-1"
tabindex="-1"
@@ -157,10 +148,10 @@ suite('gr-diff-element tests', () => {
1
</button>
</td>
- <td class="gr-diff lineNum right" data-value="1">
+ <td class="lineNum right" data-value="1">
<button
aria-label="1 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="1"
id="right-button-1"
tabindex="-1"
@@ -168,9 +159,9 @@ suite('gr-diff-element tests', () => {
1
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-1"
></div>
@@ -178,14 +169,13 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-2 right-button-2 right-content-2"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="2"></td>
- <td class="gr-diff left lineNum" data-value="2">
+ <td class="left lineNum" data-value="2">
<button
aria-label="2 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="2"
id="left-button-2"
tabindex="-1"
@@ -193,10 +183,10 @@ suite('gr-diff-element tests', () => {
2
</button>
</td>
- <td class="gr-diff lineNum right" data-value="2">
+ <td class="lineNum right" data-value="2">
<button
aria-label="2 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="2"
id="right-button-2"
tabindex="-1"
@@ -204,9 +194,9 @@ suite('gr-diff-element tests', () => {
2
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-2"
></div>
@@ -214,14 +204,13 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-3 right-button-3 right-content-3"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="3"></td>
- <td class="gr-diff left lineNum" data-value="3">
+ <td class="left lineNum" data-value="3">
<button
aria-label="3 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="3"
id="left-button-3"
tabindex="-1"
@@ -229,10 +218,10 @@ suite('gr-diff-element tests', () => {
3
</button>
</td>
- <td class="gr-diff lineNum right" data-value="3">
+ <td class="lineNum right" data-value="3">
<button
aria-label="3 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="3"
id="right-button-3"
tabindex="-1"
@@ -240,9 +229,9 @@ suite('gr-diff-element tests', () => {
3
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-3"
></div>
@@ -250,14 +239,13 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-4 right-button-4 right-content-4"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="4"></td>
- <td class="gr-diff left lineNum" data-value="4">
+ <td class="left lineNum" data-value="4">
<button
aria-label="4 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="4"
id="left-button-4"
tabindex="-1"
@@ -265,10 +253,10 @@ suite('gr-diff-element tests', () => {
4
</button>
</td>
- <td class="gr-diff lineNum right" data-value="4">
+ <td class="lineNum right" data-value="4">
<button
aria-label="4 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="4"
id="right-button-4"
tabindex="-1"
@@ -276,27 +264,26 @@ suite('gr-diff-element tests', () => {
4
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-4"
></div>
</td>
</tr>
</tbody>
- <tbody class="delta gr-diff section total">
+ <tbody class="delta section total">
<tr
aria-labelledby="right-button-5 right-content-5"
- class="add diff-row gr-diff unified"
+ class="add diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="blankLineNum gr-diff left"></td>
- <td class="gr-diff lineNum right" data-value="5">
+ <td class="blankLineNum left"></td>
+ <td class="lineNum right" data-value="5">
<button
aria-label="5 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="5"
id="right-button-5"
tabindex="-1"
@@ -304,9 +291,9 @@ suite('gr-diff-element tests', () => {
5
</button>
</td>
- <td class="add content gr-diff no-intraline-info right">
+ <td class="add content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-5"
></div>
@@ -314,15 +301,14 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="right-button-6 right-content-6"
- class="add diff-row gr-diff unified"
+ class="add diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="blankLineNum gr-diff left"></td>
- <td class="gr-diff lineNum right" data-value="6">
+ <td class="blankLineNum left"></td>
+ <td class="lineNum right" data-value="6">
<button
aria-label="6 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="6"
id="right-button-6"
tabindex="-1"
@@ -330,9 +316,9 @@ suite('gr-diff-element tests', () => {
6
</button>
</td>
- <td class="add content gr-diff no-intraline-info right">
+ <td class="add content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-6"
></div>
@@ -340,15 +326,14 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="right-button-7 right-content-7"
- class="add diff-row gr-diff unified"
+ class="add diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="blankLineNum gr-diff left"></td>
- <td class="gr-diff lineNum right" data-value="7">
+ <td class="blankLineNum left"></td>
+ <td class="lineNum right" data-value="7">
<button
aria-label="7 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="7"
id="right-button-7"
tabindex="-1"
@@ -356,26 +341,25 @@ suite('gr-diff-element tests', () => {
7
</button>
</td>
- <td class="add content gr-diff no-intraline-info right">
+ <td class="add content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-7"
></div>
</td>
</tr>
</tbody>
- <tbody class="both gr-diff section">
+ <tbody class="both section">
<tr
aria-labelledby="left-button-5 right-button-8 right-content-8"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="5"></td>
- <td class="gr-diff left lineNum" data-value="5">
+ <td class="left lineNum" data-value="5">
<button
aria-label="5 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="5"
id="left-button-5"
tabindex="-1"
@@ -383,10 +367,10 @@ suite('gr-diff-element tests', () => {
5
</button>
</td>
- <td class="gr-diff lineNum right" data-value="8">
+ <td class="lineNum right" data-value="8">
<button
aria-label="8 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="8"
id="right-button-8"
tabindex="-1"
@@ -394,9 +378,9 @@ suite('gr-diff-element tests', () => {
8
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-8"
></div>
@@ -404,14 +388,13 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-6 right-button-9 right-content-9"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="6"></td>
- <td class="gr-diff left lineNum" data-value="6">
+ <td class="left lineNum" data-value="6">
<button
aria-label="6 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="6"
id="left-button-6"
tabindex="-1"
@@ -419,10 +402,10 @@ suite('gr-diff-element tests', () => {
6
</button>
</td>
- <td class="gr-diff lineNum right" data-value="9">
+ <td class="lineNum right" data-value="9">
<button
aria-label="9 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="9"
id="right-button-9"
tabindex="-1"
@@ -430,9 +413,9 @@ suite('gr-diff-element tests', () => {
9
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-9"
></div>
@@ -440,14 +423,13 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-7 right-button-10 right-content-10"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="7"></td>
- <td class="gr-diff left lineNum" data-value="7">
+ <td class="left lineNum" data-value="7">
<button
aria-label="7 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="7"
id="left-button-7"
tabindex="-1"
@@ -455,10 +437,10 @@ suite('gr-diff-element tests', () => {
7
</button>
</td>
- <td class="gr-diff lineNum right" data-value="10">
+ <td class="lineNum right" data-value="10">
<button
aria-label="10 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="10"
id="right-button-10"
tabindex="-1"
@@ -466,9 +448,9 @@ suite('gr-diff-element tests', () => {
10
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-10"
></div>
@@ -476,14 +458,13 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-8 right-button-11 right-content-11"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="8"></td>
- <td class="gr-diff left lineNum" data-value="8">
+ <td class="left lineNum" data-value="8">
<button
aria-label="8 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="8"
id="left-button-8"
tabindex="-1"
@@ -491,10 +472,10 @@ suite('gr-diff-element tests', () => {
8
</button>
</td>
- <td class="gr-diff lineNum right" data-value="11">
+ <td class="lineNum right" data-value="11">
<button
aria-label="11 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="11"
id="right-button-11"
tabindex="-1"
@@ -502,9 +483,9 @@ suite('gr-diff-element tests', () => {
11
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-11"
></div>
@@ -512,14 +493,13 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-9 right-button-12 right-content-12"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="9"></td>
- <td class="gr-diff left lineNum" data-value="9">
+ <td class="left lineNum" data-value="9">
<button
aria-label="9 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="9"
id="left-button-9"
tabindex="-1"
@@ -527,10 +507,10 @@ suite('gr-diff-element tests', () => {
9
</button>
</td>
- <td class="gr-diff lineNum right" data-value="12">
+ <td class="lineNum right" data-value="12">
<button
aria-label="12 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="12"
id="right-button-12"
tabindex="-1"
@@ -538,26 +518,25 @@ suite('gr-diff-element tests', () => {
12
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-12"
></div>
</td>
</tr>
</tbody>
- <tbody class="delta gr-diff section total">
+ <tbody class="delta section total">
<tr
aria-labelledby="left-button-10 left-content-10"
- class="diff-row gr-diff remove unified"
+ class="diff-row remove unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="10"></td>
- <td class="gr-diff left lineNum" data-value="10">
+ <td class="left lineNum" data-value="10">
<button
aria-label="10 removed"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="10"
id="left-button-10"
tabindex="-1"
@@ -565,10 +544,10 @@ suite('gr-diff-element tests', () => {
10
</button>
</td>
- <td class="blankLineNum gr-diff right"></td>
- <td class="content gr-diff left no-intraline-info remove">
+ <td class="blankLineNum right"></td>
+ <td class="content left no-intraline-info remove">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-10"
></div>
@@ -576,14 +555,13 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-11 left-content-11"
- class="diff-row gr-diff remove unified"
+ class="diff-row remove unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="11"></td>
- <td class="gr-diff left lineNum" data-value="11">
+ <td class="left lineNum" data-value="11">
<button
aria-label="11 removed"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="11"
id="left-button-11"
tabindex="-1"
@@ -591,10 +569,10 @@ suite('gr-diff-element tests', () => {
11
</button>
</td>
- <td class="blankLineNum gr-diff right"></td>
- <td class="content gr-diff left no-intraline-info remove">
+ <td class="blankLineNum right"></td>
+ <td class="content left no-intraline-info remove">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-11"
></div>
@@ -602,14 +580,13 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-12 left-content-12"
- class="diff-row gr-diff remove unified"
+ class="diff-row remove unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="12"></td>
- <td class="gr-diff left lineNum" data-value="12">
+ <td class="left lineNum" data-value="12">
<button
aria-label="12 removed"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="12"
id="left-button-12"
tabindex="-1"
@@ -617,10 +594,10 @@ suite('gr-diff-element tests', () => {
12
</button>
</td>
- <td class="blankLineNum gr-diff right"></td>
- <td class="content gr-diff left no-intraline-info remove">
+ <td class="blankLineNum right"></td>
+ <td class="content left no-intraline-info remove">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-12"
></div>
@@ -628,14 +605,13 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-13 left-content-13"
- class="diff-row gr-diff remove unified"
+ class="diff-row remove unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="13"></td>
- <td class="gr-diff left lineNum" data-value="13">
+ <td class="left lineNum" data-value="13">
<button
aria-label="13 removed"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="13"
id="left-button-13"
tabindex="-1"
@@ -643,28 +619,27 @@ suite('gr-diff-element tests', () => {
13
</button>
</td>
- <td class="blankLineNum gr-diff right"></td>
- <td class="content gr-diff left no-intraline-info remove">
+ <td class="blankLineNum right"></td>
+ <td class="content left no-intraline-info remove">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-13"
></div>
</td>
</tr>
</tbody>
- <tbody class="delta gr-diff ignoredWhitespaceOnly section">
+ <tbody class="delta ignoredWhitespaceOnly section">
<tr
aria-labelledby="right-button-13 right-content-13"
- class="add diff-row gr-diff unified"
+ class="add diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="blankLineNum gr-diff left"></td>
- <td class="gr-diff lineNum right" data-value="13">
+ <td class="blankLineNum left"></td>
+ <td class="lineNum right" data-value="13">
<button
aria-label="13 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="13"
id="right-button-13"
tabindex="-1"
@@ -672,9 +647,9 @@ suite('gr-diff-element tests', () => {
13
</button>
</td>
- <td class="add content gr-diff no-intraline-info right">
+ <td class="add content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-13"
></div>
@@ -682,15 +657,14 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="right-button-14 right-content-14"
- class="add diff-row gr-diff unified"
+ class="add diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="blankLineNum gr-diff left"></td>
- <td class="gr-diff lineNum right" data-value="14">
+ <td class="blankLineNum left"></td>
+ <td class="lineNum right" data-value="14">
<button
aria-label="14 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="14"
id="right-button-14"
tabindex="-1"
@@ -698,26 +672,25 @@ suite('gr-diff-element tests', () => {
14
</button>
</td>
- <td class="add content gr-diff no-intraline-info right">
+ <td class="add content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-14"
></div>
</td>
</tr>
</tbody>
- <tbody class="delta gr-diff section">
+ <tbody class="delta section">
<tr
aria-labelledby="left-button-16 left-content-16"
- class="diff-row gr-diff remove unified"
+ class="diff-row remove unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="16"></td>
- <td class="gr-diff left lineNum" data-value="16">
+ <td class="left lineNum" data-value="16">
<button
aria-label="16 removed"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="16"
id="left-button-16"
tabindex="-1"
@@ -725,10 +698,10 @@ suite('gr-diff-element tests', () => {
16
</button>
</td>
- <td class="blankLineNum gr-diff right"></td>
- <td class="content gr-diff left remove">
+ <td class="blankLineNum right"></td>
+ <td class="content left remove">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-16"
></div>
@@ -736,15 +709,14 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="right-button-15 right-content-15"
- class="add diff-row gr-diff unified"
+ class="add diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="blankLineNum gr-diff left"></td>
- <td class="gr-diff lineNum right" data-value="15">
+ <td class="blankLineNum left"></td>
+ <td class="lineNum right" data-value="15">
<button
aria-label="15 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="15"
id="right-button-15"
tabindex="-1"
@@ -752,26 +724,25 @@ suite('gr-diff-element tests', () => {
15
</button>
</td>
- <td class="add content gr-diff right">
+ <td class="add content right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-15"
></div>
</td>
</tr>
</tbody>
- <tbody class="both gr-diff section">
+ <tbody class="both section">
<tr
aria-labelledby="left-button-17 right-button-16 right-content-16"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="17"></td>
- <td class="gr-diff left lineNum" data-value="17">
+ <td class="left lineNum" data-value="17">
<button
aria-label="17 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="17"
id="left-button-17"
tabindex="-1"
@@ -779,10 +750,10 @@ suite('gr-diff-element tests', () => {
17
</button>
</td>
- <td class="gr-diff lineNum right" data-value="16">
+ <td class="lineNum right" data-value="16">
<button
aria-label="16 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="16"
id="right-button-16"
tabindex="-1"
@@ -790,9 +761,9 @@ suite('gr-diff-element tests', () => {
16
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-16"
></div>
@@ -800,14 +771,13 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-18 right-button-17 right-content-17"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="18"></td>
- <td class="gr-diff left lineNum" data-value="18">
+ <td class="left lineNum" data-value="18">
<button
aria-label="18 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="18"
id="left-button-18"
tabindex="-1"
@@ -815,10 +785,10 @@ suite('gr-diff-element tests', () => {
18
</button>
</td>
- <td class="gr-diff lineNum right" data-value="17">
+ <td class="lineNum right" data-value="17">
<button
aria-label="17 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="17"
id="right-button-17"
tabindex="-1"
@@ -826,9 +796,9 @@ suite('gr-diff-element tests', () => {
17
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-17"
></div>
@@ -836,14 +806,13 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-19 right-button-18 right-content-18"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="19"></td>
- <td class="gr-diff left lineNum" data-value="19">
+ <td class="left lineNum" data-value="19">
<button
aria-label="19 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="19"
id="left-button-19"
tabindex="-1"
@@ -851,10 +820,10 @@ suite('gr-diff-element tests', () => {
19
</button>
</td>
- <td class="gr-diff lineNum right" data-value="18">
+ <td class="lineNum right" data-value="18">
<button
aria-label="18 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="18"
id="right-button-18"
tabindex="-1"
@@ -862,47 +831,43 @@ suite('gr-diff-element tests', () => {
18
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-18"
></div>
</td>
</tr>
</tbody>
- <tbody class="contextControl gr-diff section">
- <tr class="above contextBackground gr-diff unified">
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="contextLineNum gr-diff"></td>
- <td class="contextLineNum gr-diff"></td>
- <td class="gr-diff"></td>
- </tr>
- <tr class="dividerRow gr-diff show-both">
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="dividerCell gr-diff" colspan="3">
- <gr-context-controls class="gr-diff" showconfig="both">
+ <tbody class="contextControl section">
+ <tr class="above contextBackground unified">
+ <td class="contextLineNum"></td>
+ <td class="contextLineNum"></td>
+ <td></td>
+ </tr>
+ <tr class="dividerRow show-both">
+ <td class="dividerCell" colspan="3">
+ <gr-context-controls showconfig="both">
</gr-context-controls>
</td>
</tr>
- <tr class="below contextBackground gr-diff unified">
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="contextLineNum gr-diff"></td>
- <td class="contextLineNum gr-diff"></td>
- <td class="gr-diff"></td>
+ <tr class="below contextBackground unified">
+ <td class="contextLineNum"></td>
+ <td class="contextLineNum"></td>
+ <td></td>
</tr>
</tbody>
- <tbody class="both gr-diff section">
+ <tbody class="both section">
<tr
aria-labelledby="left-button-38 right-button-37 right-content-37"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="38"></td>
- <td class="gr-diff left lineNum" data-value="38">
+ <td class="left lineNum" data-value="38">
<button
aria-label="38 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="38"
id="left-button-38"
tabindex="-1"
@@ -910,10 +875,10 @@ suite('gr-diff-element tests', () => {
38
</button>
</td>
- <td class="gr-diff lineNum right" data-value="37">
+ <td class="lineNum right" data-value="37">
<button
aria-label="37 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="37"
id="right-button-37"
tabindex="-1"
@@ -921,9 +886,9 @@ suite('gr-diff-element tests', () => {
37
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-37"
></div>
@@ -931,14 +896,13 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-39 right-button-38 right-content-38"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="39"></td>
- <td class="gr-diff left lineNum" data-value="39">
+ <td class="left lineNum" data-value="39">
<button
aria-label="39 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="39"
id="left-button-39"
tabindex="-1"
@@ -946,10 +910,10 @@ suite('gr-diff-element tests', () => {
39
</button>
</td>
- <td class="gr-diff lineNum right" data-value="38">
+ <td class="lineNum right" data-value="38">
<button
aria-label="38 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="38"
id="right-button-38"
tabindex="-1"
@@ -957,9 +921,9 @@ suite('gr-diff-element tests', () => {
38
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-38"
></div>
@@ -967,14 +931,13 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-40 right-button-39 right-content-39"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="40"></td>
- <td class="gr-diff left lineNum" data-value="40">
+ <td class="left lineNum" data-value="40">
<button
aria-label="40 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="40"
id="left-button-40"
tabindex="-1"
@@ -982,10 +945,10 @@ suite('gr-diff-element tests', () => {
40
</button>
</td>
- <td class="gr-diff lineNum right" data-value="39">
+ <td class="lineNum right" data-value="39">
<button
aria-label="39 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="39"
id="right-button-39"
tabindex="-1"
@@ -993,27 +956,26 @@ suite('gr-diff-element tests', () => {
39
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-39"
></div>
</td>
</tr>
</tbody>
- <tbody class="delta gr-diff section total">
+ <tbody class="delta section total">
<tr
aria-labelledby="right-button-40 right-content-40"
- class="add diff-row gr-diff unified"
+ class="add diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="blankLineNum gr-diff left"></td>
- <td class="gr-diff lineNum right" data-value="40">
+ <td class="blankLineNum left"></td>
+ <td class="lineNum right" data-value="40">
<button
aria-label="40 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="40"
id="right-button-40"
tabindex="-1"
@@ -1021,9 +983,9 @@ suite('gr-diff-element tests', () => {
40
</button>
</td>
- <td class="add content gr-diff no-intraline-info right">
+ <td class="add content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-40"
></div>
@@ -1031,15 +993,14 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="right-button-41 right-content-41"
- class="add diff-row gr-diff unified"
+ class="add diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="blankLineNum gr-diff left"></td>
- <td class="gr-diff lineNum right" data-value="41">
+ <td class="blankLineNum left"></td>
+ <td class="lineNum right" data-value="41">
<button
aria-label="41 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="41"
id="right-button-41"
tabindex="-1"
@@ -1047,9 +1008,9 @@ suite('gr-diff-element tests', () => {
41
</button>
</td>
- <td class="add content gr-diff no-intraline-info right">
+ <td class="add content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-41"
></div>
@@ -1057,15 +1018,14 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="right-button-42 right-content-42"
- class="add diff-row gr-diff unified"
+ class="add diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="blankLineNum gr-diff left"></td>
- <td class="gr-diff lineNum right" data-value="42">
+ <td class="blankLineNum left"></td>
+ <td class="lineNum right" data-value="42">
<button
aria-label="42 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="42"
id="right-button-42"
tabindex="-1"
@@ -1073,9 +1033,9 @@ suite('gr-diff-element tests', () => {
42
</button>
</td>
- <td class="add content gr-diff no-intraline-info right">
+ <td class="add content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-42"
></div>
@@ -1083,15 +1043,14 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="right-button-43 right-content-43"
- class="add diff-row gr-diff unified"
+ class="add diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="blankLineNum gr-diff left"></td>
- <td class="gr-diff lineNum right" data-value="43">
+ <td class="blankLineNum left"></td>
+ <td class="lineNum right" data-value="43">
<button
aria-label="43 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="43"
id="right-button-43"
tabindex="-1"
@@ -1099,26 +1058,25 @@ suite('gr-diff-element tests', () => {
43
</button>
</td>
- <td class="add content gr-diff no-intraline-info right">
+ <td class="add content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-43"
></div>
</td>
</tr>
</tbody>
- <tbody class="both gr-diff section">
+ <tbody class="both section">
<tr
aria-labelledby="left-button-41 right-button-44 right-content-44"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="41"></td>
- <td class="gr-diff left lineNum" data-value="41">
+ <td class="left lineNum" data-value="41">
<button
aria-label="41 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="41"
id="left-button-41"
tabindex="-1"
@@ -1126,10 +1084,10 @@ suite('gr-diff-element tests', () => {
41
</button>
</td>
- <td class="gr-diff lineNum right" data-value="44">
+ <td class="lineNum right" data-value="44">
<button
aria-label="44 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="44"
id="right-button-44"
tabindex="-1"
@@ -1137,9 +1095,9 @@ suite('gr-diff-element tests', () => {
44
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-44"
></div>
@@ -1147,14 +1105,13 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-42 right-button-45 right-content-45"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="42"></td>
- <td class="gr-diff left lineNum" data-value="42">
+ <td class="left lineNum" data-value="42">
<button
aria-label="42 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="42"
id="left-button-42"
tabindex="-1"
@@ -1162,10 +1119,10 @@ suite('gr-diff-element tests', () => {
42
</button>
</td>
- <td class="gr-diff lineNum right" data-value="45">
+ <td class="lineNum right" data-value="45">
<button
aria-label="45 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="45"
id="right-button-45"
tabindex="-1"
@@ -1173,9 +1130,9 @@ suite('gr-diff-element tests', () => {
45
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-45"
></div>
@@ -1183,14 +1140,13 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-43 right-button-46 right-content-46"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="43"></td>
- <td class="gr-diff left lineNum" data-value="43">
+ <td class="left lineNum" data-value="43">
<button
aria-label="43 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="43"
id="left-button-43"
tabindex="-1"
@@ -1198,10 +1154,10 @@ suite('gr-diff-element tests', () => {
43
</button>
</td>
- <td class="gr-diff lineNum right" data-value="46">
+ <td class="lineNum right" data-value="46">
<button
aria-label="46 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="46"
id="right-button-46"
tabindex="-1"
@@ -1209,9 +1165,9 @@ suite('gr-diff-element tests', () => {
46
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-46"
></div>
@@ -1219,14 +1175,13 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-44 right-button-47 right-content-47"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="44"></td>
- <td class="gr-diff left lineNum" data-value="44">
+ <td class="left lineNum" data-value="44">
<button
aria-label="44 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="44"
id="left-button-44"
tabindex="-1"
@@ -1234,10 +1189,10 @@ suite('gr-diff-element tests', () => {
44
</button>
</td>
- <td class="gr-diff lineNum right" data-value="47">
+ <td class="lineNum right" data-value="47">
<button
aria-label="47 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="47"
id="right-button-47"
tabindex="-1"
@@ -1245,9 +1200,9 @@ suite('gr-diff-element tests', () => {
47
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-47"
></div>
@@ -1255,14 +1210,13 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-45 right-button-48 right-content-48"
- class="both diff-row gr-diff unified"
+ class="both diff-row unified"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="45"></td>
- <td class="gr-diff left lineNum" data-value="45">
+ <td class="left lineNum" data-value="45">
<button
aria-label="45 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="45"
id="left-button-45"
tabindex="-1"
@@ -1270,10 +1224,10 @@ suite('gr-diff-element tests', () => {
45
</button>
</td>
- <td class="gr-diff lineNum right" data-value="48">
+ <td class="lineNum right" data-value="48">
<button
aria-label="48 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="48"
id="right-button-48"
tabindex="-1"
@@ -1281,9 +1235,9 @@ suite('gr-diff-element tests', () => {
48
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-48"
></div>
@@ -1299,7 +1253,6 @@ suite('gr-diff-element tests', () => {
'gr-diff-section',
'gr-diff-row',
'gr-diff-text',
- 'gr-legacy-text',
'slot',
],
}
@@ -1320,44 +1273,37 @@ suite('gr-diff-element tests', () => {
<div class="diffContainer sideBySide">
<table id="diffTable">
<colgroup>
- <col class="blame gr-diff" />
- <col class="gr-diff left" width="48" />
- <col class="gr-diff left" />
- <col class="gr-diff right" width="48" />
- <col class="gr-diff right" />
+ <col class="left" width="48" />
+ <col class="left" />
+ <col class="right" width="48" />
+ <col class="right" />
</colgroup>
- <tbody class="both gr-diff section">
+ <tbody class="both section">
<tr
aria-labelledby="left-button-LOST left-content-LOST right-button-LOST right-content-LOST"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="LOST"></td>
- <td class="gr-diff left lineNum" data-value="LOST"></td>
- <td
- class="both content gr-diff left lost no-intraline-info"
- ></td>
- <td class="gr-diff lineNum right" data-value="LOST"></td>
- <td
- class="both content gr-diff lost no-intraline-info right"
- ></td>
+ <td class="left lineNum" data-value="LOST"></td>
+ <td class="both content left lost no-intraline-info"></td>
+ <td class="lineNum right" data-value="LOST"></td>
+ <td class="both content lost no-intraline-info right"></td>
</tr>
</tbody>
- <tbody class="both gr-diff section">
+ <tbody class="both section">
<tr
aria-labelledby="left-button-FILE left-content-FILE right-button-FILE right-content-FILE"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="FILE"></td>
- <td class="gr-diff left lineNum" data-value="FILE">
+ <td class="left lineNum" data-value="FILE">
<button
aria-label="Add file comment"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="FILE"
id="left-button-FILE"
tabindex="-1"
@@ -1365,13 +1311,11 @@ suite('gr-diff-element tests', () => {
FILE
</button>
</td>
- <td
- class="both content file gr-diff left no-intraline-info"
- ></td>
- <td class="gr-diff lineNum right" data-value="FILE">
+ <td class="both content file left no-intraline-info"></td>
+ <td class="lineNum right" data-value="FILE">
<button
aria-label="Add file comment"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="FILE"
id="right-button-FILE"
tabindex="-1"
@@ -1379,24 +1323,21 @@ suite('gr-diff-element tests', () => {
FILE
</button>
</td>
- <td
- class="both content file gr-diff no-intraline-info right"
- ></td>
+ <td class="both content file no-intraline-info right"></td>
</tr>
</tbody>
- <tbody class="both gr-diff section">
+ <tbody class="both section">
<tr
aria-labelledby="left-button-1 left-content-1 right-button-1 right-content-1"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="1"></td>
- <td class="gr-diff left lineNum" data-value="1">
+ <td class="left lineNum" data-value="1">
<button
aria-label="1 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="1"
id="left-button-1"
tabindex="-1"
@@ -1404,17 +1345,17 @@ suite('gr-diff-element tests', () => {
1
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-1"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="1">
+ <td class="lineNum right" data-value="1">
<button
aria-label="1 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="1"
id="right-button-1"
tabindex="-1"
@@ -1422,9 +1363,9 @@ suite('gr-diff-element tests', () => {
1
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-1"
></div>
@@ -1432,16 +1373,15 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-2 left-content-2 right-button-2 right-content-2"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="2"></td>
- <td class="gr-diff left lineNum" data-value="2">
+ <td class="left lineNum" data-value="2">
<button
aria-label="2 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="2"
id="left-button-2"
tabindex="-1"
@@ -1449,17 +1389,17 @@ suite('gr-diff-element tests', () => {
2
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-2"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="2">
+ <td class="lineNum right" data-value="2">
<button
aria-label="2 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="2"
id="right-button-2"
tabindex="-1"
@@ -1467,9 +1407,9 @@ suite('gr-diff-element tests', () => {
2
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-2"
></div>
@@ -1477,16 +1417,15 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-3 left-content-3 right-button-3 right-content-3"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="3"></td>
- <td class="gr-diff left lineNum" data-value="3">
+ <td class="left lineNum" data-value="3">
<button
aria-label="3 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="3"
id="left-button-3"
tabindex="-1"
@@ -1494,17 +1433,17 @@ suite('gr-diff-element tests', () => {
3
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-3"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="3">
+ <td class="lineNum right" data-value="3">
<button
aria-label="3 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="3"
id="right-button-3"
tabindex="-1"
@@ -1512,9 +1451,9 @@ suite('gr-diff-element tests', () => {
3
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-3"
></div>
@@ -1522,16 +1461,15 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-4 left-content-4 right-button-4 right-content-4"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="4"></td>
- <td class="gr-diff left lineNum" data-value="4">
+ <td class="left lineNum" data-value="4">
<button
aria-label="4 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="4"
id="left-button-4"
tabindex="-1"
@@ -1539,17 +1477,17 @@ suite('gr-diff-element tests', () => {
4
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-4"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="4">
+ <td class="lineNum right" data-value="4">
<button
aria-label="4 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="4"
id="right-button-4"
tabindex="-1"
@@ -1557,32 +1495,31 @@ suite('gr-diff-element tests', () => {
4
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-4"
></div>
</td>
</tr>
</tbody>
- <tbody class="delta gr-diff section total">
+ <tbody class="delta section total">
<tr
aria-labelledby="right-button-5 right-content-5"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="blank"
right-type="add"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="blankLineNum gr-diff left"></td>
- <td class="blank gr-diff left no-intraline-info">
- <div class="contentText gr-diff" data-side="left"></div>
+ <td class="blankLineNum left"></td>
+ <td class="blank left no-intraline-info">
+ <div class="contentText" data-side="left"></div>
</td>
- <td class="gr-diff lineNum right" data-value="5">
+ <td class="lineNum right" data-value="5">
<button
aria-label="5 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="5"
id="right-button-5"
tabindex="-1"
@@ -1590,9 +1527,9 @@ suite('gr-diff-element tests', () => {
5
</button>
</td>
- <td class="add content gr-diff no-intraline-info right">
+ <td class="add content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-5"
></div>
@@ -1600,20 +1537,19 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="right-button-6 right-content-6"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="blank"
right-type="add"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="blankLineNum gr-diff left"></td>
- <td class="blank gr-diff left no-intraline-info">
- <div class="contentText gr-diff" data-side="left"></div>
+ <td class="blankLineNum left"></td>
+ <td class="blank left no-intraline-info">
+ <div class="contentText" data-side="left"></div>
</td>
- <td class="gr-diff lineNum right" data-value="6">
+ <td class="lineNum right" data-value="6">
<button
aria-label="6 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="6"
id="right-button-6"
tabindex="-1"
@@ -1621,9 +1557,9 @@ suite('gr-diff-element tests', () => {
6
</button>
</td>
- <td class="add content gr-diff no-intraline-info right">
+ <td class="add content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-6"
></div>
@@ -1631,20 +1567,19 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="right-button-7 right-content-7"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="blank"
right-type="add"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="blankLineNum gr-diff left"></td>
- <td class="blank gr-diff left no-intraline-info">
- <div class="contentText gr-diff" data-side="left"></div>
+ <td class="blankLineNum left"></td>
+ <td class="blank left no-intraline-info">
+ <div class="contentText" data-side="left"></div>
</td>
- <td class="gr-diff lineNum right" data-value="7">
+ <td class="lineNum right" data-value="7">
<button
aria-label="7 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="7"
id="right-button-7"
tabindex="-1"
@@ -1652,28 +1587,27 @@ suite('gr-diff-element tests', () => {
7
</button>
</td>
- <td class="add content gr-diff no-intraline-info right">
+ <td class="add content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-7"
></div>
</td>
</tr>
</tbody>
- <tbody class="both gr-diff section">
+ <tbody class="both section">
<tr
aria-labelledby="left-button-5 left-content-5 right-button-8 right-content-8"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="5"></td>
- <td class="gr-diff left lineNum" data-value="5">
+ <td class="left lineNum" data-value="5">
<button
aria-label="5 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="5"
id="left-button-5"
tabindex="-1"
@@ -1681,17 +1615,17 @@ suite('gr-diff-element tests', () => {
5
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-5"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="8">
+ <td class="lineNum right" data-value="8">
<button
aria-label="8 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="8"
id="right-button-8"
tabindex="-1"
@@ -1699,9 +1633,9 @@ suite('gr-diff-element tests', () => {
8
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-8"
></div>
@@ -1709,16 +1643,15 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-6 left-content-6 right-button-9 right-content-9"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="6"></td>
- <td class="gr-diff left lineNum" data-value="6">
+ <td class="left lineNum" data-value="6">
<button
aria-label="6 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="6"
id="left-button-6"
tabindex="-1"
@@ -1726,17 +1659,17 @@ suite('gr-diff-element tests', () => {
6
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-6"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="9">
+ <td class="lineNum right" data-value="9">
<button
aria-label="9 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="9"
id="right-button-9"
tabindex="-1"
@@ -1744,9 +1677,9 @@ suite('gr-diff-element tests', () => {
9
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-9"
></div>
@@ -1754,16 +1687,15 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-7 left-content-7 right-button-10 right-content-10"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="7"></td>
- <td class="gr-diff left lineNum" data-value="7">
+ <td class="left lineNum" data-value="7">
<button
aria-label="7 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="7"
id="left-button-7"
tabindex="-1"
@@ -1771,17 +1703,17 @@ suite('gr-diff-element tests', () => {
7
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-7"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="10">
+ <td class="lineNum right" data-value="10">
<button
aria-label="10 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="10"
id="right-button-10"
tabindex="-1"
@@ -1789,9 +1721,9 @@ suite('gr-diff-element tests', () => {
10
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-10"
></div>
@@ -1799,16 +1731,15 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-8 left-content-8 right-button-11 right-content-11"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="8"></td>
- <td class="gr-diff left lineNum" data-value="8">
+ <td class="left lineNum" data-value="8">
<button
aria-label="8 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="8"
id="left-button-8"
tabindex="-1"
@@ -1816,17 +1747,17 @@ suite('gr-diff-element tests', () => {
8
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-8"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="11">
+ <td class="lineNum right" data-value="11">
<button
aria-label="11 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="11"
id="right-button-11"
tabindex="-1"
@@ -1834,9 +1765,9 @@ suite('gr-diff-element tests', () => {
11
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-11"
></div>
@@ -1844,16 +1775,15 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-9 left-content-9 right-button-12 right-content-12"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="9"></td>
- <td class="gr-diff left lineNum" data-value="9">
+ <td class="left lineNum" data-value="9">
<button
aria-label="9 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="9"
id="left-button-9"
tabindex="-1"
@@ -1861,17 +1791,17 @@ suite('gr-diff-element tests', () => {
9
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-9"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="12">
+ <td class="lineNum right" data-value="12">
<button
aria-label="12 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="12"
id="right-button-12"
tabindex="-1"
@@ -1879,28 +1809,27 @@ suite('gr-diff-element tests', () => {
12
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-12"
></div>
</td>
</tr>
</tbody>
- <tbody class="delta gr-diff section total">
+ <tbody class="delta section total">
<tr
aria-labelledby="left-button-10 left-content-10"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="remove"
right-type="blank"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="10"></td>
- <td class="gr-diff left lineNum" data-value="10">
+ <td class="left lineNum" data-value="10">
<button
aria-label="10 removed"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="10"
id="left-button-10"
tabindex="-1"
@@ -1908,30 +1837,29 @@ suite('gr-diff-element tests', () => {
10
</button>
</td>
- <td class="content gr-diff left no-intraline-info remove">
+ <td class="content left no-intraline-info remove">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-10"
></div>
</td>
- <td class="blankLineNum gr-diff right"></td>
- <td class="blank gr-diff no-intraline-info right">
- <div class="contentText gr-diff" data-side="right"></div>
+ <td class="blankLineNum right"></td>
+ <td class="blank no-intraline-info right">
+ <div class="contentText" data-side="right"></div>
</td>
</tr>
<tr
aria-labelledby="left-button-11 left-content-11"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="remove"
right-type="blank"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="11"></td>
- <td class="gr-diff left lineNum" data-value="11">
+ <td class="left lineNum" data-value="11">
<button
aria-label="11 removed"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="11"
id="left-button-11"
tabindex="-1"
@@ -1939,30 +1867,29 @@ suite('gr-diff-element tests', () => {
11
</button>
</td>
- <td class="content gr-diff left no-intraline-info remove">
+ <td class="content left no-intraline-info remove">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-11"
></div>
</td>
- <td class="blankLineNum gr-diff right"></td>
- <td class="blank gr-diff no-intraline-info right">
- <div class="contentText gr-diff" data-side="right"></div>
+ <td class="blankLineNum right"></td>
+ <td class="blank no-intraline-info right">
+ <div class="contentText" data-side="right"></div>
</td>
</tr>
<tr
aria-labelledby="left-button-12 left-content-12"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="remove"
right-type="blank"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="12"></td>
- <td class="gr-diff left lineNum" data-value="12">
+ <td class="left lineNum" data-value="12">
<button
aria-label="12 removed"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="12"
id="left-button-12"
tabindex="-1"
@@ -1970,30 +1897,29 @@ suite('gr-diff-element tests', () => {
12
</button>
</td>
- <td class="content gr-diff left no-intraline-info remove">
+ <td class="content left no-intraline-info remove">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-12"
></div>
</td>
- <td class="blankLineNum gr-diff right"></td>
- <td class="blank gr-diff no-intraline-info right">
- <div class="contentText gr-diff" data-side="right"></div>
+ <td class="blankLineNum right"></td>
+ <td class="blank no-intraline-info right">
+ <div class="contentText" data-side="right"></div>
</td>
</tr>
<tr
aria-labelledby="left-button-13 left-content-13"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="remove"
right-type="blank"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="13"></td>
- <td class="gr-diff left lineNum" data-value="13">
+ <td class="left lineNum" data-value="13">
<button
aria-label="13 removed"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="13"
id="left-button-13"
tabindex="-1"
@@ -2001,32 +1927,31 @@ suite('gr-diff-element tests', () => {
13
</button>
</td>
- <td class="content gr-diff left no-intraline-info remove">
+ <td class="content left no-intraline-info remove">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-13"
></div>
</td>
- <td class="blankLineNum gr-diff right"></td>
- <td class="blank gr-diff no-intraline-info right">
- <div class="contentText gr-diff" data-side="right"></div>
+ <td class="blankLineNum right"></td>
+ <td class="blank no-intraline-info right">
+ <div class="contentText" data-side="right"></div>
</td>
</tr>
</tbody>
- <tbody class="delta gr-diff ignoredWhitespaceOnly section">
+ <tbody class="delta ignoredWhitespaceOnly section">
<tr
aria-labelledby="left-button-14 left-content-14 right-button-13 right-content-13"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="remove"
right-type="add"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="14"></td>
- <td class="gr-diff left lineNum" data-value="14">
+ <td class="left lineNum" data-value="14">
<button
aria-label="14 removed"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="14"
id="left-button-14"
tabindex="-1"
@@ -2034,17 +1959,17 @@ suite('gr-diff-element tests', () => {
14
</button>
</td>
- <td class="content gr-diff left no-intraline-info remove">
+ <td class="content left no-intraline-info remove">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-14"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="13">
+ <td class="lineNum right" data-value="13">
<button
aria-label="13 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="13"
id="right-button-13"
tabindex="-1"
@@ -2052,9 +1977,9 @@ suite('gr-diff-element tests', () => {
13
</button>
</td>
- <td class="add content gr-diff no-intraline-info right">
+ <td class="add content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-13"
></div>
@@ -2062,16 +1987,15 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-15 left-content-15 right-button-14 right-content-14"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="remove"
right-type="add"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="15"></td>
- <td class="gr-diff left lineNum" data-value="15">
+ <td class="left lineNum" data-value="15">
<button
aria-label="15 removed"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="15"
id="left-button-15"
tabindex="-1"
@@ -2079,17 +2003,17 @@ suite('gr-diff-element tests', () => {
15
</button>
</td>
- <td class="content gr-diff left no-intraline-info remove">
+ <td class="content left no-intraline-info remove">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-15"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="14">
+ <td class="lineNum right" data-value="14">
<button
aria-label="14 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="14"
id="right-button-14"
tabindex="-1"
@@ -2097,28 +2021,27 @@ suite('gr-diff-element tests', () => {
14
</button>
</td>
- <td class="add content gr-diff no-intraline-info right">
+ <td class="add content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-14"
></div>
</td>
</tr>
</tbody>
- <tbody class="delta gr-diff section">
+ <tbody class="delta section">
<tr
aria-labelledby="left-button-16 left-content-16 right-button-15 right-content-15"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="remove"
right-type="add"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="16"></td>
- <td class="gr-diff left lineNum" data-value="16">
+ <td class="left lineNum" data-value="16">
<button
aria-label="16 removed"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="16"
id="left-button-16"
tabindex="-1"
@@ -2126,17 +2049,17 @@ suite('gr-diff-element tests', () => {
16
</button>
</td>
- <td class="content gr-diff left remove">
+ <td class="content left remove">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-16"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="15">
+ <td class="lineNum right" data-value="15">
<button
aria-label="15 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="15"
id="right-button-15"
tabindex="-1"
@@ -2144,28 +2067,27 @@ suite('gr-diff-element tests', () => {
15
</button>
</td>
- <td class="add content gr-diff right">
+ <td class="add content right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-15"
></div>
</td>
</tr>
</tbody>
- <tbody class="both gr-diff section">
+ <tbody class="both section">
<tr
aria-labelledby="left-button-17 left-content-17 right-button-16 right-content-16"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="17"></td>
- <td class="gr-diff left lineNum" data-value="17">
+ <td class="left lineNum" data-value="17">
<button
aria-label="17 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="17"
id="left-button-17"
tabindex="-1"
@@ -2173,17 +2095,17 @@ suite('gr-diff-element tests', () => {
17
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-17"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="16">
+ <td class="lineNum right" data-value="16">
<button
aria-label="16 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="16"
id="right-button-16"
tabindex="-1"
@@ -2191,9 +2113,9 @@ suite('gr-diff-element tests', () => {
16
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-16"
></div>
@@ -2201,16 +2123,15 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-18 left-content-18 right-button-17 right-content-17"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="18"></td>
- <td class="gr-diff left lineNum" data-value="18">
+ <td class="left lineNum" data-value="18">
<button
aria-label="18 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="18"
id="left-button-18"
tabindex="-1"
@@ -2218,17 +2139,17 @@ suite('gr-diff-element tests', () => {
18
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-18"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="17">
+ <td class="lineNum right" data-value="17">
<button
aria-label="17 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="17"
id="right-button-17"
tabindex="-1"
@@ -2236,9 +2157,9 @@ suite('gr-diff-element tests', () => {
17
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-17"
></div>
@@ -2246,16 +2167,15 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-19 left-content-19 right-button-18 right-content-18"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="19"></td>
- <td class="gr-diff left lineNum" data-value="19">
+ <td class="left lineNum" data-value="19">
<button
aria-label="19 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="19"
id="left-button-19"
tabindex="-1"
@@ -2263,17 +2183,17 @@ suite('gr-diff-element tests', () => {
19
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-19"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="18">
+ <td class="lineNum right" data-value="18">
<button
aria-label="18 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="18"
id="right-button-18"
tabindex="-1"
@@ -2281,61 +2201,56 @@ suite('gr-diff-element tests', () => {
18
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-18"
></div>
</td>
</tr>
</tbody>
- <tbody class="contextControl gr-diff section">
+ <tbody class="contextControl section">
<tr
- class="above contextBackground gr-diff side-by-side"
+ class="above contextBackground side-by-side"
left-type="contextControl"
right-type="contextControl"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="contextLineNum gr-diff"></td>
- <td class="gr-diff"></td>
- <td class="contextLineNum gr-diff"></td>
- <td class="gr-diff"></td>
+ <td class="contextLineNum"></td>
+ <td></td>
+ <td class="contextLineNum"></td>
+ <td></td>
</tr>
- <tr class="dividerRow gr-diff show-both">
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="dividerCell gr-diff" colspan="4">
+ <tr class="dividerRow show-both">
+ <td class="dividerCell" colspan="4">
<gr-context-controls
- class="gr-diff"
showconfig="both"
></gr-context-controls>
</td>
</tr>
<tr
- class="below contextBackground gr-diff side-by-side"
+ class="below contextBackground side-by-side"
left-type="contextControl"
right-type="contextControl"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="contextLineNum gr-diff"></td>
- <td class="gr-diff"></td>
- <td class="contextLineNum gr-diff"></td>
- <td class="gr-diff"></td>
+ <td class="contextLineNum"></td>
+ <td></td>
+ <td class="contextLineNum"></td>
+ <td></td>
</tr>
</tbody>
- <tbody class="both gr-diff section">
+ <tbody class="both section">
<tr
aria-labelledby="left-button-38 left-content-38 right-button-37 right-content-37"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="38"></td>
- <td class="gr-diff left lineNum" data-value="38">
+ <td class="left lineNum" data-value="38">
<button
aria-label="38 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="38"
id="left-button-38"
tabindex="-1"
@@ -2343,17 +2258,17 @@ suite('gr-diff-element tests', () => {
38
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-38"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="37">
+ <td class="lineNum right" data-value="37">
<button
aria-label="37 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="37"
id="right-button-37"
tabindex="-1"
@@ -2361,9 +2276,9 @@ suite('gr-diff-element tests', () => {
37
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-37"
></div>
@@ -2371,16 +2286,15 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-39 left-content-39 right-button-38 right-content-38"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="39"></td>
- <td class="gr-diff left lineNum" data-value="39">
+ <td class="left lineNum" data-value="39">
<button
aria-label="39 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="39"
id="left-button-39"
tabindex="-1"
@@ -2388,17 +2302,17 @@ suite('gr-diff-element tests', () => {
39
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-39"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="38">
+ <td class="lineNum right" data-value="38">
<button
aria-label="38 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="38"
id="right-button-38"
tabindex="-1"
@@ -2406,9 +2320,9 @@ suite('gr-diff-element tests', () => {
38
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-38"
></div>
@@ -2416,16 +2330,15 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-40 left-content-40 right-button-39 right-content-39"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="40"></td>
- <td class="gr-diff left lineNum" data-value="40">
+ <td class="left lineNum" data-value="40">
<button
aria-label="40 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="40"
id="left-button-40"
tabindex="-1"
@@ -2433,17 +2346,17 @@ suite('gr-diff-element tests', () => {
40
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-40"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="39">
+ <td class="lineNum right" data-value="39">
<button
aria-label="39 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="39"
id="right-button-39"
tabindex="-1"
@@ -2451,32 +2364,31 @@ suite('gr-diff-element tests', () => {
39
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-39"
></div>
</td>
</tr>
</tbody>
- <tbody class="delta gr-diff section total">
+ <tbody class="delta section total">
<tr
aria-labelledby="right-button-40 right-content-40"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="blank"
right-type="add"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="blankLineNum gr-diff left"></td>
- <td class="blank gr-diff left no-intraline-info">
- <div class="contentText gr-diff" data-side="left"></div>
+ <td class="blankLineNum left"></td>
+ <td class="blank left no-intraline-info">
+ <div class="contentText" data-side="left"></div>
</td>
- <td class="gr-diff lineNum right" data-value="40">
+ <td class="lineNum right" data-value="40">
<button
aria-label="40 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="40"
id="right-button-40"
tabindex="-1"
@@ -2484,9 +2396,9 @@ suite('gr-diff-element tests', () => {
40
</button>
</td>
- <td class="add content gr-diff no-intraline-info right">
+ <td class="add content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-40"
></div>
@@ -2494,20 +2406,19 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="right-button-41 right-content-41"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="blank"
right-type="add"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="blankLineNum gr-diff left"></td>
- <td class="blank gr-diff left no-intraline-info">
- <div class="contentText gr-diff" data-side="left"></div>
+ <td class="blankLineNum left"></td>
+ <td class="blank left no-intraline-info">
+ <div class="contentText" data-side="left"></div>
</td>
- <td class="gr-diff lineNum right" data-value="41">
+ <td class="lineNum right" data-value="41">
<button
aria-label="41 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="41"
id="right-button-41"
tabindex="-1"
@@ -2515,9 +2426,9 @@ suite('gr-diff-element tests', () => {
41
</button>
</td>
- <td class="add content gr-diff no-intraline-info right">
+ <td class="add content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-41"
></div>
@@ -2525,20 +2436,19 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="right-button-42 right-content-42"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="blank"
right-type="add"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="blankLineNum gr-diff left"></td>
- <td class="blank gr-diff left no-intraline-info">
- <div class="contentText gr-diff" data-side="left"></div>
+ <td class="blankLineNum left"></td>
+ <td class="blank left no-intraline-info">
+ <div class="contentText" data-side="left"></div>
</td>
- <td class="gr-diff lineNum right" data-value="42">
+ <td class="lineNum right" data-value="42">
<button
aria-label="42 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="42"
id="right-button-42"
tabindex="-1"
@@ -2546,9 +2456,9 @@ suite('gr-diff-element tests', () => {
42
</button>
</td>
- <td class="add content gr-diff no-intraline-info right">
+ <td class="add content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-42"
></div>
@@ -2556,20 +2466,19 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="right-button-43 right-content-43"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="blank"
right-type="add"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="0"></td>
- <td class="blankLineNum gr-diff left"></td>
- <td class="blank gr-diff left no-intraline-info">
- <div class="contentText gr-diff" data-side="left"></div>
+ <td class="blankLineNum left"></td>
+ <td class="blank left no-intraline-info">
+ <div class="contentText" data-side="left"></div>
</td>
- <td class="gr-diff lineNum right" data-value="43">
+ <td class="lineNum right" data-value="43">
<button
aria-label="43 added"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="43"
id="right-button-43"
tabindex="-1"
@@ -2577,28 +2486,27 @@ suite('gr-diff-element tests', () => {
43
</button>
</td>
- <td class="add content gr-diff no-intraline-info right">
+ <td class="add content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-43"
></div>
</td>
</tr>
</tbody>
- <tbody class="both gr-diff section">
+ <tbody class="both section">
<tr
aria-labelledby="left-button-41 left-content-41 right-button-44 right-content-44"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="41"></td>
- <td class="gr-diff left lineNum" data-value="41">
+ <td class="left lineNum" data-value="41">
<button
aria-label="41 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="41"
id="left-button-41"
tabindex="-1"
@@ -2606,17 +2514,17 @@ suite('gr-diff-element tests', () => {
41
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-41"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="44">
+ <td class="lineNum right" data-value="44">
<button
aria-label="44 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="44"
id="right-button-44"
tabindex="-1"
@@ -2624,9 +2532,9 @@ suite('gr-diff-element tests', () => {
44
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-44"
></div>
@@ -2634,16 +2542,15 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-42 left-content-42 right-button-45 right-content-45"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="42"></td>
- <td class="gr-diff left lineNum" data-value="42">
+ <td class="left lineNum" data-value="42">
<button
aria-label="42 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="42"
id="left-button-42"
tabindex="-1"
@@ -2651,17 +2558,17 @@ suite('gr-diff-element tests', () => {
42
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-42"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="45">
+ <td class="lineNum right" data-value="45">
<button
aria-label="45 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="45"
id="right-button-45"
tabindex="-1"
@@ -2669,9 +2576,9 @@ suite('gr-diff-element tests', () => {
45
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-45"
></div>
@@ -2679,16 +2586,15 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-43 left-content-43 right-button-46 right-content-46"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="43"></td>
- <td class="gr-diff left lineNum" data-value="43">
+ <td class="left lineNum" data-value="43">
<button
aria-label="43 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="43"
id="left-button-43"
tabindex="-1"
@@ -2696,17 +2602,17 @@ suite('gr-diff-element tests', () => {
43
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-43"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="46">
+ <td class="lineNum right" data-value="46">
<button
aria-label="46 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="46"
id="right-button-46"
tabindex="-1"
@@ -2714,9 +2620,9 @@ suite('gr-diff-element tests', () => {
46
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-46"
></div>
@@ -2724,16 +2630,15 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-44 left-content-44 right-button-47 right-content-47"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="44"></td>
- <td class="gr-diff left lineNum" data-value="44">
+ <td class="left lineNum" data-value="44">
<button
aria-label="44 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="44"
id="left-button-44"
tabindex="-1"
@@ -2741,17 +2646,17 @@ suite('gr-diff-element tests', () => {
44
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-44"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="47">
+ <td class="lineNum right" data-value="47">
<button
aria-label="47 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="47"
id="right-button-47"
tabindex="-1"
@@ -2759,9 +2664,9 @@ suite('gr-diff-element tests', () => {
47
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-47"
></div>
@@ -2769,16 +2674,15 @@ suite('gr-diff-element tests', () => {
</tr>
<tr
aria-labelledby="left-button-45 left-content-45 right-button-48 right-content-48"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="45"></td>
- <td class="gr-diff left lineNum" data-value="45">
+ <td class="left lineNum" data-value="45">
<button
aria-label="45 unmodified"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="45"
id="left-button-45"
tabindex="-1"
@@ -2786,17 +2690,17 @@ suite('gr-diff-element tests', () => {
45
</button>
</td>
- <td class="both content gr-diff left no-intraline-info">
+ <td class="both content left no-intraline-info">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="left"
id="left-content-45"
></div>
</td>
- <td class="gr-diff lineNum right" data-value="48">
+ <td class="lineNum right" data-value="48">
<button
aria-label="48 unmodified"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="48"
id="right-button-48"
tabindex="-1"
@@ -2804,9 +2708,9 @@ suite('gr-diff-element tests', () => {
48
</button>
</td>
- <td class="both content gr-diff no-intraline-info right">
+ <td class="both content no-intraline-info right">
<div
- class="contentText gr-diff"
+ class="contentText"
data-side="right"
id="right-content-48"
></div>
@@ -2822,7 +2726,6 @@ suite('gr-diff-element tests', () => {
'gr-diff-section',
'gr-diff-row',
'gr-diff-text',
- 'gr-legacy-text',
'slot',
],
}
@@ -2859,25 +2762,23 @@ suite('gr-diff-element tests', () => {
<gr-diff-row class="left-FILE right-FILE"> </gr-diff-row>
<table id="diffTable">
<colgroup>
- <col class="blame gr-diff" />
- <col class="gr-diff left" width="48" />
- <col class="gr-diff left" />
- <col class="gr-diff right" width="48" />
- <col class="gr-diff right" />
+ <col class="left" width="48" />
+ <col class="left" />
+ <col class="right" width="48" />
+ <col class="right" />
</colgroup>
- <tbody class="both gr-diff section">
+ <tbody class="both section">
<tr
aria-labelledby="left-button-FILE left-content-FILE right-button-FILE right-content-FILE"
- class="diff-row gr-diff side-by-side"
+ class="diff-row side-by-side"
left-type="both"
right-type="both"
tabindex="-1"
>
- <td class="blame gr-diff" data-line-number="FILE"></td>
- <td class="gr-diff left lineNum" data-value="FILE">
+ <td class="left lineNum" data-value="FILE">
<button
aria-label="Add file comment"
- class="gr-diff left lineNumButton"
+ class="left lineNumButton"
data-value="FILE"
id="left-button-FILE"
tabindex="-1"
@@ -2885,13 +2786,11 @@ suite('gr-diff-element tests', () => {
FILE
</button>
</td>
- <td
- class="both content file gr-diff left no-intraline-info"
- ></td>
- <td class="gr-diff lineNum right" data-value="FILE">
+ <td class="both content file left no-intraline-info"></td>
+ <td class="lineNum right" data-value="FILE">
<button
aria-label="Add file comment"
- class="gr-diff lineNumButton right"
+ class="lineNumButton right"
data-value="FILE"
id="right-button-FILE"
tabindex="-1"
@@ -2899,14 +2798,12 @@ suite('gr-diff-element tests', () => {
FILE
</button>
</td>
- <td
- class="both content file gr-diff no-intraline-info right"
- ></td>
+ <td class="both content file no-intraline-info right"></td>
</tr>
</tbody>
- <tbody class="binary-diff gr-diff">
- <tr class="gr-diff">
- <td class="gr-diff" colspan="5">
+ <tbody class="binary-diff">
+ <tr>
+ <td colspan="4">
<span> Difference in binary files </span>
</td>
</tr>
@@ -2982,32 +2879,32 @@ suite('gr-diff-element tests', () => {
assert.lightDom.equal(
imageDiffSection,
/* HTML */ `
- <tbody class="gr-diff image-diff">
- <tr class="gr-diff">
- <td class="blank gr-diff left lineNum"></td>
- <td class="gr-diff left">
+ <tbody class="image-diff">
+ <tr>
+ <td class="blank left lineNum"></td>
+ <td class="left">
<img
class="gr-diff left"
src="data:image/bmp;base64,${mockFile1.body}"
/>
</td>
- <td class="blank gr-diff lineNum right"></td>
- <td class="gr-diff right">
+ <td class="blank lineNum right"></td>
+ <td class="right">
<img
class="gr-diff right"
src="data:image/bmp;base64,${mockFile2.body}"
/>
</td>
</tr>
- <tr class="gr-diff">
- <td class="blank gr-diff left lineNum"></td>
- <td class="gr-diff left">
+ <tr>
+ <td class="blank left lineNum"></td>
+ <td class="left">
<label class="gr-diff">
<span class="gr-diff label"> 1×1 image/bmp </span>
</label>
</td>
- <td class="blank gr-diff lineNum right"></td>
- <td class="gr-diff right">
+ <td class="blank lineNum right"></td>
+ <td class="right">
<label class="gr-diff">
<span class="gr-diff label"> 1×1 image/bmp </span>
</label>
@@ -3020,8 +2917,8 @@ suite('gr-diff-element tests', () => {
assert.dom.equal(
endpoint,
/* HTML */ `
- <tbody class="gr-diff endpoint">
- <tr class="gr-diff">
+ <tbody class="endpoint">
+ <tr>
<gr-endpoint-decorator class="gr-diff" name="image-diff">
<gr-endpoint-param class="gr-diff" name="baseImage">
</gr-endpoint-param>
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
index f4062154b6..c5366e7799 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
@@ -9,10 +9,12 @@ import {assertIsDefined, assert} from '../../../utils/common-util';
import {isDefined} from '../../../types/types';
export enum GrDiffGroupType {
- /** Unchanged context. */
+ /** A group of unchanged diff lines. */
BOTH = 'both',
- /** A widget used to show more context. */
+ /**
+ * A context control group "hides" other diff groups in its `contextGroups` field.
+ */
CONTEXT_CONTROL = 'contextControl',
/** Added, removed or modified chunk. */
@@ -216,7 +218,13 @@ export interface GrMoveDetails {
};
}
-/** A chunk of the diff that should be rendered together. */
+/**
+ * A chunk of the diff that should be rendered together. Typically corresponds
+ * to a gr-diff-section. It mostly just contains an array of diff `lines`.
+ *
+ * A group of type CONTEXT_CONTROL does not contain any lines directly, but
+ * "hides" other groups in `contextGroups`, which the user can expand.
+ */
export class GrDiffGroup {
constructor(
options:
@@ -322,6 +330,12 @@ export class GrDiffGroup {
readonly removes: GrDiffLine[] = [];
+ /**
+ * Only set, iff type is CONTEXT_CONTROL.
+ *
+ * A CONTEXT_CONTROL group "hides" other groups that the user may expand.
+ * This field contains those hidden groups.
+ */
readonly contextGroups: GrDiffGroup[] = [];
readonly skip?: number;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts
index 95b2f4e8ae..8b333e6fbd 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts
@@ -5,14 +5,8 @@
*/
import {css} from 'lit';
+// Styles related to the top-level <gr-diff> component.
export const grDiffStyles = css`
- :host(.disable-context-control-buttons) .section {
- border-right: none;
- }
- :host(.hide-line-length-indicator) .full-width td.content .contentText {
- background-image: none;
- }
-
:host {
font-family: var(--monospace-font-family, ''), 'Roboto Mono';
font-size: var(--font-size, var(--font-size-code, 12px));
@@ -22,129 +16,270 @@ export const grDiffStyles = css`
);
}
- .thread-group {
- display: block;
- max-width: var(--content-width, 80ch);
- white-space: normal;
- background-color: var(--diff-blank-background-color);
+ gr-diff-image-new,
+ gr-diff-image-old,
+ gr-diff-section,
+ gr-context-controls-section,
+ gr-diff-row {
+ display: contents;
}
- .diffContainer {
+`;
+
+// Styles related to the <gr-diff-element> component.
+export const grDiffElementStyles = css`
+ gr-diff-element div.diffContainer {
max-width: var(--diff-max-width, none);
font-family: var(--monospace-font-family);
}
- table {
+ gr-diff-element table {
border-collapse: collapse;
table-layout: fixed;
}
- td.lineNum,
- td.blankLineNum {
- /* Enforces background whenever lines wrap */
- background-color: var(--diff-blank-background-color);
+ gr-diff-element table.responsive {
+ width: 100%;
}
-
- /* Provides the option to add side borders (left and right) to the line
- number column. */
- td.lineNum,
- td.blankLineNum,
- td.moveControlsLineNumCol,
- td.contextLineNum {
- box-shadow: var(--line-number-box-shadow, unset);
+ gr-diff-element div#diffHeader {
+ background-color: var(--table-header-background-color);
+ border-bottom: 1px solid var(--border-color);
+ color: var(--link-color);
+ padding: var(--spacing-m) 0 var(--spacing-m) 48px;
+ }
+ gr-diff-element table#diffTable:focus {
+ outline: none;
}
+ gr-diff-element div#loadingError,
+ gr-diff-element div#sizeWarning {
+ display: block;
+ margin: var(--spacing-l) auto;
+ max-width: 60em;
+ text-align: center;
+ }
+ gr-diff-element div#loadingError {
+ color: var(--error-text-color);
+ }
+ gr-diff-element div#sizeWarning gr-button {
+ margin: var(--spacing-l);
+ }
+ gr-diff-element div.newlineWarning {
+ color: var(--deemphasized-text-color);
+ text-align: center;
+ }
+ gr-diff-element div.newlineWarning.hidden {
+ display: none;
+ }
+ gr-diff-element div.whitespace-change-only-message {
+ background-color: var(--diff-context-control-background-color);
+ border: 1px solid var(--diff-context-control-border-color);
+ text-align: center;
+ }
+ gr-diff-element {
+ /* for gr-selection-action-box positioning */
+ position: relative;
+ }
+ gr-diff-element gr-selection-action-box {
+ /* Needs z-index to appear above wrapped content, since it's inserted
+ into DOM before it. */
+ z-index: 120;
+ }
+`;
+// Styles related to the <gr-diff-section> component.
+export const grDiffSectionStyles = css`
+ :host(.disable-context-control-buttons) gr-diff-section tbody.section {
+ border-right: none;
+ }
/* Context controls break up the table visually, so we set the right
border on individual sections to leave a gap for the divider.
Also taken into account for max-width calculations in SHRINK_ONLY mode
(check GrDiff.updatePreferenceStyles). */
- .section {
+ gr-diff-section tbody.section {
border-right: 1px solid var(--border-color);
}
- .section.contextControl {
+ gr-diff-section tbody.section.contextControl {
/* Divider inside this section must not have border; we set borders on
the padding rows below. */
border-right-width: 0;
}
+ gr-diff-section tr.moveControls td.moveHeader a {
+ color: inherit;
+ }
+ gr-diff-section tbody.contextControl {
+ display: table-row-group;
+ background-color: transparent;
+ border: none;
+ --divider-height: var(--spacing-s);
+ --divider-border: 1px;
+ }
+ /* TODO: Is this still used? */
+ gr-diff-section tbody.contextControl gr-button gr-icon {
+ /* should match line-height of gr-button */
+ font-size: var(--line-height-mono, 18px);
+ }
+ gr-diff-section tbody.contextControl td:not(.lineNumButton) {
+ text-align: center;
+ }
+ /* Option to add side borders (left and right) to the line number column. */
+ gr-diff-section tr.moveControls td.moveControlsLineNumCol {
+ box-shadow: var(--line-number-box-shadow, unset);
+ }
+`;
+
+// Styles related to the <gr-diff-row> component.
+export const grDiffContextControlsSectionStyles = css`
+ /* Hide the actual context control buttons */
+ :host(.disable-context-control-buttons)
+ gr-diff-section
+ tbody.contextControl
+ gr-context-controls-section
+ gr-context-controls {
+ display: none;
+ }
+ /* Maintain a small amount of padding at the edges of diff chunks */
+ :host(.disable-context-control-buttons)
+ gr-diff-section
+ tbody.contextControl
+ gr-context-controls-section
+ tr.contextBackground {
+ height: var(--spacing-s);
+ border-right: none;
+ }
+ gr-context-controls-section tr.contextBackground {
+ /* One line of background behind the context expanders which they can
+ render on top of, plus some padding. */
+ height: calc(var(--line-height-normal) + var(--spacing-s));
+ }
/* Padding rows behind context controls. The diff is styled to be cut
into two halves by the negative space of the divider on which the
context control buttons are anchored. */
- .contextBackground {
+ gr-context-controls-section tr.contextBackground {
border-right: 1px solid var(--border-color);
}
- .contextBackground.above {
+ gr-context-controls-section tr.contextBackground.above {
border-bottom: 1px solid var(--border-color);
}
- .contextBackground.below {
+ gr-context-controls-section tr.contextBackground.below {
border-top: 1px solid var(--border-color);
}
+ gr-context-controls-section tr.contextBackground td.contextLineNum {
+ color: var(--deemphasized-text-color);
+ padding: 0 var(--spacing-m);
+ text-align: right;
+ }
+ /* Option to add side borders (left and right) to the line number column. */
+ gr-context-controls-section tr.contextBackground td.contextLineNum {
+ box-shadow: var(--line-number-box-shadow, unset);
+ }
+ /* Padding rows behind context controls. Styled as a continuation of the
+ line gutters and code area. */
+ gr-context-controls-section tr.contextBackground td.contextLineNum {
+ background-color: var(--diff-blank-background-color);
+ }
+ gr-context-controls-section tr.contextBackground > td:not(.contextLineNum) {
+ background-color: var(--view-background-color);
+ }
+ gr-context-controls-section td.dividerCell {
+ vertical-align: top;
+ }
+ gr-context-controls-section tr.dividerRow.show-both td.dividerCell {
+ height: var(--divider-height);
+ }
+ gr-context-controls-section tr.dividerRow.show-above td.dividerCell {
+ height: 0;
+ }
+`;
- .lineNumButton {
+// Styles related to the <gr-diff-row> component.
+export const grDiffRowStyles = css`
+ gr-diff-row td.content div.thread-group {
+ display: block;
+ max-width: var(--content-width, 80ch);
+ white-space: normal;
+ background-color: var(--diff-blank-background-color);
+ }
+ gr-diff-row tr.target-row td.blame {
+ background: var(--diff-selection-background-color);
+ }
+ gr-diff-row td.content.lost div {
+ background-color: var(--info-background);
+ }
+ gr-diff-row td.content.lost div.lost-message {
+ font-family: var(--font-family, 'Roboto');
+ font-size: var(--font-size-normal, 14px);
+ line-height: var(--line-height-normal);
+ padding: var(--spacing-s) 0;
+ }
+ gr-diff-row td.content.lost div.lost-message gr-icon {
+ padding: 0 var(--spacing-s) 0 var(--spacing-m);
+ color: var(--blue-700);
+ }
+ gr-diff-row td.blame {
+ padding: 0 var(--spacing-m);
+ white-space: pre;
+ }
+ gr-diff-row td.blame > span {
+ opacity: 0.6;
+ }
+ gr-diff-row td.blame > span.startOfRange {
+ opacity: 1;
+ }
+ gr-diff-row td.blame .blameDate {
+ font-family: var(--monospace-font-family);
+ color: var(--link-color);
+ text-decoration: none;
+ }
+ gr-diff-row td.content div.contentText gr-diff-text:empty:after,
+ gr-diff-row td.content div.contentText:empty:after {
+ /* Newline, to ensure empty lines are one line-height tall. */
+ content: '\\A';
+ }
+ /* Option to add side borders (left and right) to the line number column. */
+ gr-diff-row td.lineNum,
+ gr-diff-row td.blankLineNum {
+ box-shadow: var(--line-number-box-shadow, unset);
+ }
+ gr-diff-row td.lineNum,
+ gr-diff-row td.blankLineNum {
+ /* Enforces background whenever lines wrap */
+ background-color: var(--diff-blank-background-color);
+ }
+ gr-diff-row td.lineNum button.lineNumButton {
display: block;
width: 100%;
height: 100%;
background-color: var(--diff-blank-background-color);
box-shadow: var(--line-number-box-shadow, unset);
}
- td.lineNum {
+ gr-diff-row td.lineNum {
vertical-align: top;
}
-
/* The only way to focus this (clicking) will apply our own focus
styling, so this default styling is not needed and distracting. */
- .lineNumButton:focus {
+ gr-diff-row td.lineNum button.lineNumButton:focus {
outline: none;
}
- gr-image-viewer {
- width: 100%;
- height: 100%;
- max-width: var(--image-viewer-max-width, 95vw);
- max-height: var(--image-viewer-max-height, 90vh);
- /* Defined by paper-styles default-theme and used in various
- components. background-color-secondary is a compromise between
- fairly light in light theme (where we ideally would want
- background-color-primary) yet slightly offset against the app
- background in dark mode, where drop shadows e.g. around paper-card
- are almost invisible. */
- --primary-background-color: var(--background-color-secondary);
- }
- .image-diff .gr-diff {
- text-align: center;
- }
- .image-diff img {
- box-shadow: var(--elevation-level-1);
- max-width: 50em;
- }
- .image-diff .right.lineNumButton {
- border-left: 1px solid var(--border-color);
- }
- .image-diff label {
- font-family: var(--font-family);
- font-style: italic;
- }
- tbody.binary-diff td {
- font-family: var(--font-family);
- font-style: italic;
- text-align: center;
- padding: var(--spacing-s) 0;
- }
- .diff-row {
+ gr-diff-row tr.diff-row {
outline: none;
- user-select: none;
}
- .diff-row.target-row.target-side-left .lineNumButton.left,
- .diff-row.target-row.target-side-right .lineNumButton.right,
- .diff-row.target-row.unified .lineNumButton {
+ gr-diff-row
+ tr.diff-row.target-row.target-side-left
+ td.lineNum
+ button.lineNumButton.left,
+ gr-diff-row
+ tr.diff-row.target-row.target-side-right
+ td.lineNum
+ button.lineNumButton.right,
+ gr-diff-row tr.diff-row.target-row.unified td.lineNum button.lineNumButton {
color: var(--primary-text-color);
}
-
/* Preparing selected line cells with position relative so it allows a
positioned overlay with 'position: absolute'. */
- .target-row td {
+ gr-diff-row tr.target-row td {
position: relative;
}
-
/* Defines an overlay to the selected line for drawing an outline without
blocking user interaction (e.g. text selection). */
- .target-row td::before {
+ gr-diff-row tr.target-row td::before {
border-width: 0;
border-style: solid;
border-color: var(--focused-line-outline-color);
@@ -157,108 +292,67 @@ export const grDiffStyles = css`
user-select: none;
content: ' ';
}
-
/* The outline for the selected content cell should be the same in all
cases. */
- .target-row.target-side-left td.left.content::before,
- .target-row.target-side-right td.right.content::before,
- .unified.target-row td.content::before {
+ gr-diff-row tr.target-row.target-side-left td.left.content::before,
+ gr-diff-row tr.target-row.target-side-right td.right.content::before,
+ gr-diff-row tr.unified.target-row td.content::before {
border-width: 1px 1px 1px 0;
}
-
/* The outline for the sign cell should be always be contiguous
top/bottom. */
- .target-row.target-side-left td.left.sign::before,
- .target-row.target-side-right td.right.sign::before {
+ gr-diff-row tr.target-row.target-side-left td.left.sign::before,
+ gr-diff-row tr.target-row.target-side-right td.right.sign::before {
border-width: 1px 0;
}
-
/* For side-by-side we need to select the correct line number to
"visually close" the outline. */
- .side-by-side.target-row.target-side-left td.left.lineNum::before,
- .side-by-side.target-row.target-side-right td.right.lineNum::before {
+ gr-diff-row
+ tr.side-by-side.target-row.target-side-left
+ td.left.lineNum::before,
+ gr-diff-row
+ tr.side-by-side.target-row.target-side-right
+ td.right.lineNum::before {
border-width: 1px 0 1px 1px;
}
-
/* For unified diff we always start the overlay from the left cell. */
- .unified.target-row td.left:not(.content)::before {
+ gr-diff-row tr.unified.target-row td.left:not(.content)::before {
border-width: 1px 0 1px 1px;
}
-
/* For unified diff we should continue the top/bottom border in right
line number column. */
- .unified.target-row td.right:not(.content)::before {
+ gr-diff-row tr.unified.target-row td.right:not(.content)::before {
border-width: 1px 0;
}
-
- .content {
+ gr-diff-row td.content {
background-color: var(--diff-blank-background-color);
}
-
- /* Describes two states of semantic tokens: whenever a token has a
- definition that can be navigated to (navigable) and whenever
- the token is actually clickable to perform this navigation. */
- .semantic-token.navigable {
- text-decoration-style: dotted;
- text-decoration-line: underline;
- }
- .semantic-token.navigable.clickable {
- text-decoration-style: solid;
- cursor: pointer;
- }
-
/* The file line, which has no contentText, add some margin before the
first comment. We cannot add padding the container because we only
want it if there is at least one comment thread, and the slotting
makes :empty not work as expected. */
- .content.file slot:first-child::slotted(.comment-thread) {
+ gr-diff-row td.content.file slot:first-child::slotted(.comment-thread) {
display: block;
margin-top: var(--spacing-xs);
}
- .contentText {
+ gr-diff-row td.content div.contentText {
background-color: var(--view-background-color);
}
- .blank {
- background-color: var(--diff-blank-background-color);
- }
- .image-diff .content {
+ gr-diff-row td.blank {
background-color: var(--diff-blank-background-color);
}
- .responsive {
- width: 100%;
- }
- .responsive .contentText {
- white-space: break-spaces;
- word-break: break-all;
- }
- .lineNumButton,
- .content {
- vertical-align: top;
- white-space: pre;
- }
- .contextLineNum,
- .lineNumButton {
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-
- color: var(--deemphasized-text-color);
- padding: 0 var(--spacing-m);
- text-align: right;
- }
- .canComment .lineNumButton {
+ gr-diff-element div.canComment gr-diff-row td.lineNum button.lineNumButton {
cursor: pointer;
}
- .sign {
+ gr-diff-row td.sign {
min-width: 1ch;
width: 1ch;
background-color: var(--view-background-color);
}
- .sign.blank {
+ gr-diff-row td.sign.blank {
background-color: var(--diff-blank-background-color);
}
- .content {
+ gr-diff-row td.content {
/* Set min width since setting width on table cells still allows them
to shrink. Do not set max width because CJK
(Chinese-Japanese-Korean) glyphs have variable width. */
@@ -266,395 +360,408 @@ export const grDiffStyles = css`
width: var(--content-width, 80ch);
}
/* If there are no intraline info, consider everything changed */
- .content.add .contentText .intraline,
- .content.add.no-intraline-info .contentText,
- .sign.add.no-intraline-info,
- .delta.total .content.add .contentText {
+ gr-diff-row td.content.add div.contentText .intraline,
+ gr-diff-row td.content.add.no-intraline-info div.contentText,
+ gr-diff-row td.sign.add.no-intraline-info,
+ gr-diff-section tbody.delta.total gr-diff-row td.content.add div.contentText {
background-color: var(--dark-add-highlight-color);
}
- .content.add .contentText,
- .sign.add {
+ gr-diff-row td.content.add div.contentText,
+ gr-diff-row td.sign.add {
background-color: var(--light-add-highlight-color);
}
/* If there are no intraline info, consider everything changed */
- .content.remove .contentText .intraline,
- .content.remove.no-intraline-info .contentText,
- .delta.total .content.remove .contentText,
- .sign.remove.no-intraline-info {
+ gr-diff-row td.content.remove div.contentText .intraline,
+ gr-diff-row td.content.remove.no-intraline-info div.contentText,
+ gr-diff-section
+ tbody.delta.total
+ gr-diff-row
+ td.content.remove
+ div.contentText,
+ gr-diff-row td.sign.remove.no-intraline-info {
background-color: var(--dark-remove-highlight-color);
}
- .content.remove .contentText,
- .sign.remove {
+ gr-diff-row td.content.remove div.contentText,
+ gr-diff-row td.sign.remove {
background-color: var(--light-remove-highlight-color);
}
-
- .ignoredWhitespaceOnly .sign.no-intraline-info {
- background-color: var(--view-background-color);
+ gr-diff-element table.responsive gr-diff-row td.content div.contentText {
+ white-space: break-spaces;
+ word-break: break-all;
}
-
- /* dueToRebase */
- .dueToRebase .content.add .contentText .intraline,
- .delta.total.dueToRebase .content.add .contentText {
- background-color: var(--dark-rebased-add-highlight-color);
+ gr-diff-row td.lineNum button.lineNumButton,
+ td.content {
+ vertical-align: top;
+ white-space: pre;
}
- .dueToRebase .content.add .contentText {
- background-color: var(--light-rebased-add-highlight-color);
+ gr-diff-row td.lineNum button.lineNumButton {
+ color: var(--deemphasized-text-color);
+ padding: 0 var(--spacing-m);
+ text-align: right;
}
- .dueToRebase .content.remove .contentText .intraline,
- .delta.total.dueToRebase .content.remove .contentText {
- background-color: var(--dark-rebased-remove-highlight-color);
+ gr-diff-element table.responsive gr-diff-row td.blame {
+ overflow: hidden;
+ width: 200px;
}
- .dueToRebase .content.remove .contentText {
- background-color: var(--light-rebased-remove-highlight-color);
+ /** Support the line length indicator **/
+ gr-diff-element table.responsive gr-diff-row td.content div.contentText {
+ /* Same strategy as in
+ https://stackoverflow.com/questions/1179928/how-can-i-put-a-vertical-line-down-the-center-of-a-div
+ */
+ background-image: linear-gradient(
+ var(--line-length-indicator-color),
+ var(--line-length-indicator-color)
+ );
+ background-size: 1px 100%;
+ background-position: var(--line-limit-marker) 0;
+ background-repeat: no-repeat;
}
+ gr-diff-row td.lineNum.COVERED button.lineNumButton {
+ color: var(
+ --coverage-covered-line-num-color,
+ var(--deemphasized-text-color)
+ );
+ background-color: var(--coverage-covered, #e0f2f1);
+ }
+ gr-diff-row td.lineNum.NOT_COVERED button.lineNumButton {
+ color: var(
+ --coverage-covered-line-num-color,
+ var(--deemphasized-text-color)
+ );
+ background-color: var(--coverage-not-covered, #ffd1a4);
+ }
+ gr-diff-row td.lineNum.PARTIALLY_COVERED button.lineNumButton {
+ color: var(
+ --coverage-covered-line-num-color,
+ var(--deemphasized-text-color)
+ );
+ background: linear-gradient(
+ to right bottom,
+ var(--coverage-not-covered, #ffd1a4) 0%,
+ var(--coverage-not-covered, #ffd1a4) 50%,
+ var(--coverage-covered, #e0f2f1) 50%,
+ var(--coverage-covered, #e0f2f1) 100%
+ );
+ }
+`;
+
+// Styles related to (not) highlighting ignored whitespace.
+export const grDiffIgnoredWhitespaceStyles = css`
+ gr-diff-section
+ tbody.ignoredWhitespaceOnly
+ gr-diff-row
+ td.sign.no-intraline-info,
+ gr-diff-section
+ tbody.ignoredWhitespaceOnly
+ gr-diff-row
+ td.content.add
+ div.contentText
+ .intraline,
+ gr-diff-section
+ tbody.delta.total.ignoredWhitespaceOnly
+ gr-diff-row
+ td.content.add
+ div.contentText,
+ gr-diff-section
+ tbody.ignoredWhitespaceOnly
+ gr-diff-row
+ td.content.add
+ div.contentText,
+ gr-diff-section
+ tbody.ignoredWhitespaceOnly
+ gr-diff-row
+ td.content.remove
+ div.contentText
+ .intraline,
+ gr-diff-section
+ tbody.delta.total.ignoredWhitespaceOnly
+ gr-diff-row
+ td.content.remove
+ div.contentText,
+ gr-diff-section
+ tbody.ignoredWhitespaceOnly
+ gr-diff-row
+ td.content.remove
+ div.contentText {
+ background-color: var(--view-background-color);
+ }
+`;
- /* dueToMove */
- .dueToMove .sign.add,
- .dueToMove .content.add .contentText,
- .dueToMove .moveControls.movedIn .sign.right,
- .dueToMove .moveControls.movedIn .moveHeader,
- .delta.total.dueToMove .content.add .contentText {
+// Styles related to highlighting moved code sections.
+export const grDiffMoveStyles = css`
+ gr-diff-section tbody.dueToMove gr-diff-row .sign.add,
+ gr-diff-section tbody.dueToMove gr-diff-row td.content.add div.contentText,
+ gr-diff-section tbody.dueToMove tr.moveControls.movedIn .sign.right,
+ gr-diff-section tbody.dueToMove tr.moveControls.movedIn td.moveHeader,
+ gr-diff-section tbody.delta.total.dueToMove td.content.add div.contentText {
background-color: var(--diff-moved-in-background);
}
- .dueToMove.changed .sign.add,
- .dueToMove.changed .content.add .contentText,
- .dueToMove.changed .moveControls.movedIn .sign.right,
- .dueToMove.changed .moveControls.movedIn .moveHeader,
- .delta.total.dueToMove.changed .content.add .contentText {
+ gr-diff-section tbody.dueToMove.changed gr-diff-row .sign.add,
+ gr-diff-section
+ tbody.dueToMove.changed
+ gr-diff-row
+ td.content.add
+ div.contentText,
+ gr-diff-section tbody.dueToMove.changed tr.moveControls.movedIn .sign.right,
+ gr-diff-section tbody.dueToMove.changed tr.moveControls.movedIn td.moveHeader,
+ gr-diff-section
+ tbody.delta.total.dueToMove.changed
+ td.content.add
+ div.contentText {
background-color: var(--diff-moved-in-changed-background);
}
- .dueToMove .sign.remove,
- .dueToMove .content.remove .contentText,
- .dueToMove .moveControls.movedOut .moveHeader,
- .dueToMove .moveControls.movedOut .sign.left,
- .delta.total.dueToMove .content.remove .contentText {
+ gr-diff-section tbody.dueToMove .sign.remove,
+ gr-diff-section tbody.dueToMove td.content.remove div.contentText,
+ gr-diff-section tbody.dueToMove tr.moveControls.movedOut td.moveHeader,
+ gr-diff-section tbody.dueToMove tr.moveControls.movedOut .sign.left,
+ gr-diff-section
+ tbody.delta.total.dueToMove
+ td.content.remove
+ div.contentText {
background-color: var(--diff-moved-out-background);
}
- .delta.dueToMove .movedIn .moveHeader {
+ gr-diff-section tbody.delta.dueToMove tr.movedIn td.moveHeader {
--gr-range-header-color: var(--diff-moved-in-label-color);
}
- .delta.dueToMove.changed .movedIn .moveHeader {
+ gr-diff-section tbody.delta.dueToMove.changed tr.movedIn td.moveHeader {
--gr-range-header-color: var(--diff-moved-in-changed-label-color);
}
- .delta.dueToMove .movedOut .moveHeader {
+ gr-diff-section tbody.delta.dueToMove tr.movedOut td.moveHeader {
--gr-range-header-color: var(--diff-moved-out-label-color);
}
+`;
- .moveHeader a {
- color: inherit;
- }
-
- /* ignoredWhitespaceOnly */
- .ignoredWhitespaceOnly .content.add .contentText .intraline,
- .delta.total.ignoredWhitespaceOnly .content.add .contentText,
- .ignoredWhitespaceOnly .content.add .contentText,
- .ignoredWhitespaceOnly .content.remove .contentText .intraline,
- .delta.total.ignoredWhitespaceOnly .content.remove .contentText,
- .ignoredWhitespaceOnly .content.remove .contentText {
- background-color: var(--view-background-color);
- }
-
- .content .contentText gr-diff-text:empty:after,
- .content .contentText gr-legacy-text:empty:after,
- .content .contentText:empty:after {
- /* Newline, to ensure empty lines are one line-height tall. */
- content: '\\A';
+// Styles related to highlighting rebased code sections.
+export const grDiffRebaseStyles = css`
+ gr-diff-section
+ tbody.dueToRebase
+ gr-diff-row
+ td.content.add
+ div.contentText
+ .intraline,
+ gr-diff-section
+ tbody.delta.total.dueToRebase
+ gr-diff-row
+ td.content.add
+ div.contentText {
+ background-color: var(--dark-rebased-add-highlight-color);
}
-
- /* Context controls */
- .contextControl {
- display: table-row-group;
- background-color: transparent;
- border: none;
- --divider-height: var(--spacing-s);
- --divider-border: 1px;
+ gr-diff-section tbody.dueToRebase gr-diff-row td.content.add div.contentText {
+ background-color: var(--light-rebased-add-highlight-color);
}
- /* TODO: Is this still used? */
- .contextControl gr-button gr-icon {
- /* should match line-height of gr-button */
- font-size: var(--line-height-mono, 18px);
+ gr-diff-section
+ tbody.dueToRebase
+ gr-diff-row
+ td.content.remove
+ div.contentText
+ .intraline,
+ gr-diff-section
+ tbody.delta.total.dueToRebase
+ gr-diff-row
+ td.content.remove
+ div.contentText {
+ background-color: var(--dark-rebased-remove-highlight-color);
}
- .contextControl td:not(.lineNumButton) {
- text-align: center;
+ gr-diff-section
+ tbody.dueToRebase
+ gr-diff-row
+ td.content.remove
+ div.contentText {
+ background-color: var(--light-rebased-remove-highlight-color);
}
+`;
- /* Padding rows behind context controls. Styled as a continuation of the
- line gutters and code area. */
- .contextBackground > .contextLineNum {
- background-color: var(--diff-blank-background-color);
+// Styles related to selecting code in gr-diff.
+export const grDiffSelectionStyles = css`
+ /* by default do not allow selecting anything */
+ gr-context-controls-section .contextLineNum,
+ gr-diff-row td.lineNum button.lineNumButton,
+ gr-diff-row tr.diff-row,
+ gr-diff-row td.content,
+ gr-diff-section tbody.contextControl,
+ gr-diff-row td.blame,
+ #diffHeader {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
}
- .contextBackground > td:not(.contextLineNum) {
- background-color: var(--view-background-color);
+ /* selected-left */
+ gr-diff-element.selected-left:not(.selected-comment)
+ gr-diff-row
+ tr.side-by-side
+ td.left
+ + td.content
+ div.contentText,
+ gr-diff-element.selected-left:not(.selected-comment)
+ gr-diff-row
+ tr.unified
+ td.left.lineNum
+ ~ td.content:not(.both)
+ div.contentText {
+ -webkit-user-select: text;
+ -moz-user-select: text;
+ -ms-user-select: text;
+ user-select: text;
}
- .contextBackground {
- /* One line of background behind the context expanders which they can
- render on top of, plus some padding. */
- height: calc(var(--line-height-normal) + var(--spacing-s));
+ /* selected-right */
+ gr-diff-element.selected-right:not(.selected-comment)
+ gr-diff-row
+ tr.side-by-side
+ td.right
+ + td.content
+ div.contentText,
+ gr-diff-element.selected-right:not(.selected-comment)
+ gr-diff-row
+ tr.unified
+ td.right.lineNum
+ ~ td.content
+ div.contentText {
+ -webkit-user-select: text;
+ -moz-user-select: text;
+ -ms-user-select: text;
+ user-select: text;
}
-
- /* Hide the actual context control buttons */
- :host(.disable-context-control-buttons) .contextControl gr-context-controls {
- display: none;
+ /* selected-comment */
+ gr-diff-element.selected-left.selected-comment
+ gr-diff-row
+ tr.side-by-side
+ td.left
+ + td.content
+ div.thread-group
+ .message,
+ gr-diff-element.selected-left.selected-comment
+ ::slotted(.comment-thread[diff-side='left']),
+ gr-diff-element.selected-right.selected-comment
+ gr-diff-row
+ tr.side-by-side
+ td.right
+ + td.content
+ div.thread-group
+ .message
+ :not(.collapsedContent),
+ gr-diff-element.selected-right.selected-comment
+ ::slotted(.comment-thread[diff-side='right']),
+ gr-diff-element.selected-comment
+ gr-diff-row
+ tr.unified
+ td.content
+ div.thread-group
+ .message
+ :not(.collapsedContent) {
+ -webkit-user-select: text;
+ -moz-user-select: text;
+ -ms-user-select: text;
+ user-select: text;
}
- /* Maintain a small amount of padding at the edges of diff chunks */
- :host(.disable-context-control-buttons) .contextControl .contextBackground {
- height: var(--spacing-s);
- border-right: none;
+ /* selected-blame */
+ gr-diff-element.selected-blame gr-diff-row td.blame {
+ -webkit-user-select: text;
+ -moz-user-select: text;
+ -ms-user-select: text;
+ user-select: text;
}
+`;
- .dividerCell {
- vertical-align: top;
+// Styles related to the <gr-diff-text> component.
+export const grDiffTextStyles = css`
+ gr-diff-text .token-highlight {
+ background-color: var(--token-highlighting-color, #fffd54);
}
- .dividerRow.show-both .dividerCell {
- height: var(--divider-height);
+ /* Describes two states of semantic tokens: whenever a token has a
+ definition that can be navigated to (navigable) and whenever
+ the token is actually clickable to perform this navigation. */
+ gr-diff-text .semantic-token.navigable {
+ text-decoration-style: dotted;
+ text-decoration-line: underline;
}
- .dividerRow.show-above .dividerCell,
- .dividerRow.show-above .dividerCell {
- height: 0;
+ gr-diff-text .semantic-token.navigable.clickable {
+ text-decoration-style: solid;
+ cursor: pointer;
}
-
- .br:after {
+ gr-diff-text .br:after {
/* Line feed */
content: '\\A';
}
- .tab {
+ gr-diff-text .tab {
display: inline-block;
}
- .tab-indicator:before {
+ gr-diff-text .tab-indicator:before {
color: var(--diff-tab-indicator-color);
/* >> character */
content: '\\00BB';
position: absolute;
}
- .special-char-indicator {
+ gr-diff-text .special-char-indicator {
/* spacing so elements don't collide */
padding-right: var(--spacing-m);
}
- .special-char-indicator:before {
+ gr-diff-text .special-char-indicator:before {
color: var(--diff-tab-indicator-color);
content: '•';
position: absolute;
}
- .special-char-warning {
+ gr-diff-text .special-char-warning {
/* spacing so elements don't collide */
padding-right: var(--spacing-m);
}
- .special-char-warning:before {
+ gr-diff-text .special-char-warning:before {
color: var(--warning-foreground);
content: '!';
position: absolute;
}
/* Is defined after other background-colors, such that this
rule wins in case of same specificity. */
- .trailing-whitespace,
- .content .contentText .trailing-whitespace,
- .trailing-whitespace .intraline,
- .content .contentText .trailing-whitespace .intraline {
+ td.content div.contentText gr-diff-text .trailing-whitespace,
+ td.content div.contentText gr-diff-text .trailing-whitespace .intraline {
border-radius: var(--border-radius, 4px);
background-color: var(--diff-trailing-whitespace-indicator);
}
- #diffHeader {
- background-color: var(--table-header-background-color);
- border-bottom: 1px solid var(--border-color);
- color: var(--link-color);
- padding: var(--spacing-m) 0 var(--spacing-m) 48px;
- }
- gr-diff-element {
- /* for gr-selection-action-box positioning */
- position: relative;
- }
- #diffTable:focus {
- outline: none;
- }
- #loadingError,
- #sizeWarning {
- display: block;
- margin: var(--spacing-l) auto;
- max-width: 60em;
- text-align: center;
- }
- #loadingError {
- color: var(--error-text-color);
- }
- #sizeWarning gr-button {
- margin: var(--spacing-l);
- }
- .target-row td.blame {
- background: var(--diff-selection-background-color);
- }
- td.lost div {
- background-color: var(--info-background);
- }
- td.lost div.lost-message {
- font-family: var(--font-family, 'Roboto');
- font-size: var(--font-size-normal, 14px);
- line-height: var(--line-height-normal);
- padding: var(--spacing-s) 0;
- }
- td.lost div.lost-message gr-icon {
- padding: 0 var(--spacing-s) 0 var(--spacing-m);
- color: var(--blue-700);
- }
+`;
- col.blame {
- display: none;
- }
- td.blame {
- display: none;
- padding: 0 var(--spacing-m);
- white-space: pre;
- }
- :host(.showBlame) col.blame {
- display: table-column;
- }
- :host(.showBlame) td.blame {
- display: table-cell;
- }
- td.blame > span {
- opacity: 0.6;
- }
- td.blame > span.startOfRange {
- opacity: 1;
- }
- td.blame .blameDate {
- font-family: var(--monospace-font-family);
- color: var(--link-color);
- text-decoration: none;
- }
- .responsive td.blame {
- overflow: hidden;
- width: 200px;
- }
- /** Support the line length indicator **/
- .responsive td.content .contentText {
- /* Same strategy as in
- https://stackoverflow.com/questions/1179928/how-can-i-put-a-vertical-line-down-the-center-of-a-div
- */
- background-image: linear-gradient(
- var(--line-length-indicator-color),
- var(--line-length-indicator-color)
- );
- background-size: 1px 100%;
- background-position: var(--line-limit-marker) 0;
- background-repeat: no-repeat;
+// Styles related to the image diffs.
+export const grDiffImageStyles = css`
+ gr-image-viewer {
+ width: 100%;
+ height: 100%;
+ max-width: var(--image-viewer-max-width, 95vw);
+ max-height: var(--image-viewer-max-height, 90vh);
+ /* Defined by paper-styles default-theme and used in various
+ components. background-color-secondary is a compromise between
+ fairly light in light theme (where we ideally would want
+ background-color-primary) yet slightly offset against the app
+ background in dark mode, where drop shadows e.g. around paper-card
+ are almost invisible. */
+ --primary-background-color: var(--background-color-secondary);
}
- .newlineWarning {
- color: var(--deemphasized-text-color);
+ tbody.image-diff .gr-diff {
text-align: center;
}
- .newlineWarning.hidden {
- display: none;
- }
- .lineNum.COVERED .lineNumButton {
- color: var(
- --coverage-covered-line-num-color,
- var(--deemphasized-text-color)
- );
- background-color: var(--coverage-covered, #e0f2f1);
- }
- .lineNum.NOT_COVERED .lineNumButton {
- color: var(
- --coverage-covered-line-num-color,
- var(--deemphasized-text-color)
- );
- background-color: var(--coverage-not-covered, #ffd1a4);
- }
- .lineNum.PARTIALLY_COVERED .lineNumButton {
- color: var(
- --coverage-covered-line-num-color,
- var(--deemphasized-text-color)
- );
- background: linear-gradient(
- to right bottom,
- var(--coverage-not-covered, #ffd1a4) 0%,
- var(--coverage-not-covered, #ffd1a4) 50%,
- var(--coverage-covered, #e0f2f1) 50%,
- var(--coverage-covered, #e0f2f1) 100%
- );
+ tbody.image-diff img {
+ box-shadow: var(--elevation-level-1);
+ max-width: 50em;
}
-
- // TODO: Investigate whether this CSS is still necessary.
- /* BEGIN: Select and copy for Polymer 2 */
- /* Below was copied and modified from the original css in gr-diff-selection.html. */
- .content,
- .contextControl,
- .blame {
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
+ tbody.image-diff button.right.lineNumButton {
+ border-left: 1px solid var(--border-color);
}
-
- .selected-left:not(.selected-comment)
- .side-by-side
- .left
- + .content
- .contentText,
- .selected-right:not(.selected-comment)
- .side-by-side
- .right
- + .content
- .contentText,
- .selected-left:not(.selected-comment)
- .unified
- .left.lineNum
- ~ .content:not(.both)
- .contentText,
- .selected-right:not(.selected-comment)
- .unified
- .right.lineNum
- ~ .content
- .contentText,
- .selected-left.selected-comment .side-by-side .left + .content .message,
- .selected-right.selected-comment
- .side-by-side
- .right
- + .content
- .message
- :not(.collapsedContent),
- .selected-comment .unified .message :not(.collapsedContent),
- .selected-blame .blame {
- -webkit-user-select: text;
- -moz-user-select: text;
- -ms-user-select: text;
- user-select: text;
+ tbody.image-diff label {
+ font-family: var(--font-family);
+ font-style: italic;
}
-
- /* Make comments and check results selectable when selected */
- .selected-left.selected-comment ::slotted(.comment-thread[diff-side='left']),
- .selected-right.selected-comment
- ::slotted(.comment-thread[diff-side='right']) {
- -webkit-user-select: text;
- -moz-user-select: text;
- -ms-user-select: text;
- user-select: text;
+ .image-diff td.content {
+ background-color: var(--diff-blank-background-color);
}
- /* END: Select and copy for Polymer 2 */
+`;
- .whitespace-change-only-message {
- background-color: var(--diff-context-control-background-color);
- border: 1px solid var(--diff-context-control-border-color);
+// Styles related to the binary diffs.
+export const grDiffBinaryStyles = css`
+ gr-diff-element tbody.binary-diff td {
+ font-family: var(--font-family);
+ font-style: italic;
text-align: center;
- }
-
- .token-highlight {
- background-color: var(--token-highlighting-color, #fffd54);
- }
-
- gr-selection-action-box {
- /* Needs z-index to appear above wrapped content, since it's inserted
- into DOM before it. */
- z-index: 120;
- }
-
- gr-diff-image-new,
- gr-diff-image-old,
- gr-diff-section,
- gr-context-controls-section,
- gr-diff-row {
- display: contents;
+ padding: var(--spacing-s) 0;
}
`;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
index 219f16e8fd..c4aaa2d399 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
@@ -3,7 +3,7 @@
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {CommentRange} from '../../../types/common';
+import {BlameInfo, CommentRange} from '../../../types/common';
import {Side, SpecialFilePath} from '../../../constants/constants';
import {
DiffContextExpandedExternalDetail,
@@ -315,146 +315,6 @@ export function isThreadEl(
);
}
-/**
- * Simple helper method for creating element classes in the context of
- * gr-diff. This is just a super simple convenience function.
- */
-export function diffClasses(...additionalClasses: string[]) {
- return ['gr-diff', ...additionalClasses].join(' ');
-}
-
-/**
- * Simple helper method for creating elements in the context of gr-diff.
- * This is just a super simple convenience function.
- */
-export function createElementDiff(
- tagName: string,
- classStr?: string
-): HTMLElement {
- const el = document.createElement(tagName);
-
- el.classList.add('gr-diff');
- if (classStr) {
- for (const className of classStr.split(' ')) {
- el.classList.add(className);
- }
- }
- return el;
-}
-
-export function createElementDiffWithText(
- tagName: string,
- textContent: string
-) {
- const element = createElementDiff(tagName);
- element.textContent = textContent;
- return element;
-}
-
-export function createLineBreak(mode: DiffResponsiveMode) {
- return isResponsive(mode)
- ? createElementDiff('wbr')
- : createElementDiff('span', 'br');
-}
-
-/**
- * Returns a <span> element holding a '\t' character, that will visually
- * occupy |tabSize| many columns.
- *
- * @param tabSize The effective size of this tab stop.
- */
-export function createTabWrapper(tabSize: number): HTMLElement {
- // Force this to be a number to prevent arbitrary injection.
- const result = createElementDiff('span', 'tab');
- result.setAttribute(
- 'style',
- `tab-size: ${tabSize}; -moz-tab-size: ${tabSize};`
- );
- result.innerText = '\t';
- return result;
-}
-
-/**
- * Returns a 'div' element containing the supplied |text| as its innerText,
- * with '\t' characters expanded to a width determined by |tabSize|, and the
- * text wrapped at column |lineLimit|, which may be Infinity if no wrapping is
- * desired.
- *
- * @param text The text to be formatted.
- * @param responsiveMode The responsive mode of the diff.
- * @param tabSize The width of each tab stop.
- * @param lineLimit The column after which to wrap lines.
- */
-export function formatText(
- text: string,
- responsiveMode: DiffResponsiveMode,
- tabSize: number,
- lineLimit: number,
- elementId: string
-): HTMLElement {
- const contentText = createElementDiff('div', 'contentText');
- // <gr-legacy-text> is not defined anywhere, so this behave just as a <div>
- // would. We use this during the migration to lit based diff elements to
- // match <gr-diff-text>. We define a css rule with `display:contents` making
- // sure that this extra element is basically a no-op.
- const legacyText = document.createElement('gr-legacy-text');
- contentText.appendChild(legacyText);
- contentText.id = elementId;
- let columnPos = 0;
- let textOffset = 0;
- for (const segment of text.split(REGEX_TAB_OR_SURROGATE_PAIR)) {
- if (segment) {
- // |segment| contains only normal characters. If |segment| doesn't fit
- // entirely on the current line, append chunks of |segment| followed by
- // line breaks.
- let rowStart = 0;
- let rowEnd = lineLimit - columnPos;
- while (rowEnd < segment.length) {
- legacyText.appendChild(
- document.createTextNode(segment.substring(rowStart, rowEnd))
- );
- legacyText.appendChild(createLineBreak(responsiveMode));
- columnPos = 0;
- rowStart = rowEnd;
- rowEnd += lineLimit;
- }
- // Append the last part of |segment|, which fits on the current line.
- legacyText.appendChild(
- document.createTextNode(segment.substring(rowStart))
- );
- columnPos += segment.length - rowStart;
- textOffset += segment.length;
- }
- if (textOffset < text.length) {
- // Handle the special character at |textOffset|.
- if (text.startsWith('\t', textOffset)) {
- // Append a single '\t' character.
- let effectiveTabSize = tabSize - (columnPos % tabSize);
- if (columnPos + effectiveTabSize > lineLimit) {
- legacyText.appendChild(createLineBreak(responsiveMode));
- columnPos = 0;
- effectiveTabSize = tabSize;
- }
- legacyText.appendChild(createTabWrapper(effectiveTabSize));
- columnPos += effectiveTabSize;
- textOffset++;
- } else {
- // Append a single surrogate pair.
- if (columnPos >= lineLimit) {
- legacyText.appendChild(createLineBreak(responsiveMode));
- columnPos = 0;
- }
- legacyText.appendChild(
- document.createTextNode(text.substring(textOffset, textOffset + 2))
- );
- textOffset += 2;
- columnPos += 1;
- }
- }
- }
- return contentText;
-}
-
export interface DiffContextExpandedEventDetail
extends DiffContextExpandedExternalDetail {
/** The context control group that should be replaced by `groups`. */
@@ -462,3 +322,10 @@ export interface DiffContextExpandedEventDetail
groups: GrDiffGroup[];
numLines: number;
}
+
+export function findBlame(blameInfos: BlameInfo[], line?: LineNumber) {
+ if (typeof line !== 'number') return undefined;
+ return blameInfos.find(info =>
+ info.ranges.find(range => range.start <= line && line <= range.end)
+ );
+}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
index f425e2b16c..07aeaf0ffd 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
@@ -6,9 +6,6 @@
import {assert} from '@open-wc/testing';
import '../../../test/common-test-setup';
import {
- createElementDiff,
- formatText,
- createTabWrapper,
getRange,
computeKeyLocations,
GrDiffCommentThread,
@@ -23,156 +20,7 @@ import {
import {FILE, LOST, Side} from '../../../api/diff';
import {createDefaultDiffPrefs} from '../../../constants/constants';
-const LINE_BREAK_HTML = '<span class="gr-diff br"></span>';
-
suite('gr-diff-utils tests', () => {
- test('createElementDiff classStr applies all classes', () => {
- const node = createElementDiff('div', 'test classes');
- assert.isTrue(node.classList.contains('gr-diff'));
- assert.isTrue(node.classList.contains('test'));
- assert.isTrue(node.classList.contains('classes'));
- });
-
- test('formatText newlines 1', () => {
- let text = 'abcdef';
-
- assert.equal(
- formatText(text, 'NONE', 4, 10, '').firstElementChild?.innerHTML,
- text
- );
- text = 'a'.repeat(20);
- assert.equal(
- formatText(text, 'NONE', 4, 10, '').firstElementChild?.innerHTML,
- 'a'.repeat(10) + LINE_BREAK_HTML + 'a'.repeat(10)
- );
- });
-
- test('formatText newlines 2', () => {
- const text = '<span class="thumbsup">ðŸ‘</span>';
- assert.equal(
- formatText(text, 'NONE', 4, 10, '').firstElementChild?.innerHTML,
- '&lt;span clas' +
- LINE_BREAK_HTML +
- 's="thumbsu' +
- LINE_BREAK_HTML +
- 'p"&gt;ðŸ‘&lt;/span' +
- LINE_BREAK_HTML +
- '&gt;'
- );
- });
-
- test('formatText newlines 3', () => {
- const text = '01234\t56789';
- assert.equal(
- formatText(text, 'NONE', 4, 10, '').firstElementChild?.innerHTML,
- '01234' + createTabWrapper(3).outerHTML + '56' + LINE_BREAK_HTML + '789'
- );
- });
-
- test('formatText newlines 4', () => {
- const text = 'ðŸ‘'.repeat(58);
- assert.equal(
- formatText(text, 'NONE', 4, 20, '').firstElementChild?.innerHTML,
- 'ðŸ‘'.repeat(20) +
- LINE_BREAK_HTML +
- 'ðŸ‘'.repeat(20) +
- LINE_BREAK_HTML +
- 'ðŸ‘'.repeat(18)
- );
- });
-
- test('tab wrapper style', () => {
- const pattern = new RegExp(
- '^<span class="gr-diff tab" ' +
- 'style="((?:-moz-)?tab-size: (\\d+);.?)+">\\t<\\/span>$'
- );
-
- for (const size of [1, 3, 8, 55]) {
- const html = createTabWrapper(size).outerHTML;
- assert.match(html, pattern);
- assert.equal(html.match(pattern)?.[2], size.toString());
- }
- });
-
- test('tab wrapper insertion', () => {
- const html = 'abc\tdef';
- const tabSize = 8;
- const wrapper = createTabWrapper(tabSize - 3);
- assert.ok(wrapper);
- assert.equal(wrapper.innerText, '\t');
- assert.equal(
- formatText(html, 'NONE', tabSize, Infinity, '').firstElementChild
- ?.innerHTML,
- 'abc' + wrapper.outerHTML + 'def'
- );
- });
-
- test('escaping HTML', () => {
- let input = '<script>alert("XSS");<' + '/script>';
- let expected = '&lt;script&gt;alert("XSS");&lt;/script&gt;';
-
- let result = formatText(input, 'NONE', 1, Number.POSITIVE_INFINITY, '')
- .firstElementChild?.innerHTML;
- assert.equal(result, expected);
-
- input = '& < > " \' / `';
- expected = '&amp; &lt; &gt; " \' / `';
- result = formatText(input, 'NONE', 1, Number.POSITIVE_INFINITY, '')
- .firstElementChild?.innerHTML;
- assert.equal(result, expected);
- });
-
- test('text length with tabs and unicode', () => {
- function expectTextLength(text: string, tabSize: number, expected: number) {
- // Formatting to |expected| columns should not introduce line breaks.
- const result = formatText(text, 'NONE', tabSize, expected, '')
- .firstElementChild!;
- assert.isNotOk(
- result.querySelector('.contentText > .br'),
- ' Expected the result of: \n' +
- ` _formatText(${text}', 'NONE', ${tabSize}, ${expected})\n` +
- ' to not contain a br. But the actual result HTML was:\n' +
- ` '${result.innerHTML}'\nwhereupon`
- );
-
- // Increasing the line limit should produce the same markup.
- assert.equal(
- formatText(text, 'NONE', tabSize, Infinity, '').firstElementChild
- ?.innerHTML,
- result.innerHTML
- );
- assert.equal(
- formatText(text, 'NONE', tabSize, expected + 1, '').firstElementChild
- ?.innerHTML,
- result.innerHTML
- );
-
- // Decreasing the line limit should introduce line breaks.
- if (expected > 0) {
- const tooSmall = formatText(text, 'NONE', tabSize, expected - 1, '')
- .firstElementChild!;
- assert.isOk(
- tooSmall.querySelector('.contentText .br'),
- ' Expected the result of: \n' +
- ` _formatText(${text}', ${tabSize}, ${expected - 1})\n` +
- ' to contain a br. But the actual result HTML was:\n' +
- ` '${tooSmall.innerHTML}'\nwhereupon`
- );
- }
- }
- expectTextLength('12345', 4, 5);
- expectTextLength('\t\t12', 4, 10);
- expectTextLength('abc💢123', 4, 7);
- expectTextLength('abc\t', 8, 8);
- expectTextLength('abc\t\t', 10, 20);
- expectTextLength('', 10, 0);
- // 17 Thai combining chars.
- expectTextLength('à¸à¹‰à¹‰à¹‰à¹‰à¹‰à¹‰à¹‰à¹‰à¹‰à¹‰à¹‰à¹‰à¹‰à¹‰à¹‰à¹‰', 4, 17);
- expectTextLength('abc\tde', 10, 12);
- expectTextLength('abc\tde\t', 10, 20);
- expectTextLength('\t\t\t\t\t', 20, 100);
- });
-
test('getRange returns undefined with start_line = 0', () => {
const range = {
start_line: 0,
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
index a0b579abac..90b9cfef54 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -59,7 +59,20 @@ import {grRangedCommentTheme} from '../gr-ranged-comment-themes/gr-ranged-commen
import {iconStyles} from '../../../styles/gr-icon-styles';
import {DiffModel, diffModelToken} from '../gr-diff-model/gr-diff-model';
import {provide} from '../../../models/dependency';
-import {grDiffStyles} from './gr-diff-styles';
+import {
+ grDiffBinaryStyles,
+ grDiffContextControlsSectionStyles,
+ grDiffElementStyles,
+ grDiffIgnoredWhitespaceStyles,
+ grDiffImageStyles,
+ grDiffMoveStyles,
+ grDiffRebaseStyles,
+ grDiffRowStyles,
+ grDiffSectionStyles,
+ grDiffSelectionStyles,
+ grDiffStyles,
+ grDiffTextStyles,
+} from './gr-diff-styles';
import {GrCoverageLayer} from '../gr-coverage-layer/gr-coverage-layer';
import {
GrAnnotationImpl,
@@ -277,6 +290,17 @@ export class GrDiff extends LitElement implements GrDiffApi {
grSyntaxTheme,
grRangedCommentTheme,
grDiffStyles,
+ grDiffElementStyles,
+ grDiffSectionStyles,
+ grDiffContextControlsSectionStyles,
+ grDiffRowStyles,
+ grDiffIgnoredWhitespaceStyles,
+ grDiffMoveStyles,
+ grDiffRebaseStyles,
+ grDiffSelectionStyles,
+ grDiffTextStyles,
+ grDiffImageStyles,
+ grDiffBinaryStyles,
];
}
@@ -392,7 +416,7 @@ export class GrDiff extends LitElement implements GrDiffApi {
this.layersChanged();
}
if (changedProperties.has('blame')) {
- this.blameChanged();
+ this.diffModel.updateState({blameInfo: this.blame ?? []});
}
if (changedProperties.has('renderPrefs')) {
this.renderPrefsChanged();
@@ -508,15 +532,6 @@ export class GrDiff extends LitElement implements GrDiffApi {
return !!this.highlights.selectedRange;
}
- private blameChanged() {
- this.setBlame(this.blame ?? []);
- if (this.blame) {
- this.classList.add('showBlame');
- } else {
- this.classList.remove('showBlame');
- }
- }
-
// Private but used in tests.
selectLine(el: Element) {
const lineNumber = Number(el.getAttribute('data-value'));
@@ -545,8 +560,6 @@ export class GrDiff extends LitElement implements GrDiffApi {
private prefsChanged() {
if (!this.prefs) return;
-
- this.blame = null;
this.updatePreferenceStyles();
if (!Number.isInteger(this.prefs.tab_size) || this.prefs.tab_size <= 0) {
@@ -796,7 +809,7 @@ export class GrDiff extends LitElement implements GrDiffApi {
// differences to highlight and apply them to the element as
// annotations.
annotate(contentEl: HTMLElement, _: HTMLElement, line: GrDiffLine) {
- const HL_CLASS = 'gr-diff intraline';
+ const HL_CLASS = 'intraline';
for (const highlight of line.highlights) {
// The start and end indices could be the same if a highlight is
// meant to start at the end of a line and continue onto the
@@ -865,7 +878,7 @@ export class GrDiff extends LitElement implements GrDiffApi {
contentEl,
index,
length,
- 'gr-diff trailing-whitespace'
+ 'trailing-whitespace'
);
}
},
@@ -963,21 +976,6 @@ export class GrDiff extends LitElement implements GrDiffApi {
.slice(startIndex, endIndex + 1)
.filter(group => group.lines.length > 0);
}
-
- /**
- * Set the blame information for the diff. For any already-rendered line,
- * re-render its blame cell content.
- */
- setBlame(blame: BlameInfo[]) {
- for (const blameInfo of blame) {
- for (const range of blameInfo.ranges) {
- for (let line = range.start; line <= range.end; line++) {
- const row = this.findRow(Side.LEFT, line);
- if (row) row.blameInfo = blameInfo;
- }
- }
- }
- }
}
function getLineNumberCellWidth(prefs: DiffPreferencesInfo) {
@@ -998,7 +996,7 @@ function annotateSymbols(
// Skip forward by the length of the content
pos += split[i].length;
- GrAnnotationImpl.annotateElement(contentEl, pos, 1, `gr-diff ${className}`);
+ GrAnnotationImpl.annotateElement(contentEl, pos, 1, className);
pos++;
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
index bceafa31b2..5fa478813c 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
@@ -190,13 +190,13 @@ suite('gr-diff tests', () => {
element.renderPrefs = {hide_left_side: true};
await element.updateComplete;
let cols = queryAll(element, 'col');
- assert.equal(cols.length, 3);
+ assert.equal(cols.length, 2);
diffModel.updateState({renderPrefs: {hide_left_side: false}});
element.renderPrefs = {hide_left_side: false};
await element.updateComplete;
cols = queryAll(element, 'col');
- assert.equal(cols.length, 5);
+ assert.equal(cols.length, 4);
});
suite('getCursorStops', () => {
@@ -333,32 +333,6 @@ suite('gr-diff tests', () => {
});
});
- suite('blame', () => {
- test('unsetting', async () => {
- element.blame = [];
- const setBlameSpy = sinon.spy(element, 'setBlame');
- element.classList.add('showBlame');
- element.blame = null;
- await element.updateComplete;
- assert.isTrue(setBlameSpy.calledWithExactly([]));
- assert.isFalse(element.classList.contains('showBlame'));
- });
-
- test('setting', async () => {
- element.blame = [
- {
- author: 'test-author',
- time: 12345,
- commit_msg: '',
- id: 'commit id',
- ranges: [{start: 1, end: 2}],
- },
- ];
- await element.updateComplete;
- assert.isTrue(element.classList.contains('showBlame'));
- });
- });
-
const setupSampleDiff = async function (params: {
content: DiffContent[];
ignore_whitespace?: IgnoreWhitespaceType;
diff --git a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
index 4d9c71a29d..fa9a782817 100644
--- a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
+++ b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
@@ -49,8 +49,8 @@ type RangesMap = {
[side in Side]: LinesMap;
};
-const RANGE_BASE_ONLY = 'gr-diff range';
-const RANGE_HIGHLIGHT = 'gr-diff range rangeHighlight';
+const RANGE_BASE_ONLY = 'range';
+const RANGE_HIGHLIGHT = 'range rangeHighlight';
// Note that there is also `rangeHoverHighlight` being set by GrDiffHighlight.
/**
diff --git a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts
index 7c25eeb968..338ac07bfb 100644
--- a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts
@@ -159,10 +159,7 @@ suite('gr-ranged-comment-layer', () => {
assert.equal(lastCall.args[0], el);
assert.equal(lastCall.args[1], expectedStart);
assert.equal(lastCall.args[2], expectedLength);
- assert.equal(
- lastCall.args[3],
- 'gr-diff range rangeHighlight generated_a'
- );
+ assert.equal(lastCall.args[3], 'range rangeHighlight generated_a');
});
test('type=Both has-comment', () => {
@@ -179,10 +176,7 @@ suite('gr-ranged-comment-layer', () => {
assert.equal(lastCall.args[0], el);
assert.equal(lastCall.args[1], expectedStart);
assert.equal(lastCall.args[2], expectedLength);
- assert.equal(
- lastCall.args[3],
- 'gr-diff range rangeHighlight generated_a'
- );
+ assert.equal(lastCall.args[3], 'range rangeHighlight generated_a');
});
test('type=Both has-comment off side', () => {
@@ -210,10 +204,7 @@ suite('gr-ranged-comment-layer', () => {
assert.equal(lastCall.args[0], el);
assert.equal(lastCall.args[1], expectedStart);
assert.equal(lastCall.args[2], expectedLength);
- assert.equal(
- lastCall.args[3],
- 'gr-diff range rangeHighlight generated_b'
- );
+ assert.equal(lastCall.args[3], 'range rangeHighlight generated_b');
});
test('long range comment', () => {
@@ -226,7 +217,7 @@ suite('gr-ranged-comment-layer', () => {
assert.isTrue(annotateElementStub.called);
assert.equal(
annotateElementStub.lastCall.args[3],
- 'gr-diff range generated_right-60-1-71-1'
+ 'range generated_right-60-1-71-1'
);
});
diff --git a/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts b/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts
index cd10f4d819..120aa2b119 100644
--- a/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts
+++ b/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts
@@ -68,6 +68,8 @@ export class GrSelectionActionBox extends LitElement {
`;
}
+ // TODO(b/315277651): This is very similar in purpose to gr-tooltip-content.
+ // We should figure out a way to reuse as much of the logic as possible.
async placeAbove(el: Text | Element | Range) {
if (!this.tooltip) return;
await this.tooltip.updateComplete;
diff --git a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
index 4e166ba009..50742903de 100644
--- a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
+++ b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
@@ -77,6 +77,7 @@ const LANGUAGE_MAP = new Map<string, string>([
['text/x-puppet', 'puppet'],
['text/x-python', 'python'],
['text/x-q', 'q'],
+ ['text/x-qml', 'qml'],
['text/x-ruby', 'ruby'],
['text/x-rustsrc', 'rust'],
['text/x-scala', 'scala'],
@@ -99,7 +100,7 @@ const LANGUAGE_MAP = new Map<string, string>([
['text/vbscript', 'vbscript'],
]);
-const CLASS_PREFIX = 'gr-diff gr-syntax gr-syntax-';
+const CLASS_PREFIX = 'gr-syntax gr-syntax-';
const CLASS_SAFELIST = new Set<string>([
'attr',
diff --git a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts
index c6c46f95d8..221eada3e6 100644
--- a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts
@@ -139,7 +139,7 @@ suite('gr-syntax-layer-worker tests', () => {
const el = annotate(Side.LEFT, 1, 'import it;');
assert.equal(
el.innerHTML,
- '<hl class="gr-diff gr-syntax gr-syntax-literal">import</hl> it;'
+ '<hl class="gr-syntax gr-syntax-literal">import</hl> it;'
);
assert.equal(listener.callCount, 2);
assert.equal(listener.getCall(0).args[0], 1);
@@ -155,9 +155,9 @@ suite('gr-syntax-layer-worker tests', () => {
const el = annotate(Side.RIGHT, 3, ' public static final {');
assert.equal(
el.innerHTML,
- ' <hl class="gr-diff gr-syntax gr-syntax-literal">public</hl> ' +
- '<hl class="gr-diff gr-syntax gr-syntax-keyword">static</hl> ' +
- '<hl class="gr-diff gr-syntax gr-syntax-name">final</hl> {'
+ ' <hl class="gr-syntax gr-syntax-literal">public</hl> ' +
+ '<hl class="gr-syntax gr-syntax-keyword">static</hl> ' +
+ '<hl class="gr-syntax gr-syntax-name">final</hl> {'
);
assert.equal(listener.callCount, 2);
});
diff --git a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
index 26003e197a..a0709440d0 100644
--- a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
+++ b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
@@ -27,6 +27,8 @@ import {
import {getUserId} from '../../utils/account-util';
import {getChangeNumber} from '../../utils/change-util';
import {deepEqual} from '../../utils/deep-util';
+import {throwingErrorCallback} from '../../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {assert} from '../../utils/common-util';
export const bulkActionsModelToken =
define<BulkActionsModel>('bulk-actions-model');
@@ -126,7 +128,7 @@ export class BulkActionsModel extends Model<BulkActionsState> {
reason?: string,
// errorFn is needed to avoid showing an error dialog
errFn?: (changeNum: NumericChangeId) => void
- ): Promise<Response | undefined>[] {
+ ): Promise<Response>[] {
const current = this.getState();
return current.selectedChangeNums.map(changeNum => {
if (!current.allChanges.get(changeNum))
@@ -207,10 +209,16 @@ export class BulkActionsModel extends Model<BulkActionsState> {
const current = this.getState();
return current.selectedChangeNums.map(changeNum =>
this.restApiService
- .setChangeHashtag(changeNum, {
- add: hashtags,
- })
+ .setChangeHashtag(
+ changeNum,
+ {
+ add: hashtags,
+ },
+ throwingErrorCallback
+ )
.then(responseHashtags => {
+ // With throwingErrorCallback guaranteed to be non-null.
+ assert(!!responseHashtags, 'setChangeHastag returned undefined');
// Once we get server confirmation that the hashtags were added to the
// change, we are updating the model's ChangeInfo. This way we can
// keep the page state (dialog status) but use the updated change info
diff --git a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
index ece126c33f..1ac959306e 100644
--- a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
+++ b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
@@ -223,7 +223,7 @@ suite('bulk actions model test', () => {
});
test('already abandoned change does not call executeChangeAction', () => {
- const actionStub = stubRestApi('executeChangeAction');
+ const actionStub = stubRestApi('executeChangeAction').resolves();
bulkActionsModel.abandonChanges();
assert.equal(actionStub.callCount, 1);
assert.deepEqual(actionStub.lastCall.args.slice(0, 5), [
diff --git a/polygerrit-ui/app/models/change/files-model.ts b/polygerrit-ui/app/models/change/files-model.ts
index 192520d40e..9e8718d54a 100644
--- a/polygerrit-ui/app/models/change/files-model.ts
+++ b/polygerrit-ui/app/models/change/files-model.ts
@@ -12,7 +12,7 @@ import {
PatchSetNumber,
RevisionPatchSetNum,
} from '../../types/common';
-import {combineLatest, of, from} from 'rxjs';
+import {combineLatest, of, from, Observable} from 'rxjs';
import {switchMap, map} from 'rxjs/operators';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
import {select} from '../../utils/observable-util';
@@ -120,6 +120,11 @@ export const filesModelToken = define<FilesModel>('files-model');
export class FilesModel extends Model<FilesState> {
public readonly files$ = select(this.state$, state => state.files);
+ public file$ = (path$: Observable<string | undefined>) =>
+ combineLatest([path$, this.files$]).pipe(
+ map(([path, files]) => files.find(f => f.__path === path))
+ );
+
/**
* `files$` only includes the files that were modified. Here we also include
* all unmodified files that have comments with
diff --git a/polygerrit-ui/app/models/checks/checks-util.ts b/polygerrit-ui/app/models/checks/checks-util.ts
index a567fb53a8..fc18968b4e 100644
--- a/polygerrit-ui/app/models/checks/checks-util.ts
+++ b/polygerrit-ui/app/models/checks/checks-util.ts
@@ -14,13 +14,13 @@ import {
Replacement,
RunStatus,
} from '../../api/checks';
-import {PatchSetNumber, RevisionPatchSetNum} from '../../api/rest-api';
-import {CommentSide} from '../../constants/constants';
import {
- FixSuggestionInfo,
+ PatchSetNumber,
+ RevisionPatchSetNum,
FixReplacementInfo,
- DraftInfo,
-} from '../../types/common';
+} from '../../api/rest-api';
+import {CommentSide} from '../../constants/constants';
+import {FixSuggestionInfo, DraftInfo} from '../../types/common';
import {OpenFixPreviewEventDetail} from '../../types/events';
import {isDefined} from '../../types/types';
import {PROVIDED_FIX_ID, createNew} from '../../utils/comment-util';
@@ -145,7 +145,7 @@ export function rectifyFix(
if (replacements.length === 0) return undefined;
return {
- description: fix.description ?? `Fix provided by ${checkName}`,
+ description: fix.description || `Fix provided by ${checkName}`,
fix_id: PROVIDED_FIX_ID,
replacements,
};
@@ -520,7 +520,6 @@ export function secondaryLinks(result?: CheckResultApi): Link[] {
}
export function computeIsExpandable(result?: CheckResultApi) {
- if (!result?.summary) return false;
const hasMessage = !!result?.message;
const hasMultipleLinks = (result?.links ?? []).length > 1;
const hasPointers = (result?.codePointers ?? []).length > 0;
diff --git a/polygerrit-ui/app/models/checks/checks-util_test.ts b/polygerrit-ui/app/models/checks/checks-util_test.ts
index 83b4cd6b1b..3b5c1082e5 100644
--- a/polygerrit-ui/app/models/checks/checks-util_test.ts
+++ b/polygerrit-ui/app/models/checks/checks-util_test.ts
@@ -93,6 +93,29 @@ suite('checks-util tests', () => {
assert.equal(rectified?.fix_id, PROVIDED_FIX_ID);
});
+ test('rectifyFix changes description when description is empty', () => {
+ const rectified = rectifyFix(
+ {
+ replacements: [
+ {
+ path: 'test-path',
+ range: {
+ start_line: 1,
+ end_line: 1,
+ start_character: 0,
+ end_character: 1,
+ } as CommentRange,
+ replacement: 'test-replacement-string',
+ },
+ ],
+ description: '',
+ },
+ 'test-check-name'
+ );
+ assert.isDefined(rectified);
+ assert.equal(rectified?.description, 'Fix provided by test-check-name');
+ });
+
test('sortAttemptChoices', () => {
const unsorted: (AttemptChoice | undefined)[] = [
3,
@@ -121,7 +144,7 @@ suite('checks-util tests', () => {
});
test('no summary', () => {
- assert.isFalse(
+ assert.isTrue(
computeIsExpandable({
...createCheckResult(),
message: 'asdf',
diff --git a/polygerrit-ui/app/models/comments/comments-model.ts b/polygerrit-ui/app/models/comments/comments-model.ts
index e61b88be9c..f9a8401319 100644
--- a/polygerrit-ui/app/models/comments/comments-model.ts
+++ b/polygerrit-ui/app/models/comments/comments-model.ts
@@ -7,7 +7,6 @@ import {ChangeComments} from '../../elements/diff/gr-comment-api/gr-comment-api'
import {
CommentInfo,
NumericChangeId,
- PatchSetNum,
RevisionId,
UrlEncodedCommentId,
RobotCommentInfo,
@@ -26,15 +25,25 @@ import {
convertToCommentInput,
createNew,
createNewPatchsetLevel,
+ getFirstComment,
+ hasSuggestion,
id,
isDraftThread,
isNewThread,
+ isUnresolved,
reportingDetails,
} from '../../utils/comment-util';
import {deepEqual} from '../../utils/deep-util';
import {select} from '../../utils/observable-util';
import {define} from '../dependency';
-import {combineLatest, forkJoin, from, Observable, of} from 'rxjs';
+import {
+ BehaviorSubject,
+ combineLatest,
+ forkJoin,
+ from,
+ Observable,
+ of,
+} from 'rxjs';
import {fire, fireAlert} from '../../utils/event-util';
import {CURRENT} from '../../utils/patch-set-util';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
@@ -57,6 +66,7 @@ import {
import {isDefined} from '../../types/types';
import {ChangeViewModel} from '../views/change';
import {NavigationService} from '../../elements/core/gr-navigation/gr-navigation';
+import {readJSONResponsePayload} from '../../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
export interface CommentState {
/** undefined means 'still loading' */
@@ -68,9 +78,20 @@ export interface CommentState {
drafts?: {[path: string]: DraftInfo[]};
// Ported comments only affect `CommentThread` properties, not individual
// comments.
- /** undefined means 'still loading' */
+ /**
+ * Comments ported from earlier patchsets.
+ *
+ * This only considers current patchset (right side), not the base patchset
+ * (left-side).
+ *
+ * undefined means 'still loading'
+ */
portedComments?: {[path: string]: CommentInfo[]};
- /** undefined means 'still loading' */
+ /**
+ * Drafts ported from earlier patchsets.
+ *
+ * undefined means 'still loading'
+ */
portedDrafts?: {[path: string]: DraftInfo[]};
/**
* If a draft is discarded by the user, then we temporarily keep it in this
@@ -215,7 +236,7 @@ export function deleteDiscardedDraft(
return nextState;
}
-/** Adds or updates a draft. */
+/** Adds or updates a draft in the state. */
export function setDraft(state: CommentState, draft: DraftInfo): CommentState {
const nextState = {...state};
assert(!!draft.path, 'draft without path');
@@ -234,6 +255,12 @@ export function setDraft(state: CommentState, draft: DraftInfo): CommentState {
return nextState;
}
+/** Removes a draft from the state.
+ *
+ * Removed draft is stored in discardedDrafts for potential undo operation.
+ * discardedDrafts however is only a client-side cache and such drafts are not
+ * retained in the server.
+ */
export function deleteDraft(
state: CommentState,
draft: DraftInfo
@@ -253,6 +280,10 @@ export function deleteDraft(
}
export const commentsModelToken = define<CommentsModel>('comments-model');
+/**
+ * Model that maintains the state of all comments and drafts for the current
+ * change in the context of change-view.
+ */
export class CommentsModel extends Model<CommentState> {
public readonly commentsLoading$ = select(
this.state$,
@@ -401,6 +432,17 @@ export class CommentsModel extends Model<CommentState> {
threads.filter(t => !isNewThread(t) && isDraftThread(t))
);
+ public readonly threadsWithSuggestions$ = select(
+ combineLatest([this.threads$, this.changeModel.latestPatchNum$]),
+ ([threads, latestPs]) =>
+ threads.filter(
+ t =>
+ isUnresolved(t) &&
+ hasSuggestion(t) &&
+ getFirstComment(t)?.patch_set === latestPs
+ )
+ );
+
public readonly commentedPaths$ = select(
combineLatest([
this.changeComments$,
@@ -414,6 +456,8 @@ export class CommentsModel extends Model<CommentState> {
}
);
+ public readonly reloadAllComments$ = new BehaviorSubject(undefined);
+
public thread$(id: UrlEncodedCommentId) {
return select(this.threads$, threads => threads.find(t => t.rootId === id));
}
@@ -422,8 +466,6 @@ export class CommentsModel extends Model<CommentState> {
private changeNum?: NumericChangeId;
- private patchNum?: PatchSetNum;
-
private drafts: {[path: string]: DraftInfo[]} = {};
private draftToastTask?: DelayedTask;
@@ -463,9 +505,8 @@ export class CommentsModel extends Model<CommentState> {
this.subscriptions.push(
this.drafts$.subscribe(x => (this.drafts = x ?? {}))
);
- this.subscriptions.push(
- this.changeModel.patchNum$.subscribe(x => (this.patchNum = x))
- );
+ // Patchset-level draft should always exist when opening reply dialog.
+ // If there are none, create an empty one.
this.subscriptions.push(
combineLatest([
this.draftsLoading$,
@@ -477,41 +518,55 @@ export class CommentsModel extends Model<CommentState> {
})
);
this.subscriptions.push(
- this.changeViewModel.changeNum$.subscribe(changeNum => {
- this.changeNum = changeNum;
- this.setState({...initialState});
- this.reloadAllComments();
- })
+ combineLatest([this.changeViewModel.changeNum$, this.reloadAllComments$])
+ .pipe(
+ switchMap(([changeNum, _]) => {
+ this.changeNum = changeNum;
+ this.setState({...initialState});
+ if (!changeNum) return of([undefined, undefined, undefined]);
+ return forkJoin([
+ this.restApiService.getDiffComments(changeNum),
+ this.restApiService.getDiffRobotComments(changeNum),
+ this.restApiService.getDiffDrafts(changeNum),
+ ]);
+ })
+ )
+ .subscribe(([comments, robotComments, drafts]) => {
+ this.reportRobotCommentStats(robotComments);
+ this.modifyState(s => {
+ s = setComments(s, comments);
+ s = setRobotComments(s, robotComments);
+ return setDrafts(s, drafts);
+ });
+ })
);
+ // When the patchset selection changes update information about comments
+ // ported from earlier patchsets.
this.subscriptions.push(
- combineLatest([
- this.changeModel.changeNum$,
- this.changeModel.patchNum$,
- ]).subscribe(([changeNum, patchNum]) => {
- this.changeNum = changeNum;
- this.patchNum = patchNum;
- this.reloadAllPortedComments();
- })
+ combineLatest([this.changeModel.changeNum$, this.changeModel.patchNum$])
+ .pipe(
+ switchMap(([changeNum, patchNum]) => {
+ this.changeNum = changeNum;
+ if (!changeNum) return of([undefined, undefined]);
+ const revision = patchNum ?? (CURRENT as RevisionId);
+ return forkJoin([
+ this.restApiService.getPortedComments(changeNum, revision),
+ this.restApiService.getPortedDrafts(changeNum, revision),
+ ]);
+ })
+ )
+ .subscribe(([portedComments, portedDrafts]) =>
+ this.modifyState(s => {
+ s = setPortedComments(s, portedComments);
+ return setPortedDrafts(s, portedDrafts);
+ })
+ )
);
}
// Note that this does *not* reload ported comments.
- async reloadAllComments() {
- if (!this.changeNum) return;
- await Promise.all([
- this.reloadComments(this.changeNum),
- this.reloadRobotComments(this.changeNum),
- this.reloadDrafts(this.changeNum),
- ]);
- }
-
- async reloadAllPortedComments() {
- if (!this.changeNum) return;
- if (!this.patchNum) return;
- await Promise.all([
- this.reloadPortedComments(this.changeNum, this.patchNum),
- this.reloadPortedDrafts(this.changeNum, this.patchNum),
- ]);
+ reloadAllComments() {
+ this.reloadAllComments$.next(undefined);
}
// visible for testing
@@ -519,19 +574,6 @@ export class CommentsModel extends Model<CommentState> {
this.setState(reducer({...this.getState()}));
}
- async reloadComments(changeNum: NumericChangeId): Promise<void> {
- const comments = await this.restApiService.getDiffComments(changeNum);
- this.modifyState(s => setComments(s, comments));
- }
-
- async reloadRobotComments(changeNum: NumericChangeId): Promise<void> {
- const robotComments = await this.restApiService.getDiffRobotComments(
- changeNum
- );
- this.reportRobotCommentStats(robotComments);
- this.modifyState(s => setRobotComments(s, robotComments));
- }
-
private reportRobotCommentStats(obj?: PathToRobotCommentsInfoMap) {
if (!obj) return;
const comments = Object.values(obj).flat();
@@ -560,33 +602,6 @@ export class CommentsModel extends Model<CommentState> {
);
}
- async reloadDrafts(changeNum: NumericChangeId): Promise<void> {
- const drafts = await this.restApiService.getDiffDrafts(changeNum);
- this.modifyState(s => setDrafts(s, drafts));
- }
-
- async reloadPortedComments(
- changeNum: NumericChangeId,
- patchNum = CURRENT as RevisionId
- ): Promise<void> {
- const portedComments = await this.restApiService.getPortedComments(
- changeNum,
- patchNum
- );
- this.modifyState(s => setPortedComments(s, portedComments));
- }
-
- async reloadPortedDrafts(
- changeNum: NumericChangeId,
- patchNum = CURRENT as RevisionId
- ): Promise<void> {
- const portedDrafts = await this.restApiService.getPortedDrafts(
- changeNum,
- patchNum
- );
- this.modifyState(s => setPortedDrafts(s, portedDrafts));
- }
-
async restoreDraft(draftId: UrlEncodedCommentId) {
const found = this.discardedDrafts?.find(d => id(d) === draftId);
if (!found) throw new Error('discarded draft not found');
@@ -643,9 +658,8 @@ export class CommentsModel extends Model<CommentState> {
);
if (changeNum !== this.changeNum) return draft;
if (!result.ok) throw new Error('request failed');
- savedComment = (await this.restApiService.getResponseObject(
- result
- )) as unknown as CommentInfo;
+ savedComment = (await readJSONResponsePayload(result))
+ .parsed as unknown as CommentInfo;
} catch (error) {
if (showToast) this.handleFailedDraftRequest();
const draftError: DraftInfo = {...draft, savingState: SavingState.ERROR};
@@ -727,7 +741,10 @@ export class CommentsModel extends Model<CommentState> {
comment.id,
reason
);
- this.modifyState(s => updateComment(s, newComment));
+ // Don't update state on server error.
+ if (newComment) {
+ this.modifyState(s => updateComment(s, newComment));
+ }
}
private report(interaction: Interaction, comment: Comment) {
diff --git a/polygerrit-ui/app/models/plugins/plugins-model.ts b/polygerrit-ui/app/models/plugins/plugins-model.ts
index 33ec35a53e..8e9bead7ea 100644
--- a/polygerrit-ui/app/models/plugins/plugins-model.ts
+++ b/polygerrit-ui/app/models/plugins/plugins-model.ts
@@ -45,6 +45,11 @@ export interface ChecksUpdate {
/** Application wide state of plugins. */
interface PluginsState {
/**
+ * Initially false. Becomes true, if either all plugins were loaded, or if
+ * loading plugins has timed out. Once true, it will not change again.
+ */
+ pluginsLoaded: boolean;
+ /**
* List of plugins that have called annotationApi().setCoverageProvider().
*/
coveragePlugins: CoveragePlugin[];
@@ -84,8 +89,11 @@ export class PluginsModel extends Model<PluginsState> {
public coveragePlugins$ = select(this.state$, state => state.coveragePlugins);
+ public pluginsLoaded$ = select(this.state$, state => state.pluginsLoaded);
+
constructor() {
super({
+ pluginsLoaded: false,
coveragePlugins: [],
checksPlugins: [],
suggestionsPlugins: [],
diff --git a/polygerrit-ui/app/models/user/user-model.ts b/polygerrit-ui/app/models/user/user-model.ts
index 55d387b576..cd6a66a1f0 100644
--- a/polygerrit-ui/app/models/user/user-model.ts
+++ b/polygerrit-ui/app/models/user/user-model.ts
@@ -29,6 +29,7 @@ import {select} from '../../utils/observable-util';
import {define} from '../dependency';
import {Model} from '../base/model';
import {isDefined} from '../../types/types';
+import {readJSONResponsePayload} from '../../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
export function changeTablePrefs(prefs: Partial<PreferencesInfo>) {
const cols = prefs.change_table ?? [];
@@ -214,8 +215,8 @@ export class UserModel extends Model<UserState> {
return this.restApiService
.saveDiffPreferences(diffPrefs)
.then((response: Response) =>
- this.restApiService.getResponseObject(response).then(obj => {
- const newPrefs = obj as unknown as DiffPreferencesInfo;
+ readJSONResponsePayload(response).then(obj => {
+ const newPrefs = obj.parsed as unknown as DiffPreferencesInfo;
if (!newPrefs) return;
this.setDiffPreferences(newPrefs);
})
@@ -226,8 +227,8 @@ export class UserModel extends Model<UserState> {
return this.restApiService
.saveEditPreferences(editPrefs)
.then((response: Response) => {
- this.restApiService.getResponseObject(response).then(obj => {
- const newPrefs = obj as unknown as EditPreferencesInfo;
+ readJSONResponsePayload(response).then(obj => {
+ const newPrefs = obj.parsed as unknown as EditPreferencesInfo;
if (!newPrefs) return;
this.setEditPreferences(newPrefs);
});
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index a5dc4215c5..9004eecc8b 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -30,21 +30,21 @@
"@polymer/paper-toggle-button": "^3.0.1",
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "^3.5.1",
- "@types/resemblejs": "^4.1.0",
- "@types/resize-observer-browser": "^0.1.7",
+ "@types/resemblejs": "^4.1.3",
+ "@types/resize-observer-browser": "^0.1.11",
"@webcomponents/shadycss": "^1.11.2",
"@webcomponents/webcomponentsjs": "^1.3.3",
- "highlight.js": "^11.8.0",
+ "highlight.js": "^11.9.0",
"highlightjs-closure-templates": "https://github.com/highlightjs/highlightjs-closure-templates",
"highlightjs-structured-text": "https://github.com/highlightjs/highlightjs-structured-text",
"immer": "^9.0.21",
- "lit": "^3.0.0",
+ "lit": "^3.1.0",
"polymer-bridges": "file:../../polymer-bridges",
"polymer-resin": "^2.0.1",
"resemblejs": "^5.0.0",
"rxjs": "^6.6.7",
"safevalues": "0.3.1",
- "web-vitals": "^3.4.0"
+ "web-vitals": "^3.5.1"
},
"dependencies // comments": {
"safevalues": [
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
index 9ab0f64b03..bdfb870fd9 100644
--- a/polygerrit-ui/app/rules.bzl
+++ b/polygerrit-ui/app/rules.bzl
@@ -1,5 +1,5 @@
-load("//tools/bzl:genrule2.bzl", "genrule2")
load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
+load("//tools/bzl:genrule2.bzl", "genrule2")
def polygerrit_bundle(name, srcs, outs, entry_point, app_name):
"""Build .zip bundle from source code
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index b7d36f4983..b59d7a8b33 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -20,5 +20,6 @@ export enum KnownExperimentId {
PUSH_NOTIFICATIONS_DEVELOPER = 'UiFeature__push_notifications_developer',
PUSH_NOTIFICATIONS = 'UiFeature__push_notifications',
ML_SUGGESTED_EDIT = 'UiFeature__ml_suggested_edit',
+ ML_SUGGESTED_EDIT_V2 = 'UiFeature__ml_suggested_edit_v2',
REVISION_PARENTS_DATA = 'UiFeature__revision_parents_data',
}
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth.ts b/polygerrit-ui/app/services/gr-auth/gr-auth.ts
index a6b4fdd697..bc63099b55 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth.ts
@@ -5,33 +5,10 @@
*/
import {define} from '../../models/dependency';
import {AuthRequestInit, Finalizable} from '../../types/types';
-export enum AuthType {
- XSRF_TOKEN = 'xsrf_token',
- ACCESS_TOKEN = 'access_token',
-}
-
-export enum AuthStatus {
- UNDETERMINED = 0,
- AUTHED = 1,
- NOT_AUTHED = 2,
- ERROR = 3,
-}
-
-export interface Token {
- access_token?: string;
- expires_at?: string;
-}
-
-export type GetTokenCallback = () => Promise<Token | null>;
-
-export interface DefaultAuthOptions {
- credentials: RequestCredentials;
-}
export const authServiceToken = define<AuthService>('auth-service');
export interface AuthService extends Finalizable {
- baseUrl: string;
isAuthed: boolean;
/**
@@ -42,11 +19,6 @@ export interface AuthService extends Finalizable {
clearCache(): void;
/**
- * Enable cross-domain authentication using OAuth access token.
- */
- setup(getToken: GetTokenCallback, defaultOptions: DefaultAuthOptions): void;
-
- /**
* Perform network fetch with authentication.
*/
fetch(url: string, options?: AuthRequestInit): Promise<Response>;
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
index fb53d5e0d8..7570ba3fc1 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
@@ -6,21 +6,18 @@
import {AuthRequestInit, Finalizable} from '../../types/types';
import {fire} from '../../utils/event-util';
import {getBaseUrl} from '../../utils/url-util';
-import {
- AuthService,
- AuthStatus,
- AuthType,
- DefaultAuthOptions,
- GetTokenCallback,
- Token,
-} from './gr-auth';
+import {AuthService} from './gr-auth';
-export const MAX_AUTH_CHECK_WAIT_TIME_MS = 1000 * 30; // 30s
-const MAX_GET_TOKEN_RETRIES = 2;
+const MAX_AUTH_CHECK_WAIT_TIME_MS = 1000 * 30; // 30s
-interface ValidToken extends Token {
- access_token: string;
- expires_at: string;
+const CREDS_EXPIRED_MSG = 'Credentials expired.';
+
+// visible for testing
+export enum AuthStatus {
+ UNDETERMINED = 0,
+ AUTHED = 1,
+ NOT_AUTHED = 2,
+ ERROR = 3,
}
interface AuthRequestInitWithHeaders extends AuthRequestInit {
@@ -34,46 +31,12 @@ interface AuthRequestInitWithHeaders extends AuthRequestInit {
* Auth class.
*/
export class Auth implements AuthService, Finalizable {
- // TODO(dmfilippov): Remove Type and Status properties, expose AuthType and
- // AuthStatus to API
- static TYPE = {
- XSRF_TOKEN: AuthType.XSRF_TOKEN,
- ACCESS_TOKEN: AuthType.ACCESS_TOKEN,
- };
-
- static STATUS = {
- UNDETERMINED: AuthStatus.UNDETERMINED,
- AUTHED: AuthStatus.AUTHED,
- NOT_AUTHED: AuthStatus.NOT_AUTHED,
- ERROR: AuthStatus.ERROR,
- };
-
- static CREDS_EXPIRED_MSG = 'Credentials expired.';
-
private authCheckPromise?: Promise<boolean>;
private _last_auth_check_time: number = Date.now();
private _status = AuthStatus.UNDETERMINED;
- private retriesLeft = MAX_GET_TOKEN_RETRIES;
-
- private cachedTokenPromise: Promise<Token | null> | null = null;
-
- private type?: AuthType;
-
- private defaultOptions: AuthRequestInit = {};
-
- private getToken: GetTokenCallback;
-
- constructor() {
- this.getToken = () => Promise.resolve(this.cachedTokenPromise);
- }
-
- get baseUrl() {
- return getBaseUrl();
- }
-
finalize() {}
/**
@@ -85,7 +48,7 @@ export class Auth implements AuthService, Finalizable {
Date.now() - this._last_auth_check_time > MAX_AUTH_CHECK_WAIT_TIME_MS
) {
// Refetch after last check expired
- this.authCheckPromise = fetch(`${this.baseUrl}/auth-check`)
+ this.authCheckPromise = fetch(`${getBaseUrl()}/auth-check`)
.then(res => {
// Make a call that requires loading the body of the request. This makes it so that the browser
// can close the request even though callers of this method might only ever read headers.
@@ -99,10 +62,10 @@ export class Auth implements AuthService, Finalizable {
// auth-check will return 204 if authed
// treat the rest as unauthed
if (res.status === 204) {
- this._setStatus(Auth.STATUS.AUTHED);
+ this._setStatus(AuthStatus.AUTHED);
return true;
} else {
- this._setStatus(Auth.STATUS.NOT_AUTHED);
+ this._setStatus(AuthStatus.NOT_AUTHED);
return false;
}
})
@@ -127,35 +90,20 @@ export class Auth implements AuthService, Finalizable {
if (this._status === AuthStatus.AUTHED) {
fire(document, 'auth-error', {
- message: Auth.CREDS_EXPIRED_MSG,
+ message: CREDS_EXPIRED_MSG,
action: 'Refresh credentials',
});
}
this._status = status;
}
+ // visible for testing
get status() {
return this._status;
}
get isAuthed() {
- return this._status === Auth.STATUS.AUTHED;
- }
-
- /**
- * Enable cross-domain authentication using OAuth access token.
- */
- setup(getToken: GetTokenCallback, defaultOptions: DefaultAuthOptions) {
- this.retriesLeft = MAX_GET_TOKEN_RETRIES;
- if (getToken) {
- this.type = AuthType.ACCESS_TOKEN;
- this.cachedTokenPromise = null;
- this.getToken = getToken;
- }
- this.defaultOptions = {};
- if (defaultOptions) {
- this.defaultOptions.credentials = defaultOptions.credentials;
- }
+ return this._status === AuthStatus.AUTHED;
}
/**
@@ -164,16 +112,9 @@ export class Auth implements AuthService, Finalizable {
fetch(url: string, options?: AuthRequestInit): Promise<Response> {
const optionsWithHeaders: AuthRequestInitWithHeaders = {
headers: new Headers(),
- ...this.defaultOptions,
...options,
};
- if (this.type === AuthType.ACCESS_TOKEN) {
- return this._getAccessToken().then(accessToken =>
- this._fetchWithAccessToken(url, optionsWithHeaders, accessToken)
- );
- } else {
- return this._fetchWithXsrfToken(url, optionsWithHeaders);
- }
+ return this._fetchWithXsrfToken(url, optionsWithHeaders);
}
// private but used in test
@@ -191,23 +132,6 @@ export class Auth implements AuthService, Finalizable {
return result;
}
- // private but used in test
- _isTokenValid(token: Token | null): token is ValidToken {
- if (!token) {
- return false;
- }
- if (!token.access_token || !token.expires_at) {
- return false;
- }
-
- const expiration = new Date(Number(token.expires_at) * 1000);
- if (Date.now() >= expiration.getTime()) {
- return false;
- }
-
- return true;
- }
-
private _fetchWithXsrfToken(
url: string,
options: AuthRequestInitWithHeaders
@@ -222,72 +146,6 @@ export class Auth implements AuthService, Finalizable {
return this._ensureBodyLoaded(fetch(url, options));
}
- private _getAccessToken(): Promise<string | null> {
- if (!this.cachedTokenPromise) {
- this.cachedTokenPromise = this.getToken();
- }
- return this.cachedTokenPromise.then(token => {
- if (this._isTokenValid(token)) {
- this.retriesLeft = MAX_GET_TOKEN_RETRIES;
- return token.access_token;
- }
- if (this.retriesLeft > 0) {
- this.retriesLeft--;
- this.cachedTokenPromise = null;
- return this._getAccessToken();
- }
- // Fall back to anonymous access.
- return null;
- });
- }
-
- private _fetchWithAccessToken(
- url: string,
- options: AuthRequestInitWithHeaders,
- accessToken: string | null
- ): Promise<Response> {
- const params = [];
-
- if (accessToken) {
- params.push(`access_token=${accessToken}`);
- const baseUrl = this.baseUrl;
- const pathname = baseUrl
- ? url.substring(url.indexOf(baseUrl) + baseUrl.length)
- : url;
- if (!pathname.startsWith('/a/')) {
- url = url.replace(pathname, '/a' + pathname);
- }
- }
-
- const method = options.method || 'GET';
- let contentType = options.headers.get('Content-Type');
-
- // For all requests with body, ensure json content type.
- if (!contentType && options.body) {
- contentType = 'application/json';
- }
-
- if (method !== 'GET') {
- options.method = 'POST';
- params.push(`$m=${method}`);
- // If a request is not GET, and does not have a body, ensure text/plain
- // content type.
- if (!contentType) {
- contentType = 'text/plain';
- }
- }
-
- if (contentType) {
- options.headers.set('Content-Type', 'text/plain');
- params.push(`$ct=${encodeURIComponent(contentType)}`);
- }
-
- if (params.length) {
- url = url + (url.indexOf('?') === -1 ? '?' : '&') + params.join('&');
- }
- return this._ensureBodyLoaded(fetch(url, options));
- }
-
private _ensureBodyLoaded(response: Promise<Response>): Promise<Response> {
return response.then(response => {
if (!response.ok) {
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth_mock.ts b/polygerrit-ui/app/services/gr-auth/gr-auth_mock.ts
index 9cdd37e2f0..1be9c9e461 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_mock.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_mock.ts
@@ -5,23 +5,14 @@
*/
import {AuthRequestInit} from '../../types/types';
import {fire} from '../../utils/event-util';
-import {
- AuthService,
- AuthStatus,
- DefaultAuthOptions,
- GetTokenCallback,
-} from './gr-auth';
-import {Auth} from './gr-auth_impl';
+import {AuthService} from './gr-auth';
+import {AuthStatus} from './gr-auth_impl';
export class GrAuthMock implements AuthService {
- baseUrl = '';
-
private _status = AuthStatus.UNDETERMINED;
- constructor() {}
-
get isAuthed() {
- return this._status === Auth.STATUS.AUTHED;
+ return this._status === AuthStatus.AUTHED;
}
finalize() {}
@@ -30,7 +21,7 @@ export class GrAuthMock implements AuthService {
if (this._status === status) return;
if (this._status === AuthStatus.AUTHED) {
fire(document, 'auth-error', {
- message: Auth.CREDS_EXPIRED_MSG,
+ message: 'Credentials expired.',
action: 'Refresh credentials',
});
}
@@ -42,12 +33,12 @@ export class GrAuthMock implements AuthService {
}
authCheck() {
- return this.fetch(`${this.baseUrl}/auth-check`).then(res => {
+ return this.fetch('/auth-check').then(res => {
if (res.status === 204) {
- this._setStatus(Auth.STATUS.AUTHED);
+ this._setStatus(AuthStatus.AUTHED);
return true;
} else {
- this._setStatus(Auth.STATUS.NOT_AUTHED);
+ this._setStatus(AuthStatus.NOT_AUTHED);
return false;
}
});
@@ -55,8 +46,6 @@ export class GrAuthMock implements AuthService {
clearCache() {}
- setup(_getToken: GetTokenCallback, _defaultOptions: DefaultAuthOptions) {}
-
fetch(_url: string, _options?: AuthRequestInit): Promise<Response> {
return Promise.resolve(new Response());
}
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth_test.ts b/polygerrit-ui/app/services/gr-auth/gr-auth_test.ts
index b9cef86de0..f50921ed67 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_test.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_test.ts
@@ -4,10 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../../test/common-test-setup';
-import {Auth} from './gr-auth_impl';
-import {stubBaseUrl} from '../../test/test-utils';
+import {Auth, AuthStatus} from './gr-auth_impl';
import {SinonFakeTimers} from 'sinon';
-import {DefaultAuthOptions} from './gr-auth';
import {assert} from '@open-wc/testing';
import {AuthRequestInit} from '../../types/types';
@@ -28,28 +26,28 @@ suite('gr-auth', () => {
fakeFetch.returns(Promise.resolve({status: 403}));
const authed = await auth.authCheck();
assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ assert.equal(auth.status, AuthStatus.NOT_AUTHED);
});
test('auth-check returns 204', async () => {
fakeFetch.returns(Promise.resolve({status: 204}));
const authed = await auth.authCheck();
assert.isTrue(authed);
- assert.equal(auth.status, Auth.STATUS.AUTHED);
+ assert.equal(auth.status, AuthStatus.AUTHED);
});
test('auth-check returns 502', async () => {
fakeFetch.returns(Promise.resolve({status: 502}));
const authed = await auth.authCheck();
assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ assert.equal(auth.status, AuthStatus.NOT_AUTHED);
});
test('auth-check failed', async () => {
fakeFetch.returns(Promise.reject(new Error('random error')));
const authed = await auth.authCheck();
assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.ERROR);
+ assert.equal(auth.status, AuthStatus.ERROR);
});
});
@@ -65,42 +63,42 @@ suite('gr-auth', () => {
fakeFetch.returns(Promise.resolve({status: 403}));
const authed = await auth.authCheck();
assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ assert.equal(auth.status, AuthStatus.NOT_AUTHED);
fakeFetch.returns(Promise.resolve({status: 204}));
const authed2 = await auth.authCheck();
assert.isFalse(authed2);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ assert.equal(auth.status, AuthStatus.NOT_AUTHED);
});
test('clearCache should refetch auth-check result', async () => {
fakeFetch.returns(Promise.resolve({status: 403}));
const authed = await auth.authCheck();
assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ assert.equal(auth.status, AuthStatus.NOT_AUTHED);
fakeFetch.returns(Promise.resolve({status: 204}));
auth.clearCache();
const authed2 = await auth.authCheck();
assert.isTrue(authed2);
- assert.equal(auth.status, Auth.STATUS.AUTHED);
+ assert.equal(auth.status, AuthStatus.AUTHED);
});
test('cache expired on auth-check after certain time', async () => {
fakeFetch.returns(Promise.resolve({status: 403}));
const authed = await auth.authCheck();
assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ assert.equal(auth.status, AuthStatus.NOT_AUTHED);
clock.tick(1000 * 10000);
fakeFetch.returns(Promise.resolve({status: 204}));
const authed2 = await auth.authCheck();
assert.isTrue(authed2);
- assert.equal(auth.status, Auth.STATUS.AUTHED);
+ assert.equal(auth.status, AuthStatus.AUTHED);
});
test('no cache if auth-check failed', async () => {
fakeFetch.returns(Promise.reject(new Error('random error')));
const authed = await auth.authCheck();
assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.ERROR);
+ assert.equal(auth.status, AuthStatus.ERROR);
assert.equal(fakeFetch.callCount, 1);
await auth.authCheck();
assert.equal(fakeFetch.callCount, 2);
@@ -110,14 +108,14 @@ suite('gr-auth', () => {
fakeFetch.returns(Promise.resolve({status: 204}));
const authed = await auth.authCheck();
assert.isTrue(authed);
- assert.equal(auth.status, Auth.STATUS.AUTHED);
+ assert.equal(auth.status, AuthStatus.AUTHED);
clock.tick(1000 * 10000);
fakeFetch.returns(Promise.resolve({status: 403}));
const emitStub = sinon.stub();
document.addEventListener('auth-error', emitStub);
const authed2 = await auth.authCheck();
assert.isFalse(authed2);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ assert.equal(auth.status, AuthStatus.NOT_AUTHED);
assert.isTrue(emitStub.called);
document.removeEventListener('auth-error', emitStub);
});
@@ -126,7 +124,7 @@ suite('gr-auth', () => {
fakeFetch.returns(Promise.resolve({status: 204}));
const authed = await auth.authCheck();
assert.isTrue(authed);
- assert.equal(auth.status, Auth.STATUS.AUTHED);
+ assert.equal(auth.status, AuthStatus.AUTHED);
clock.tick(1000 * 10000);
fakeFetch.returns(Promise.reject(new Error('random error')));
const emitStub = sinon.stub();
@@ -134,7 +132,7 @@ suite('gr-auth', () => {
const authed2 = await auth.authCheck();
assert.isFalse(authed2);
assert.isTrue(emitStub.called);
- assert.equal(auth.status, Auth.STATUS.ERROR);
+ assert.equal(auth.status, AuthStatus.ERROR);
document.removeEventListener('auth-error', emitStub);
});
@@ -142,7 +140,7 @@ suite('gr-auth', () => {
fakeFetch.returns(Promise.resolve({status: 403}));
const authed = await auth.authCheck();
assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ assert.equal(auth.status, AuthStatus.NOT_AUTHED);
clock.tick(1000 * 10000);
fakeFetch.returns(Promise.resolve({status: 204}));
const emitStub = sinon.stub();
@@ -150,7 +148,7 @@ suite('gr-auth', () => {
const authed2 = await auth.authCheck();
assert.isTrue(authed2);
assert.isFalse(emitStub.called);
- assert.equal(auth.status, Auth.STATUS.AUTHED);
+ assert.equal(auth.status, AuthStatus.AUTHED);
document.removeEventListener('auth-error', emitStub);
});
@@ -158,7 +156,7 @@ suite('gr-auth', () => {
fakeFetch.returns(Promise.resolve({status: 403}));
const authed = await auth.authCheck();
assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ assert.equal(auth.status, AuthStatus.NOT_AUTHED);
clock.tick(1000 * 10000);
fakeFetch.returns(Promise.reject(new Error('random error')));
const emitStub = sinon.stub();
@@ -166,7 +164,7 @@ suite('gr-auth', () => {
const authed2 = await auth.authCheck();
assert.isFalse(authed2);
assert.isFalse(emitStub.called);
- assert.equal(auth.status, Auth.STATUS.ERROR);
+ assert.equal(auth.status, AuthStatus.ERROR);
document.removeEventListener('auth-error', emitStub);
});
});
@@ -196,134 +194,4 @@ suite('gr-auth', () => {
assert.equal(options.headers.get('X-Gerrit-Auth'), 'foobar');
});
});
-
- suite('cors (access token)', () => {
- let fakeFetch: sinon.SinonStub;
-
- setup(() => {
- fakeFetch = sinon
- .stub(window, 'fetch')
- .returns(Promise.resolve({...new Response(), ok: true}));
- });
-
- let getToken: sinon.SinonStub;
-
- const makeToken = (accessToken?: string) => {
- return {
- access_token: accessToken || 'zbaz',
- expires_at: new Date(Date.now() + 10e8).getTime(),
- };
- };
-
- setup(() => {
- getToken = sinon.stub();
- getToken.returns(Promise.resolve(makeToken()));
- const defaultOptions: DefaultAuthOptions = {
- credentials: 'include',
- };
- auth.setup(getToken, defaultOptions);
- });
-
- test('base url support', async () => {
- const baseUrl = 'http://foo';
- stubBaseUrl(baseUrl);
- await auth.fetch(baseUrl + '/url', {bar: 'bar'} as AuthRequestInit);
- const [url] = fakeFetch.lastCall.args;
- assert.equal(url, 'http://foo/a/url?access_token=zbaz');
- });
-
- test('fetch not signed in', async () => {
- getToken.returns(Promise.resolve());
- await auth.fetch('/url', {bar: 'bar'} as AuthRequestInit);
- const [url, options] = fakeFetch.lastCall.args;
- assert.equal(url, '/url');
- assert.equal(options.bar, 'bar');
- assert.equal(Object.keys(options.headers).length, 0);
- });
-
- test('fetch signed in', async () => {
- await auth.fetch('/url', {bar: 'bar'} as AuthRequestInit);
- const [url, options] = fakeFetch.lastCall.args;
- assert.equal(url, '/a/url?access_token=zbaz');
- assert.equal(options.bar, 'bar');
- });
-
- test('getToken calls are cached', async () => {
- await Promise.all([auth.fetch('/url-one'), auth.fetch('/url-two')]);
- assert.equal(getToken.callCount, 1);
- });
-
- test('getToken refreshes token', async () => {
- const isTokenValidStub = sinon.stub(auth, '_isTokenValid');
- isTokenValidStub
- .onFirstCall()
- .returns(true)
- .onSecondCall()
- .returns(false)
- .onThirdCall()
- .returns(true);
- await auth.fetch('/url-one');
- getToken.returns(Promise.resolve(makeToken('bzzbb')));
- await auth.fetch('/url-two');
-
- const [[firstUrl], [secondUrl]] = fakeFetch.args;
- assert.equal(firstUrl, '/a/url-one?access_token=zbaz');
- assert.equal(secondUrl, '/a/url-two?access_token=bzzbb');
- });
-
- test('signed in token error falls back to anonymous', async () => {
- getToken.returns(Promise.resolve('rubbish'));
- await auth.fetch('/url', {bar: 'bar'} as AuthRequestInit);
- const [url, options] = fakeFetch.lastCall.args;
- assert.equal(url, '/url');
- assert.equal(options.bar, 'bar');
- });
-
- test('_isTokenValid', () => {
- assert.isFalse(auth._isTokenValid(null));
- assert.isFalse(auth._isTokenValid({}));
- assert.isFalse(auth._isTokenValid({access_token: 'foo'}));
- assert.isFalse(
- auth._isTokenValid({
- access_token: 'foo',
- expires_at: `${Date.now() / 1000 - 1}`,
- })
- );
- assert.isTrue(
- auth._isTokenValid({
- access_token: 'foo',
- expires_at: `${Date.now() / 1000 + 1}`,
- })
- );
- });
-
- test('HTTP PUT with content type', async () => {
- const originalOptions = {
- method: 'PUT',
- headers: new Headers({'Content-Type': 'mail/pigeon'}),
- };
- await auth.fetch('/url', originalOptions);
- assert.isTrue(getToken.called);
- const [url, options] = fakeFetch.lastCall.args;
- assert.include(url, '$ct=mail%2Fpigeon');
- assert.include(url, '$m=PUT');
- assert.include(url, 'access_token=zbaz');
- assert.equal(options.method, 'POST');
- assert.equal(options.headers.get('Content-Type'), 'text/plain');
- });
-
- test('HTTP PUT without content type', async () => {
- const originalOptions = {
- method: 'PUT',
- };
- await auth.fetch('/url', originalOptions);
- assert.isTrue(getToken.called);
- const [url, options] = fakeFetch.lastCall.args;
- assert.include(url, '$ct=text%2Fplain');
- assert.include(url, '$m=PUT');
- assert.include(url, 'access_token=zbaz');
- assert.equal(options.method, 'POST');
- assert.equal(options.headers.get('Content-Type'), 'text/plain');
- });
- });
});
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
index b6bb560c51..1eb3bc2be2 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -612,8 +612,9 @@ export class GrReporting implements ReportingService, Finalizable {
this.time(Timing.DIFF_VIEW_CONTENT_DISPLAYED);
this.time(Timing.DIFF_VIEW_DISPLAYED);
this.time(Timing.FILE_LIST_DISPLAYED);
- this.reportRepoName = undefined;
- this.reportChangeId = undefined;
+
+ this.setRepoName(undefined);
+ this.setChangeId(undefined);
// reset slow rpc list since here start page loads which report these rpcs
this.slowRpcList = [];
this.hiddenDurationTimer.reset();
@@ -1004,12 +1005,17 @@ export class GrReporting implements ReportingService, Finalizable {
);
}
- setRepoName(repoName: string) {
+ setRepoName(repoName?: string) {
this.reportRepoName = repoName;
}
- setChangeId(changeId: NumericChangeId) {
+ setChangeId(changeId?: NumericChangeId) {
+ const originalChangeId = this.reportChangeId;
this.reportChangeId = changeId;
+
+ if (!!changeId && changeId !== originalChangeId) {
+ this.reportInteraction(Interaction.CHANGE_ID_CHANGED, {changeId});
+ }
}
}
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
index bbb47c26ee..2b5842aca2 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
@@ -6,17 +6,6 @@
/* NB: Order is important, because of namespaced classes. */
import {GrEtagDecorator} from '../../elements/shared/gr-rest-api-interface/gr-etag-decorator';
-import {
- FetchJSONRequest,
- FetchParams,
- FetchPromisesCache,
- GrRestApiHelper,
- parsePrefixedJSON,
- readResponsePayload,
- SendJSONRequest,
- SendRequest,
- SiteBasedCache,
-} from '../../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
import {GrReviewerUpdatesParser} from '../../elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser';
import {parseDate} from '../../utils/date-util';
import {getBaseUrl} from '../../utils/url-util';
@@ -113,7 +102,6 @@ import {
TagInput,
TopMenuEntryInfo,
UrlEncodedCommentId,
- FixReplacementInfo,
DraftInfo,
ListChangesOption,
ReviewResult,
@@ -124,7 +112,6 @@ import {
IgnoreWhitespaceType,
} from '../../types/diff';
import {
- CancelConditionCallback,
GetDiffCommentsOutput,
GetDiffRobotCommentsOutput,
RestApiService,
@@ -138,17 +125,26 @@ import {
ReviewerState,
} from '../../constants/constants';
import {firePageError, fireServerError} from '../../utils/event-util';
-import {
- AuthRequestInit,
- Finalizable,
- ParsedChangeInfo,
-} from '../../types/types';
+import {Finalizable, ParsedChangeInfo} from '../../types/types';
import {ErrorCallback} from '../../api/rest';
import {addDraftProp} from '../../utils/comment-util';
import {BaseScheduler, Scheduler} from '../scheduler/scheduler';
import {MaxInFlightScheduler} from '../scheduler/max-in-flight-scheduler';
import {escapeAndWrapSearchOperatorValue} from '../../utils/string-util';
import {FlagsService, KnownExperimentId} from '../flags/flags';
+import {RetryScheduler} from '../scheduler/retry-scheduler';
+import {FixReplacementInfo} from '../../api/rest-api';
+import {
+ FetchParams,
+ FetchPromisesCache,
+ FetchRequest,
+ getFetchOptions,
+ GrRestApiHelper,
+ parsePrefixedJSON,
+ readJSONResponsePayload,
+ SiteBasedCache,
+ throwingErrorCallback,
+} from '../../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
const MAX_PROJECT_RESULTS = 25;
@@ -169,7 +165,7 @@ let fetchPromisesCache = new FetchPromisesCache(); // Shared across instances.
let pendingRequest: {[promiseName: string]: Array<Promise<unknown>>} = {}; // Shared across instances.
let grEtagDecorator = new GrEtagDecorator(); // Shared across instances.
// TODO: consider changing this to Map()
-let projectLookup: {[changeNum: string]: Promise<RepoName | undefined>} = {}; // Shared across instances.
+let projectLookup: {[changeNum: string]: Promise<RepoName> | undefined} = {}; // Shared across instances.
function suppress404s(res?: Response | null) {
if (!res || res.status === 404) return;
@@ -177,48 +173,6 @@ function suppress404s(res?: Response | null) {
fireServerError(res);
}
-interface FetchChangeJSON {
- reportEndpointAsIs?: boolean;
- endpoint: string;
- anonymizedEndpoint?: string;
- revision?: RevisionId;
- changeNum: NumericChangeId;
- errFn?: ErrorCallback;
- params?: FetchParams;
- fetchOptions?: AuthRequestInit;
- // TODO(TS): The following properties are not used, however some methods
- // set them to true. They should be either changed to reportEndpointAsIs: true
- // or deleted. This should be done carefully case by case.
- reportEndpointAsId?: true;
-}
-
-interface SendChangeRequestBase {
- patchNum?: PatchSetNum;
- reportEndpointAsIs?: boolean;
- endpoint: string;
- anonymizedEndpoint?: string;
- changeNum: NumericChangeId;
- method: HttpMethod | undefined;
- errFn?: ErrorCallback;
- headers?: Record<string, string>;
- contentType?: string;
- body?: string | object;
-
- // TODO(TS): The following properties are not used, however some methods
- // set them to true. They should be either changed to reportEndpointAsIs: true
- // or deleted. This should be done carefully case by case.
- reportUrlAsIs?: true;
- reportEndpointAsId?: true;
-}
-
-interface SendRawChangeRequest extends SendChangeRequestBase {
- parseResponse?: false | null;
-}
-
-interface SendJSONChangeRequest extends SendChangeRequestBase {
- parseResponse: true;
-}
-
interface QueryChangesParams {
[paramName: string]: string | undefined | number | string[];
O?: string; // options
@@ -256,8 +210,6 @@ interface GetDiffParams {
base?: PatchSetNum;
}
-type SendChangeRequest = SendRawChangeRequest | SendJSONChangeRequest;
-
export function testOnlyResetGrRestApiSharedObjects(authService: AuthService) {
siteBasedCache = new SiteBasedCache();
fetchPromisesCache = new FetchPromisesCache();
@@ -268,11 +220,19 @@ export function testOnlyResetGrRestApiSharedObjects(authService: AuthService) {
}
function createReadScheduler() {
- return new MaxInFlightScheduler<Response>(new BaseScheduler<Response>(), 10);
+ return new RetryScheduler<Response>(
+ new MaxInFlightScheduler<Response>(new BaseScheduler<Response>(), 10),
+ 3 /* maxRetry */,
+ 50 /* backoffIntervalMs */
+ );
}
function createWriteScheduler() {
- return new MaxInFlightScheduler<Response>(new BaseScheduler<Response>(), 5);
+ return new RetryScheduler<Response>(
+ new MaxInFlightScheduler<Response>(new BaseScheduler<Response>(), 5),
+ 3 /* maxRetry */,
+ 50 /* backoffIntervalMs */
+ );
}
function createSerializingScheduler() {
@@ -302,32 +262,27 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
private readonly authService: AuthService,
private readonly flagService: FlagsService
) {
+ const readScheduler = createReadScheduler();
+ const writeScheduler = createWriteScheduler();
this._restApiHelper = new GrRestApiHelper(
this._cache,
this.authService,
this._sharedFetchPromises,
- createReadScheduler(),
- createWriteScheduler()
+ readScheduler,
+ writeScheduler
);
this._serialScheduler = createSerializingScheduler();
}
finalize() {}
- _fetchSharedCacheURL(
- req: FetchJSONRequest
- ): Promise<AccountDetailInfo | ParsedJSON | undefined> {
- // Cache is shared across instances
- return this._restApiHelper.fetchCacheURL(req);
- }
-
- getResponseObject(response: Response): Promise<ParsedJSON> {
- return this._restApiHelper.getResponseObject(response);
+ async getResponseObject(response: Response): Promise<ParsedJSON> {
+ return (await readJSONResponsePayload(response)).parsed;
}
getConfig(noCache?: boolean): Promise<ServerInfo | undefined> {
if (!noCache) {
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: '/config/server/info',
reportUrlAsIs: true,
}) as Promise<ServerInfo | undefined>;
@@ -343,9 +298,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
repo: RepoName,
errFn?: ErrorCallback
): Promise<ProjectInfo | undefined> {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: '/projects/' + encodeURIComponent(repo),
errFn,
anonymizedUrl: '/projects/*',
@@ -356,9 +309,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
repo: RepoName,
errFn?: ErrorCallback
): Promise<ConfigInfo | undefined> {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: '/projects/' + encodeURIComponent(repo) + '/config',
errFn,
anonymizedUrl: '/projects/*/config',
@@ -366,9 +317,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
}
getRepoAccess(repo: RepoName): Promise<RepoAccessInfoMap | undefined> {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: '/access/?project=' + encodeURIComponent(repo),
anonymizedUrl: '/access/?project=*',
}) as Promise<RepoAccessInfoMap | undefined>;
@@ -378,9 +327,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
repo: RepoName,
errFn?: ErrorCallback
): Promise<DashboardInfo[] | undefined> {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: `/projects/${encodeURIComponent(repo)}/dashboards?inherited`,
errFn,
anonymizedUrl: '/projects/*/dashboards?inherited',
@@ -388,49 +335,55 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
}
saveRepoConfig(repo: RepoName, config: ConfigInput): Promise<Response> {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
const url = `/projects/${encodeURIComponent(repo)}/config`;
this._cache.delete(url);
- return this._restApiHelper.send({
- method: HttpMethod.PUT,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: config,
+ }),
url,
- body: config,
anonymizedUrl: '/projects/*/config',
+ reportServerError: true,
});
}
runRepoGC(repo: RepoName): Promise<Response> {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
const encodeName = encodeURIComponent(repo);
- return this._restApiHelper.send({
- method: HttpMethod.POST,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.POST,
+ body: '',
+ }),
url: `/projects/${encodeName}/gc`,
- body: '',
anonymizedUrl: '/projects/*/gc',
+ reportServerError: true,
});
}
createRepo(config: ProjectInput & {name: RepoName}): Promise<Response> {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
const encodeName = encodeURIComponent(config.name);
- return this._restApiHelper.send({
- method: HttpMethod.PUT,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: config,
+ }),
url: `/projects/${encodeName}`,
- body: config,
anonymizedUrl: '/projects/*',
+ reportServerError: true,
});
}
createGroup(config: GroupInput & {name: string}): Promise<Response> {
const encodeName = encodeURIComponent(config.name);
- return this._restApiHelper.send({
- method: HttpMethod.PUT,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: config,
+ }),
url: `/groups/${encodeName}`,
- body: config,
anonymizedUrl: '/groups/*',
+ reportServerError: true,
});
}
@@ -446,28 +399,30 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
}
deleteRepoBranches(repo: RepoName, ref: GitRef): Promise<Response> {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
const encodeName = encodeURIComponent(repo);
const encodeRef = encodeURIComponent(ref);
- return this._restApiHelper.send({
- method: HttpMethod.DELETE,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.DELETE,
+ body: '',
+ }),
url: `/projects/${encodeName}/branches/${encodeRef}`,
- body: '',
anonymizedUrl: '/projects/*/branches/*',
+ reportServerError: true,
});
}
deleteRepoTags(repo: RepoName, ref: GitRef): Promise<Response> {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
const encodeName = encodeURIComponent(repo);
const encodeRef = encodeURIComponent(ref);
- return this._restApiHelper.send({
- method: HttpMethod.DELETE,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.DELETE,
+ body: '',
+ }),
url: `/projects/${encodeName}/tags/${encodeRef}`,
- body: '',
anonymizedUrl: '/projects/*/tags/*',
+ reportServerError: true,
});
}
@@ -476,15 +431,16 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
branch: BranchName,
revision: BranchInput
): Promise<Response> {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
const encodeName = encodeURIComponent(name);
const encodeBranch = encodeURIComponent(branch);
- return this._restApiHelper.send({
- method: HttpMethod.PUT,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: revision,
+ }),
url: `/projects/${encodeName}/branches/${encodeBranch}`,
- body: revision,
anonymizedUrl: '/projects/*/branches/*',
+ reportServerError: true,
});
}
@@ -493,15 +449,16 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
tag: string,
revision: TagInput
): Promise<Response> {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
const encodeName = encodeURIComponent(name);
const encodeTag = encodeURIComponent(tag);
- return this._restApiHelper.send({
- method: HttpMethod.PUT,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: revision,
+ }),
url: `/projects/${encodeName}/tags/${encodeTag}`,
- body: revision,
anonymizedUrl: '/projects/*/tags/*',
+ reportServerError: true,
});
}
@@ -512,9 +469,9 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
url: `/groups/?owned&g=${encodeName}`,
anonymizedUrl: '/groups/owned&g=*',
};
- return this._fetchSharedCacheURL(req).then(configs =>
- hasOwnProperty(configs, groupName)
- );
+ return this._restApiHelper
+ .fetchCacheJSON(req)
+ .then(configs => hasOwnProperty(configs, groupName));
}
getGroupMembers(groupName: GroupId | GroupName): Promise<AccountInfo[]> {
@@ -536,11 +493,14 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
saveGroupName(groupId: GroupId | GroupName, name: string): Promise<Response> {
const encodeId = encodeURIComponent(groupId);
- return this._restApiHelper.send({
- method: HttpMethod.PUT,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: {name},
+ }),
url: `/groups/${encodeId}/name`,
- body: {name},
anonymizedUrl: '/groups/*/name',
+ reportServerError: true,
});
}
@@ -549,11 +509,14 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
ownerId: string
): Promise<Response> {
const encodeId = encodeURIComponent(groupId);
- return this._restApiHelper.send({
- method: HttpMethod.PUT,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: {owner: ownerId},
+ }),
url: `/groups/${encodeId}/owner`,
- body: {owner: ownerId},
anonymizedUrl: '/groups/*/owner',
+ reportServerError: true,
});
}
@@ -562,11 +525,14 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
description: string
): Promise<Response> {
const encodeId = encodeURIComponent(groupId);
- return this._restApiHelper.send({
- method: HttpMethod.PUT,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: {description},
+ }),
url: `/groups/${encodeId}/description`,
- body: {description},
anonymizedUrl: '/groups/*/description',
+ reportServerError: true,
});
}
@@ -575,11 +541,14 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
options: GroupOptionsInput
): Promise<Response> {
const encodeId = encodeURIComponent(groupId);
- return this._restApiHelper.send({
- method: HttpMethod.PUT,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: options,
+ }),
url: `/groups/${encodeId}/options`,
- body: options,
anonymizedUrl: '/groups/*/options',
+ reportServerError: true,
});
}
@@ -587,7 +556,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
group: EncodedGroupId,
errFn?: ErrorCallback
): Promise<GroupAuditEventInfo[] | undefined> {
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: `/groups/${group}/log.audit`,
errFn,
anonymizedUrl: '/groups/*/log.audit',
@@ -597,15 +566,16 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
saveGroupMember(
groupName: GroupId | GroupName,
groupMember: AccountId
- ): Promise<AccountInfo> {
+ ): Promise<AccountInfo | undefined> {
const encodeName = encodeURIComponent(groupName);
const encodeMember = encodeURIComponent(`${groupMember}`);
- return this._restApiHelper.send({
- method: HttpMethod.PUT,
+ return this._restApiHelper.fetchJSON({
+ fetchOptions: {
+ method: HttpMethod.PUT,
+ },
url: `/groups/${encodeName}/members/${encodeMember}`,
- parseResponse: true,
anonymizedUrl: '/groups/*/members/*',
- }) as unknown as Promise<AccountInfo>;
+ }) as unknown as Promise<AccountInfo | undefined>;
}
saveIncludedGroup(
@@ -616,19 +586,16 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
const encodeName = encodeURIComponent(groupName);
const encodeIncludedGroup = encodeURIComponent(includedGroup);
const req = {
- method: HttpMethod.PUT,
+ fetchOptions: {
+ method: HttpMethod.PUT,
+ },
url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
errFn,
anonymizedUrl: '/groups/*/groups/*',
};
- return this._restApiHelper.send(req).then(response => {
- if (response?.ok) {
- return this.getResponseObject(
- response
- ) as unknown as Promise<GroupInfo>;
- }
- return undefined;
- });
+ return this._restApiHelper.fetchJSON(req) as unknown as Promise<
+ GroupInfo | undefined
+ >;
}
deleteGroupMember(
@@ -637,10 +604,13 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
): Promise<Response> {
const encodeName = encodeURIComponent(groupName);
const encodeMember = encodeURIComponent(`${groupMember}`);
- return this._restApiHelper.send({
- method: HttpMethod.DELETE,
+ return this._restApiHelper.fetch({
+ fetchOptions: {
+ method: HttpMethod.DELETE,
+ },
url: `/groups/${encodeName}/members/${encodeMember}`,
anonymizedUrl: '/groups/*/members/*',
+ reportServerError: true,
});
}
@@ -650,15 +620,18 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
): Promise<Response> {
const encodeName = encodeURIComponent(groupName);
const encodeIncludedGroup = encodeURIComponent(includedGroup);
- return this._restApiHelper.send({
- method: HttpMethod.DELETE,
+ return this._restApiHelper.fetch({
+ fetchOptions: {
+ method: HttpMethod.DELETE,
+ },
url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
anonymizedUrl: '/groups/*/groups/*',
+ reportServerError: true,
});
}
getVersion(): Promise<string | undefined> {
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: '/config/server/version',
reportUrlAsIs: true,
}) as Promise<string | undefined>;
@@ -667,7 +640,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
getDiffPreferences(): Promise<DiffPreferencesInfo | undefined> {
return this.getLoggedIn().then(loggedIn => {
if (loggedIn) {
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: '/accounts/self/preferences.diff',
reportUrlAsIs: true,
}) as Promise<DiffPreferencesInfo | undefined>;
@@ -679,7 +652,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
getEditPreferences(): Promise<EditPreferencesInfo | undefined> {
return this.getLoggedIn().then(loggedIn => {
if (loggedIn) {
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: '/accounts/self/preferences.edit',
reportUrlAsIs: true,
}) as Promise<EditPreferencesInfo | undefined>;
@@ -697,44 +670,46 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
prefs.download_scheme = prefs.download_scheme.toLowerCase();
}
- return this._restApiHelper
- .send({
+ return this._restApiHelper.fetchJSON({
+ fetchOptions: getFetchOptions({
method: HttpMethod.PUT,
- url: '/accounts/self/preferences',
body: prefs,
- reportUrlAsIs: true,
- })
- .then((response: Response) =>
- this.getResponseObject(response).then(
- obj => obj as unknown as PreferencesInfo
- )
- );
+ }),
+ url: '/accounts/self/preferences',
+ reportUrlAsIs: true,
+ }) as unknown as Promise<PreferencesInfo | undefined>;
}
saveDiffPreferences(prefs: DiffPreferenceInput): Promise<Response> {
// Invalidate the cache.
this._cache.delete('/accounts/self/preferences.diff');
- return this._restApiHelper.send({
- method: HttpMethod.PUT,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: prefs,
+ }),
url: '/accounts/self/preferences.diff',
- body: prefs,
reportUrlAsIs: true,
+ reportServerError: true,
});
}
saveEditPreferences(prefs: EditPreferencesInfo): Promise<Response> {
// Invalidate the cache.
this._cache.delete('/accounts/self/preferences.edit');
- return this._restApiHelper.send({
- method: HttpMethod.PUT,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: prefs,
+ }),
url: '/accounts/self/preferences.edit',
- body: prefs,
reportUrlAsIs: true,
+ reportServerError: true,
});
}
getAccount(): Promise<AccountDetailInfo | undefined> {
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: '/accounts/self/detail',
reportUrlAsIs: true,
errFn: resp => {
@@ -746,7 +721,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
}
getAvatarChangeUrl() {
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: '/accounts/self/avatar.change.url',
reportUrlAsIs: true,
errFn: resp => {
@@ -764,29 +739,34 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
}) as Promise<AccountExternalIdInfo[] | undefined>;
}
- deleteAccount() {
- return this._restApiHelper.send({
- method: HttpMethod.DELETE,
+ deleteAccount(): Promise<Response> {
+ return this._restApiHelper.fetch({
+ fetchOptions: {
+ method: HttpMethod.DELETE,
+ },
url: '/accounts/self',
reportUrlAsIs: true,
+ reportServerError: true,
});
}
- deleteAccountIdentity(id: string[]) {
- return this._restApiHelper.send({
- method: HttpMethod.POST,
+ deleteAccountIdentity(id: string[]): Promise<Response> {
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.POST,
+ body: id,
+ }),
url: '/accounts/self/external.ids:delete',
- body: id,
- parseResponse: true,
reportUrlAsIs: true,
- }) as Promise<unknown>;
+ reportServerError: true,
+ });
}
getAccountDetails(
userId: AccountId | EmailAddress,
errFn?: ErrorCallback
): Promise<AccountDetailInfo | undefined> {
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: `/accounts/${encodeURIComponent(userId)}/detail`,
anonymizedUrl: '/accounts/*/detail',
errFn,
@@ -796,7 +776,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
async getAccountEmails() {
const isloggedIn = await this.getLoggedIn();
if (isloggedIn) {
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: '/accounts/self/emails',
reportUrlAsIs: true,
}) as Promise<EmailInfo[] | undefined>;
@@ -814,7 +794,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
})
.then((capabilities: AccountCapabilityInfo | undefined) => {
if (capabilities && capabilities.viewSecondaryEmails) {
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: '/accounts/' + email + '/emails',
reportUrlAsIs: true,
errFn,
@@ -825,44 +805,51 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
}
addAccountEmail(email: string): Promise<Response> {
- return this._restApiHelper.send({
- method: HttpMethod.PUT,
+ return this._restApiHelper.fetch({
+ fetchOptions: {
+ method: HttpMethod.PUT,
+ },
url: '/accounts/self/emails/' + encodeURIComponent(email),
anonymizedUrl: '/account/self/emails/*',
+ reportServerError: true,
});
}
deleteAccountEmail(email: string): Promise<Response> {
- return this._restApiHelper.send({
- method: HttpMethod.DELETE,
+ return this._restApiHelper.fetch({
+ fetchOptions: {
+ method: HttpMethod.DELETE,
+ },
url: '/accounts/self/emails/' + encodeURIComponent(email),
anonymizedUrl: '/accounts/self/email/*',
+ reportServerError: true,
});
}
- setPreferredAccountEmail(email: string): Promise<void> {
- // TODO(TS): add correct error handling
- const encodedEmail = encodeURIComponent(email);
- const req = {
- method: HttpMethod.PUT,
- url: `/accounts/self/emails/${encodedEmail}/preferred`,
+ async setPreferredAccountEmail(email: string): Promise<void> {
+ await this._restApiHelper.fetch({
+ fetchOptions: {
+ method: HttpMethod.PUT,
+ },
+ url: `/accounts/self/emails/${encodeURIComponent(email)}/preferred`,
anonymizedUrl: '/accounts/self/emails/*/preferred',
- };
- return this._restApiHelper.send(req).then(() => {
- // If result of getAccountEmails is in cache, update it in the cache
- // so we don't have to invalidate it.
- const cachedEmails = this._cache.get('/accounts/self/emails');
- if (cachedEmails) {
- const emails = cachedEmails.map(entry => {
- if (entry.email === email) {
- return {email: entry.email, preferred: true};
- } else {
- return {email: entry.email, preferred: false};
- }
- });
- this._cache.set('/accounts/self/emails', emails);
- }
+ reportServerError: true,
});
+ // If result of getAccountEmails is in cache, update it in the cache
+ // so we don't have to invalidate it.
+ const cachedEmails = this._cache.get(
+ '/accounts/self/emails'
+ ) as unknown as EmailInfo[];
+ if (cachedEmails) {
+ const emails = cachedEmails.map(entry => {
+ if (entry.email === email) {
+ return {email: entry.email, preferred: true};
+ } else {
+ return {email: entry.email, preferred: false};
+ }
+ });
+ this._cache.set('/accounts/self/emails', emails as unknown as ParsedJSON);
+ }
}
_updateCachedAccount(obj: Partial<AccountDetailInfo>): void {
@@ -875,68 +862,90 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
}
}
- setAccountName(name: string): Promise<void> {
- // TODO(TS): add correct error handling
- const req: SendJSONRequest = {
- method: HttpMethod.PUT,
+ async setAccountName(name: string): Promise<void> {
+ const response = await this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: {name},
+ }),
url: '/accounts/self/name',
- body: {name},
- parseResponse: true,
reportUrlAsIs: true,
- };
- return this._restApiHelper
- .send(req)
- .then(newName =>
- this._updateCachedAccount({name: newName as unknown as string})
- );
+ reportServerError: true,
+ });
+ if (!response.ok) {
+ return;
+ }
+ let newName = undefined;
+ // If the name was deleted server returns 204
+ if (response.status !== 204) {
+ newName = (await readJSONResponsePayload(response))
+ .parsed as unknown as string;
+ }
+ this._updateCachedAccount({name: newName});
}
- setAccountUsername(username: string): Promise<void> {
- // TODO(TS): add correct error handling
- const req: SendJSONRequest = {
- method: HttpMethod.PUT,
+ async setAccountUsername(username: string): Promise<void> {
+ const response = await this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: {username},
+ }),
url: '/accounts/self/username',
- body: {username},
- parseResponse: true,
reportUrlAsIs: true,
- };
- return this._restApiHelper
- .send(req)
- .then(newName =>
- this._updateCachedAccount({username: newName as unknown as string})
- );
+ });
+ if (!response.ok) {
+ return;
+ }
+ let newName = undefined;
+ // If the name was deleted server returns 204
+ if (response.status !== 204) {
+ newName = (await readJSONResponsePayload(response))
+ .parsed as unknown as string;
+ }
+ this._updateCachedAccount({username: newName});
}
- setAccountDisplayName(displayName: string): Promise<void> {
- // TODO(TS): add correct error handling
- const req: SendJSONRequest = {
- method: HttpMethod.PUT,
+ async setAccountDisplayName(displayName: string): Promise<void> {
+ const response = await this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: {display_name: displayName},
+ }),
url: '/accounts/self/displayname',
- body: {display_name: displayName},
- parseResponse: true,
reportUrlAsIs: true,
- };
- return this._restApiHelper.send(req).then(newName =>
- this._updateCachedAccount({
- display_name: newName as unknown as string,
- })
- );
+ reportServerError: true,
+ });
+ if (!response.ok) {
+ return;
+ }
+ let newName = undefined;
+ // If the name was deleted server returns 204
+ if (response.status !== 204) {
+ newName = (await readJSONResponsePayload(response))
+ .parsed as unknown as string;
+ }
+ this._updateCachedAccount({display_name: newName});
}
- setAccountStatus(status: string): Promise<void> {
- // TODO(TS): add correct error handling
- const req: SendJSONRequest = {
- method: HttpMethod.PUT,
+ async setAccountStatus(status: string): Promise<void> {
+ const response = await this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: {status},
+ }),
url: '/accounts/self/status',
- body: {status},
- parseResponse: true,
reportUrlAsIs: true,
- };
- return this._restApiHelper
- .send(req)
- .then(newStatus =>
- this._updateCachedAccount({status: newStatus as unknown as string})
- );
+ });
+ if (!response.ok) {
+ return;
+ }
+ let newStatus = undefined;
+ // If the status was deleted server returns 204
+ if (response.status !== 204) {
+ newStatus = (await readJSONResponsePayload(response))
+ .parsed as unknown as string;
+ }
+ this._updateCachedAccount({status: newStatus});
}
getAccountStatus(userId: AccountId) {
@@ -961,11 +970,14 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
}
saveAccountAgreement(name: ContributorAgreementInput): Promise<Response> {
- return this._restApiHelper.send({
- method: HttpMethod.PUT,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: name,
+ }),
url: '/accounts/self/agreements',
- body: name,
reportUrlAsIs: true,
+ reportServerError: true,
});
}
@@ -977,7 +989,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
queryString =
'?q=' + params.map(param => encodeURIComponent(param)).join('&q=');
}
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: '/accounts/self/capabilities' + queryString,
anonymizedUrl: '/accounts/self/capabilities?q=*',
}) as Promise<AccountCapabilityInfo | undefined>;
@@ -1003,7 +1015,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
}
getDefaultPreferences(): Promise<PreferencesInfo | undefined> {
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: '/config/server/preferences',
reportUrlAsIs: true,
}) as Promise<PreferencesInfo | undefined>;
@@ -1013,7 +1025,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
return this.getLoggedIn().then(loggedIn => {
if (loggedIn) {
const req = {url: '/accounts/self/preferences', reportUrlAsIs: true};
- return this._fetchSharedCacheURL(req).then(res => {
+ return this._restApiHelper.fetchCacheJSON(req).then(res => {
if (!res) {
return res;
}
@@ -1026,7 +1038,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
}
getWatchedProjects() {
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: '/accounts/self/watched.projects',
reportUrlAsIs: true,
}) as unknown as Promise<ProjectWatchInfo[] | undefined>;
@@ -1034,22 +1046,26 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
saveWatchedProjects(
projects: ProjectWatchInfo[]
- ): Promise<ProjectWatchInfo[]> {
- return this._restApiHelper.send({
- method: HttpMethod.POST,
+ ): Promise<ProjectWatchInfo[] | undefined> {
+ return this._restApiHelper.fetchJSON({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.POST,
+ body: projects,
+ }),
url: '/accounts/self/watched.projects',
- body: projects,
- parseResponse: true,
reportUrlAsIs: true,
- }) as unknown as Promise<ProjectWatchInfo[]>;
+ }) as unknown as Promise<ProjectWatchInfo[] | undefined>;
}
deleteWatchedProjects(projects: ProjectWatchInfo[]): Promise<Response> {
- return this._restApiHelper.send({
- method: HttpMethod.POST,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.POST,
+ body: projects,
+ }),
url: '/accounts/self/watched.projects:delete',
- body: projects,
reportUrlAsIs: true,
+ reportServerError: true,
});
}
@@ -1198,7 +1214,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
*/
_maybeInsertInLookup(change: ChangeInfo): void {
if (change?.project && change._number) {
- this.setInProjectLookup(change._number, change.project);
+ this.addRepoNameToCache(change._number, change.project);
}
}
@@ -1214,18 +1230,12 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
async getChangeDetail(
changeNum?: NumericChangeId,
- errFn?: ErrorCallback,
- cancelCondition?: CancelConditionCallback
+ errFn?: ErrorCallback
): Promise<ParsedChangeInfo | undefined> {
if (!changeNum) return;
const optionsHex = await this.getChangeOptionsHex();
- return this._getChangeDetail(
- changeNum,
- optionsHex,
- errFn,
- cancelCondition
- ).then(detail =>
+ return this._getChangeDetail(changeNum, optionsHex, errFn).then(detail =>
// detail has ChangeViewChangeInfo type because the optionsHex always
// includes ALL_REVISIONS flag.
GrReviewerUpdatesParser.parse(detail as ChangeViewChangeInfo)
@@ -1270,6 +1280,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
ListChangesOption.DETAILED_LABELS,
ListChangesOption.DOWNLOAD_COMMANDS,
ListChangesOption.MESSAGES,
+ ListChangesOption.REVIEWER_UPDATES,
ListChangesOption.SUBMITTABLE,
ListChangesOption.WEB_LINKS,
ListChangesOption.SKIP_DIFFSTAT,
@@ -1298,6 +1309,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
'DETAILED_ACCOUNTS',
'DOWNLOAD_COMMANDS',
'MESSAGES',
+ 'REVIEWER_UPDATES',
'SUBMITTABLE',
'WEB_LINKS',
'SKIP_DIFFSTAT',
@@ -1318,22 +1330,20 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
_getChangeDetail(
changeNum: NumericChangeId,
optionsHex: string,
- errFn?: ErrorCallback,
- cancelCondition?: CancelConditionCallback
+ errFn?: ErrorCallback
): Promise<ChangeInfo | undefined> {
return this.getChangeActionURL(changeNum, undefined, '/detail').then(
url => {
const params: FetchParams = {O: optionsHex};
const urlWithParams = this._restApiHelper.urlWithParams(url, params);
- const req: FetchJSONRequest = {
+ const req: FetchRequest = {
url,
errFn,
- cancelCondition,
params,
fetchOptions: this._etags.getOptions(urlWithParams),
anonymizedUrl: '/changes/*~*/detail?O=' + optionsHex,
};
- return this._restApiHelper.fetchRawJSON(req).then(response => {
+ return this._restApiHelper.fetch(req).then(response => {
if (response?.status === 304) {
return parsePrefixedJSON(
// urlWithParams already cached
@@ -1354,32 +1364,34 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
return Promise.resolve(undefined);
}
- return readResponsePayload(response).then(payload => {
- if (!payload) {
- return undefined;
- }
- this._etags.collect(urlWithParams, response, payload.raw);
- // TODO(TS): Why it is always change info?
- this._maybeInsertInLookup(payload.parsed as unknown as ChangeInfo);
+ return readJSONResponsePayload(response)
+ .then(payload => {
+ this._etags.collect(urlWithParams, response, payload.raw);
+ this._maybeInsertInLookup(
+ payload.parsed as unknown as ChangeInfo
+ );
- return payload.parsed as unknown as ChangeInfo;
- });
+ return payload.parsed as unknown as ChangeInfo;
+ })
+ .catch(() => undefined);
});
}
);
}
- getChangeCommitInfo(changeNum: NumericChangeId, patchNum: PatchSetNum) {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/commit?links',
- revision: patchNum,
- reportEndpointAsIs: true,
+ async getChangeCommitInfo(
+ changeNum: NumericChangeId,
+ patchNum: PatchSetNum
+ ): Promise<CommitInfo | undefined> {
+ const url = await this._changeBaseURL(changeNum, patchNum);
+ return this._restApiHelper.fetchJSON({
+ url: `${url}/commit?links`,
+ anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}/commit?links`,
errFn: suppress404s,
}) as Promise<CommitInfo | undefined>;
}
- getChangeFiles(
+ async getChangeFiles(
changeNum: NumericChangeId,
patchRange: PatchRange
): Promise<FileNameToFileInfoMap | undefined> {
@@ -1389,44 +1401,45 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
} else if (patchRange.basePatchNum !== PARENT) {
params = {base: patchRange.basePatchNum};
}
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/files',
- revision: patchRange.patchNum,
+ const url = await this._changeBaseURL(changeNum, patchRange.patchNum);
+ return this._restApiHelper.fetchJSON({
+ url: `${url}/files`,
params,
- reportEndpointAsIs: true,
+ anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}/files`,
}) as Promise<FileNameToFileInfoMap | undefined>;
}
- // TODO(TS): The output type is unclear
- getChangeEditFiles(
+ async getChangeEditFiles(
changeNum: NumericChangeId,
patchRange: PatchRange
): Promise<{files: FileNameToFileInfoMap} | undefined> {
- let endpoint = '/edit?list';
- let anonymizedEndpoint = endpoint;
+ const changeUrl = await this._changeBaseURL(changeNum);
+ let url = `${changeUrl}/edit?list`;
+ let anonymizedUrl = `${changeUrl}/edit?list`;
if (patchRange.basePatchNum !== PARENT) {
- endpoint += '&base=' + encodeURIComponent(`${patchRange.basePatchNum}`);
- anonymizedEndpoint += '&base=*';
+ url += '&base=' + encodeURIComponent(`${patchRange.basePatchNum}`);
+ anonymizedUrl += '&base=*';
}
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint,
- anonymizedEndpoint,
- }) as Promise<{files: FileNameToFileInfoMap} | undefined>;
+
+ const response = await this._restApiHelper.fetch({url, anonymizedUrl});
+ if (!response.ok || response.status === 204) {
+ return undefined;
+ }
+ return (await readJSONResponsePayload(response)).parsed as unknown as
+ | {files: FileNameToFileInfoMap}
+ | undefined;
}
- queryChangeFiles(
+ async queryChangeFiles(
changeNum: NumericChangeId,
patchNum: PatchSetNum,
query: string,
errFn?: ErrorCallback
- ) {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: `/files?q=${encodeURIComponent(query)}`,
- revision: patchNum,
- anonymizedEndpoint: '/files?q=*',
+ ): Promise<string[] | undefined> {
+ const url = await this._changeBaseURL(changeNum, patchNum);
+ return this._restApiHelper.fetchJSON({
+ url: `${url}/files?q=${encodeURIComponent(query)}`,
+ anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}/files?q=*`,
errFn,
}) as Promise<string[] | undefined>;
}
@@ -1443,19 +1456,15 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
return this.getChangeFiles(changeNum, patchRange);
}
- getChangeRevisionActions(
+ async getChangeRevisionActions(
changeNum: NumericChangeId,
patchNum: PatchSetNum
): Promise<ActionNameToActionInfoMap | undefined> {
- const req: FetchChangeJSON = {
- changeNum,
- endpoint: '/actions',
- revision: patchNum,
- reportEndpointAsIs: true,
- };
- return this._getChangeURLAndFetch(req) as Promise<
- ActionNameToActionInfoMap | undefined
- >;
+ const url = await this._changeBaseURL(changeNum, patchNum);
+ return this._restApiHelper.fetchJSON({
+ url: `${url}/actions`,
+ anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}/actions`,
+ }) as Promise<ActionNameToActionInfoMap | undefined>;
}
getChangeSuggestedReviewers(
@@ -1484,7 +1493,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
);
}
- _getChangeSuggestedGroup(
+ async _getChangeSuggestedGroup(
reviewerState: ReviewerState,
changeNum: NumericChangeId,
inputVal: string,
@@ -1499,22 +1508,22 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
if (inputVal) {
params.q = inputVal;
}
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/suggest_reviewers',
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetchJSON({
+ url: `${url}/suggest_reviewers`,
params,
- reportEndpointAsIs: true,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/suggest_reviewers`,
errFn,
}) as Promise<SuggestedReviewerInfo[] | undefined>;
}
- getChangeIncludedIn(
+ async getChangeIncludedIn(
changeNum: NumericChangeId
): Promise<IncludedInInfo | undefined> {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/in',
- reportEndpointAsIs: true,
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetchJSON({
+ url: `${url}/in`,
+ anonymizedUrl: `${url}/in`,
}) as Promise<IncludedInInfo | undefined>;
}
@@ -1583,7 +1592,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
getGroups(filter: string, groupsPerPage: number, offset?: number) {
const url = this._getGroupsUrl(filter, groupsPerPage, offset);
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url,
anonymizedUrl: '/groups/?*',
}) as Promise<GroupNameToGroupInfoMap | undefined>;
@@ -1596,21 +1605,17 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
errFn?: ErrorCallback
): Promise<ProjectInfoWithName[] | undefined> {
const [isQuery, url] = this._getReposUrl(filter, reposPerPage, offset);
-
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
-
// If the request is a query then return the response directly as the result
// will already be the expected array. If it is not a query, transform the
// map to an array.
if (isQuery) {
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url,
anonymizedUrl: '/projects/?*',
errFn,
}) as Promise<ProjectInfoWithName[] | undefined>;
} else {
- const result = await (this._fetchSharedCacheURL({
+ const result = await (this._restApiHelper.fetchCacheJSON({
url,
anonymizedUrl: '/projects/?*',
errFn,
@@ -1626,13 +1631,14 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
}
setRepoHead(repo: RepoName, ref: GitRef) {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._restApiHelper.send({
- method: HttpMethod.PUT,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: {ref},
+ }),
url: `/projects/${encodeURIComponent(repo)}/HEAD`,
- body: {ref},
anonymizedUrl: '/projects/*/HEAD',
+ reportServerError: true,
});
}
@@ -1648,8 +1654,6 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
filter = this._computeFilter(filter);
const encodedRepo = encodeURIComponent(repo);
const url = `/projects/${encodedRepo}/branches?n=${count}&S=${offset}${filter}`;
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
return this._restApiHelper.fetchJSON({
url,
errFn,
@@ -1670,8 +1674,6 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
const encodedFilter = this._computeFilter(filter);
const url =
`/projects/${encodedRepo}/tags` + `?n=${n}&S=${offset}` + encodedFilter;
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
return this._restApiHelper.fetchJSON({
url,
errFn,
@@ -1700,8 +1702,6 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
repoName: RepoName,
errFn?: ErrorCallback
): Promise<ProjectAccessInfo | undefined> {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
return this._restApiHelper.fetchJSON({
url: `/projects/${encodeURIComponent(repoName)}/access`,
errFn,
@@ -1713,27 +1713,29 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
repoName: RepoName,
repoInfo: ProjectAccessInput
): Promise<Response> {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._restApiHelper.send({
- method: HttpMethod.POST,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.POST,
+ body: repoInfo,
+ }),
url: `/projects/${encodeURIComponent(repoName)}/access`,
- body: repoInfo,
anonymizedUrl: '/projects/*/access',
+ reportServerError: true,
});
}
setRepoAccessRightsForReview(
projectName: RepoName,
projectInfo: ProjectAccessInput
- ): Promise<ChangeInfo> {
- return this._restApiHelper.send({
- method: HttpMethod.PUT,
+ ): Promise<ChangeInfo | undefined> {
+ return this._restApiHelper.fetchJSON({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: projectInfo,
+ }),
url: `/projects/${encodeURIComponent(projectName)}/access:review`,
- body: projectInfo,
- parseResponse: true,
anonymizedUrl: '/projects/*/access:review',
- }) as unknown as Promise<ChangeInfo>;
+ }) as unknown as Promise<ChangeInfo | undefined>;
}
getSuggestedGroups(
@@ -1778,7 +1780,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
});
}
- async getSuggestedAccounts(
+ async queryAccounts(
inputVal: string,
n?: number,
canSee?: NumericChangeId,
@@ -1796,7 +1798,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
queryParams.push(`${escapeAndWrapSearchOperatorValue(inputVal)}`);
}
if (canSee) {
- const project = await this.getFromProjectLookup(canSee);
+ const project = await this.getRepoName(canSee);
queryParams.push(`cansee:${project}~${canSee}`);
}
if (filterActive) {
@@ -1815,6 +1817,19 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
}) as Promise<AccountInfo[] | undefined>;
}
+ getAccountSuggestions(inputVal: string): Promise<AccountInfo[] | undefined> {
+ const params: QueryAccountsParams = {suggest: undefined, q: ''};
+ inputVal = inputVal?.trim() ?? '';
+ if (inputVal.length > 0) {
+ params.q = inputVal;
+ }
+ if (!params.q) return Promise.resolve([]);
+ return this._restApiHelper.fetchJSON({
+ url: '/accounts/',
+ params,
+ }) as Promise<AccountInfo[] | undefined>;
+ }
+
addChangeReviewer(
changeNum: NumericChangeId,
reviewerID: AccountId | EmailAddress | GroupId
@@ -1856,32 +1871,36 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
assertNever(method, `Unsupported HTTP method: ${method}`);
}
- return this._restApiHelper.send({method, url, body});
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({method, body}),
+ url,
+ reportServerError: true,
+ });
}
);
}
- getRelatedChanges(
+ async getRelatedChanges(
changeNum: NumericChangeId,
patchNum: PatchSetNum
): Promise<RelatedChangesInfo | undefined> {
const options = '?o=SUBMITTABLE';
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: `/related${options}`,
- revision: patchNum,
- reportEndpointAsIs: true,
+ const url = await this._changeBaseURL(changeNum, patchNum);
+ return this._restApiHelper.fetchJSON({
+ url: `${url}/related${options}`,
+ anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}/related${options}`,
}) as Promise<RelatedChangesInfo | undefined>;
}
- getChangesSubmittedTogether(
+ async getChangesSubmittedTogether(
changeNum: NumericChangeId,
options: string[] = ['NON_VISIBLE_CHANGES']
): Promise<SubmittedTogetherInfo | undefined> {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: `/submitted_together?o=${options.join('&o=')}`,
- reportEndpointAsIs: true,
+ const url = await this._changeBaseURL(changeNum);
+ const endpoint = `/submitted_together?o=${options.join('&o=')}`;
+ return this._restApiHelper.fetchJSON({
+ url: `${url}${endpoint}`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}${endpoint}`,
}) as Promise<SubmittedTogetherInfo | undefined>;
}
@@ -1992,30 +2011,29 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
}) as Promise<ChangeInfo[] | undefined>;
}
- getReviewedFiles(
+ async getReviewedFiles(
changeNum: NumericChangeId,
patchNum: PatchSetNum
): Promise<string[] | undefined> {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/files?reviewed',
- revision: patchNum,
- reportEndpointAsIs: true,
+ const url = await this._changeBaseURL(changeNum, patchNum);
+ return this._restApiHelper.fetchJSON({
+ url: `${url}/files?reviewed`,
+ anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}/files?reviewed`,
}) as Promise<string[] | undefined>;
}
- saveFileReviewed(
+ async saveFileReviewed(
changeNum: NumericChangeId,
patchNum: PatchSetNum,
path: string,
reviewed: boolean
): Promise<Response> {
- return this._getChangeURLAndSend({
- changeNum,
- method: reviewed ? HttpMethod.PUT : HttpMethod.DELETE,
- patchNum,
- endpoint: `/files/${encodeURIComponent(path)}/reviewed`,
- anonymizedEndpoint: '/files/*/reviewed',
+ const url = await this._changeBaseURL(changeNum, patchNum);
+ return this._restApiHelper.fetch({
+ fetchOptions: {method: reviewed ? HttpMethod.PUT : HttpMethod.DELETE},
+ url: `${url}/files/${encodeURIComponent(path)}/reviewed`,
+ anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}/files/*/reviewed`,
+ reportServerError: true,
});
}
@@ -2025,7 +2043,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
review: ReviewInput,
errFn?: ErrorCallback,
fetchDetail?: boolean
- ) {
+ ): Promise<ReviewResult | undefined> {
if (fetchDetail) {
review.response_format_options = await this.getResponseFormatOptions();
}
@@ -2033,41 +2051,39 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
this.awaitPendingDiffDrafts(),
this.getChangeActionURL(changeNum, patchNum, '/review'),
];
- return Promise.all(promises)
- .then(([, url]) =>
- this._restApiHelper.send({
+ return Promise.all(promises).then(([, url]) =>
+ this._restApiHelper.fetchJSON({
+ fetchOptions: getFetchOptions({
method: HttpMethod.POST,
- url,
body: review,
- errFn,
- parseResponse: true,
- })
- )
- .then(payload => {
- if (!payload) {
- return undefined;
- }
- return payload as unknown as ReviewResult;
- });
+ }),
+ url,
+ errFn,
+ })
+ ) as unknown as Promise<ReviewResult | undefined>;
}
- getChangeEdit(changeNum?: NumericChangeId): Promise<EditInfo | undefined> {
- if (!changeNum) return Promise.resolve(undefined);
+ async getChangeEdit(
+ changeNum?: NumericChangeId
+ ): Promise<EditInfo | undefined> {
+ if (!changeNum) return undefined;
const params = {'download-commands': true};
- return this.getLoggedIn().then(loggedIn => {
- if (!loggedIn) {
- return Promise.resolve(undefined);
- }
- return this._getChangeURLAndFetch(
- {
- changeNum,
- endpoint: '/edit/',
- params,
- reportEndpointAsIs: true,
- },
- true
- ) as Promise<EditInfo | undefined>;
+ const loggedIn = await this.getLoggedIn();
+ if (!loggedIn) {
+ return undefined;
+ }
+ const url = await this._changeBaseURL(changeNum);
+ const response = await this._restApiHelper.fetch({
+ url: `${url}/edit/`,
+ params,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/edit/`,
});
+ // If there is no edit patchset 204 is returned.
+ if (!response.ok || response.status === 204) {
+ return undefined;
+ }
+ return (await readJSONResponsePayload(response))
+ .parsed as unknown as EditInfo;
}
createChange(
@@ -2079,21 +2095,22 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
workInProgress?: boolean,
baseChange?: ChangeId,
baseCommit?: string
- ) {
- return this._restApiHelper.send({
- method: HttpMethod.POST,
+ ): Promise<ChangeInfo | undefined> {
+ return this._restApiHelper.fetchJSON({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.POST,
+ body: {
+ project: repo,
+ branch,
+ subject,
+ topic,
+ is_private: isPrivate,
+ work_in_progress: workInProgress,
+ base_change: baseChange,
+ base_commit: baseCommit,
+ },
+ }),
url: '/changes/',
- body: {
- project: repo,
- branch,
- subject,
- topic,
- is_private: isPrivate,
- work_in_progress: workInProgress,
- base_change: baseChange,
- base_commit: baseCommit,
- },
- parseResponse: true,
reportUrlAsIs: true,
}) as unknown as Promise<ChangeInfo | undefined>;
}
@@ -2111,15 +2128,15 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
: this._getFileInRevision(changeNum, path, patchNum, suppress404s);
return promise.then(res => {
- if (!res || !res.ok) {
+ if (!res.ok) {
return res;
}
// The file type (used for syntax highlighting) is identified in the
// X-FYI-Content-Type header of the response.
const type = res.headers.get('X-FYI-Content-Type');
- return this.getResponseObject(res).then(content => {
- const strContent = content as unknown as string | null;
+ return readJSONResponsePayload(res).then(content => {
+ const strContent = content.parsed as unknown as string | null;
return {content: strContent, type, ok: true};
});
});
@@ -2128,200 +2145,260 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
/**
* Gets a file in a specific change and revision.
*/
- _getFileInRevision(
+ async _getFileInRevision(
changeNum: NumericChangeId,
path: string,
patchNum: PatchSetNum,
errFn?: ErrorCallback
- ) {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.GET,
- patchNum,
- endpoint: `/files/${encodeURIComponent(path)}/content`,
+ ): Promise<Response> {
+ const url = await this._changeBaseURL(changeNum, patchNum);
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ headers: {Accept: 'application/json'},
+ }),
+ url: `${url}/files/${encodeURIComponent(path)}/content`,
errFn,
- headers: {Accept: 'application/json'},
- anonymizedEndpoint: '/files/*/content',
+ anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}/files/*/content`,
+ reportServerError: true,
});
}
/**
* Gets a file in a change edit.
*/
- _getFileInChangeEdit(changeNum: NumericChangeId, path: string) {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.GET,
- endpoint: '/edit/' + encodeURIComponent(path),
- headers: {Accept: 'application/json'},
- anonymizedEndpoint: '/edit/*',
+ async _getFileInChangeEdit(
+ changeNum: NumericChangeId,
+ path: string
+ ): Promise<Response> {
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ headers: {Accept: 'application/json'},
+ }),
+ url: `${url}/edit/${encodeURIComponent(path)}`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/edit/*`,
+ reportServerError: true,
});
}
- rebaseChangeEdit(changeNum: NumericChangeId) {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.POST,
- endpoint: '/edit:rebase',
- reportEndpointAsIs: true,
+ async rebaseChangeEdit(changeNum: NumericChangeId): Promise<Response> {
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetch({
+ fetchOptions: {method: HttpMethod.POST},
+ url: `${url}/edit:rebase`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/edit:rebase`,
+ reportServerError: true,
});
}
- deleteChangeEdit(changeNum: NumericChangeId) {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.DELETE,
- endpoint: '/edit',
- reportEndpointAsIs: true,
+ async deleteChangeEdit(changeNum: NumericChangeId): Promise<Response> {
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetch({
+ fetchOptions: {method: HttpMethod.DELETE},
+ url: `${url}/edit`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/edit`,
+ reportServerError: true,
});
}
- restoreFileInChangeEdit(changeNum: NumericChangeId, restore_path: string) {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.POST,
- endpoint: '/edit',
- body: {restore_path},
- reportEndpointAsIs: true,
+ async restoreFileInChangeEdit(
+ changeNum: NumericChangeId,
+ restore_path: string
+ ): Promise<Response> {
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.POST,
+ body: {restore_path},
+ }),
+ url: `${url}/edit`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/edit`,
+ reportServerError: true,
});
}
- renameFileInChangeEdit(
+ async renameFileInChangeEdit(
changeNum: NumericChangeId,
old_path: string,
new_path: string
- ) {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.POST,
- endpoint: '/edit',
- body: {old_path, new_path},
- reportEndpointAsIs: true,
+ ): Promise<Response> {
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.POST,
+ body: {old_path, new_path},
+ }),
+ url: `${url}/edit`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/edit`,
+ reportServerError: true,
});
}
- deleteFileInChangeEdit(changeNum: NumericChangeId, path: string) {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.DELETE,
- endpoint: '/edit/' + encodeURIComponent(path),
- anonymizedEndpoint: '/edit/*',
+ async deleteFileInChangeEdit(
+ changeNum: NumericChangeId,
+ path: string
+ ): Promise<Response> {
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetch({
+ fetchOptions: {method: HttpMethod.DELETE},
+ url: `${url}/edit/${encodeURIComponent(path)}`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/edit/*`,
+ reportServerError: true,
});
}
- saveChangeEdit(changeNum: NumericChangeId, path: string, contents: string) {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.PUT,
- endpoint: '/edit/' + encodeURIComponent(path),
- body: contents,
- contentType: 'text/plain',
- anonymizedEndpoint: '/edit/*',
+ async saveChangeEdit(
+ changeNum: NumericChangeId,
+ path: string,
+ contents: string
+ ): Promise<Response> {
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: contents,
+ contentType: 'text/plain',
+ }),
+ url: `${url}/edit/${encodeURIComponent(path)}`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/edit/*`,
+ reportServerError: true,
});
}
- saveFileUploadChangeEdit(
+ async saveFileUploadChangeEdit(
changeNum: NumericChangeId,
path: string,
content: string
- ) {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.PUT,
- endpoint: '/edit/' + encodeURIComponent(path),
- body: {binary_content: content},
- anonymizedEndpoint: '/edit/*',
+ ): Promise<Response> {
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: {binary_content: content},
+ }),
+ url: `${url}/edit/${encodeURIComponent(path)}`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/edit/*`,
+ reportServerError: true,
});
}
- getFixPreview(
+ async getFixPreview(
changeNum: NumericChangeId,
patchNum: PatchSetNum,
fixReplacementInfos: FixReplacementInfo[]
): Promise<FilePathToDiffInfoMap | undefined> {
- return this._getChangeURLAndSend({
- method: HttpMethod.POST,
- changeNum,
- patchNum,
- endpoint: '/fix:preview',
- reportEndpointAsId: true,
- headers: {Accept: 'application/json'},
- parseResponse: true,
- body: {fix_replacement_infos: fixReplacementInfos},
+ const url = await this._changeBaseURL(changeNum, patchNum);
+ return this._restApiHelper.fetchJSON({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.POST,
+ body: {fix_replacement_infos: fixReplacementInfos},
+ }),
+ url: `${url}/fix:preview`,
+ anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}/fix:preview`,
}) as Promise<FilePathToDiffInfoMap | undefined>;
}
- getRobotCommentFixPreview(
+ async getRobotCommentFixPreview(
changeNum: NumericChangeId,
patchNum: PatchSetNum,
fixId: FixId
): Promise<FilePathToDiffInfoMap | undefined> {
- return this._getChangeURLAndFetch({
- changeNum,
- revision: patchNum,
- endpoint: `/fixes/${encodeURIComponent(fixId)}/preview`,
- reportEndpointAsId: true,
+ const url = await this._changeBaseURL(changeNum, patchNum);
+ const endpoint = `/fixes/${encodeURIComponent(fixId)}/preview`;
+ return this._restApiHelper.fetchJSON({
+ url: `${url}${endpoint}`,
+ anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}${endpoint}`,
}) as Promise<FilePathToDiffInfoMap | undefined>;
}
- applyFixSuggestion(
+ async applyFixSuggestion(
changeNum: NumericChangeId,
patchNum: PatchSetNum,
fixReplacementInfos: FixReplacementInfo[]
): Promise<Response> {
- return this._getChangeURLAndSend({
- method: HttpMethod.POST,
- changeNum,
- patchNum,
- endpoint: '/fix:apply',
- reportEndpointAsId: true,
- headers: {Accept: 'application/json'},
- body: {fix_replacement_infos: fixReplacementInfos},
+ const url = await this._changeBaseURL(changeNum, patchNum);
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.POST,
+ headers: {Accept: 'application/json'},
+ body: {fix_replacement_infos: fixReplacementInfos},
+ }),
+ url: `${url}/fix:apply`,
+ anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}/fix:apply`,
+ reportServerError: true,
});
}
- applyRobotFixSuggestion(
+ async applyRobotFixSuggestion(
changeNum: NumericChangeId,
patchNum: PatchSetNum,
fixId: string
): Promise<Response> {
- return this._getChangeURLAndSend({
- method: HttpMethod.POST,
- changeNum,
- patchNum,
- endpoint: `/fixes/${encodeURIComponent(fixId)}/apply`,
- reportEndpointAsId: true,
+ const url = await this._changeBaseURL(changeNum, patchNum);
+ const endpoint = `/fixes/${encodeURIComponent(fixId)}/apply`;
+ return this._restApiHelper.fetch({
+ fetchOptions: {method: HttpMethod.POST},
+ url: `${url}${endpoint}`,
+ anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}${endpoint}`,
+ reportServerError: true,
});
}
- publishChangeEdit(changeNum: NumericChangeId) {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.POST,
- endpoint: '/edit:publish',
- reportEndpointAsIs: true,
+ async publishChangeEdit(changeNum: NumericChangeId) {
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetch({
+ fetchOptions: {method: HttpMethod.POST},
+ url: `${url}/edit:publish`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/edit:publish`,
+ reportServerError: true,
});
}
- putChangeCommitMessage(changeNum: NumericChangeId, message: string) {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.PUT,
- endpoint: '/message',
- body: {message},
- reportEndpointAsIs: true,
+ async putChangeCommitMessage(
+ changeNum: NumericChangeId,
+ message: string,
+ committerEmail: string | null
+ ) {
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: {message, committer_email: committerEmail},
+ }),
+ url: `${url}/message`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/message`,
+ reportServerError: true,
+ });
+ }
+
+ async updateIdentityInChangeEdit(
+ changeNum: NumericChangeId,
+ name: string,
+ email: string,
+ type: string
+ ) {
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: {name, email, type},
+ }),
+ url: `${url}/edit:identity`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/edit:identity`,
+ reportServerError: true,
});
}
- deleteChangeCommitMessage(
+ async deleteChangeCommitMessage(
changeNum: NumericChangeId,
messageId: ChangeMessageId
) {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.DELETE,
- endpoint: `/messages/${messageId}`,
- reportEndpointAsIs: true,
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetch({
+ fetchOptions: {method: HttpMethod.DELETE},
+ url: `${url}/messages/${messageId}`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/messages/${messageId}`,
+ reportServerError: true,
});
}
@@ -2332,12 +2409,12 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
// Some servers may require the project name to be provided
// alongside the change number, so resolve the project name
// first.
- return this.getFromProjectLookup(changeNum).then(project => {
- const encodedRepoName = project ? encodeURIComponent(project) + '~' : '';
+ return this.getRepoName(changeNum).then(project => {
+ const encodedRepoName = encodeURIComponent(project) + '~';
const url = `/accounts/self/starred.changes/${encodedRepoName}${changeNum}`;
return this._serialScheduler.schedule(() =>
- this._restApiHelper.send({
- method: starred ? HttpMethod.PUT : HttpMethod.DELETE,
+ this._restApiHelper.fetch({
+ fetchOptions: {method: starred ? HttpMethod.PUT : HttpMethod.DELETE},
url,
anonymizedUrl: '/accounts/self/starred.changes/*',
})
@@ -2378,24 +2455,27 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
contentType?: string,
headers?: Record<string, string>
): Promise<Response | undefined> {
- return this._restApiHelper.send({
- method,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method,
+ body,
+ contentType,
+ headers,
+ }),
url,
- body,
errFn,
- contentType,
- headers,
+ reportServerError: true,
});
}
- getDiff(
+ async getDiff(
changeNum: NumericChangeId,
basePatchNum: PatchSetNum,
patchNum: PatchSetNum,
path: string,
whitespace?: IgnoreWhitespaceType,
errFn?: ErrorCallback
- ) {
+ ): Promise<DiffInfo | undefined> {
const params: GetDiffParams = {
intraline: null,
whitespace: whitespace || 'IGNORE_NONE',
@@ -2405,24 +2485,19 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
} else if (basePatchNum !== PARENT) {
params.base = basePatchNum;
}
- const endpoint = `/files/${encodeURIComponent(path)}/diff`;
- const req: FetchChangeJSON = {
- changeNum,
- endpoint,
- revision: patchNum,
- errFn,
- params,
- anonymizedEndpoint: '/files/*/diff',
- };
- // Invalidate the cache if its edit patch to make sure we always get latest.
- if (patchNum === EDIT) {
- if (!req.fetchOptions) req.fetchOptions = {};
- if (!req.fetchOptions.headers) req.fetchOptions.headers = new Headers();
- req.fetchOptions.headers.append('Cache-Control', 'no-cache');
- }
-
- return this._getChangeURLAndFetch(req) as Promise<DiffInfo | undefined>;
+ const url = await this._changeBaseURL(changeNum, patchNum);
+ return this._restApiHelper.fetchJSON({
+ // Invalidate the cache if this is the edit patch to make sure we always
+ // get latest.
+ fetchOptions: getFetchOptions({
+ headers: patchNum === EDIT ? {'Cache-Control': 'no-cache'} : undefined,
+ }),
+ url: `${url}/files/${encodeURIComponent(path)}/diff`,
+ params,
+ errFn,
+ anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}'/files/*/diff`,
+ }) as Promise<DiffInfo | undefined>;
}
getDiffComments(
@@ -2553,6 +2628,10 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
path?: string
): Promise<GetDiffRobotCommentsOutput>;
+ /**
+ * Fetches the comments for a given patchNum.
+ * Helper function to make promises more legible.
+ */
_getDiffComments(
changeNum: NumericChangeId,
endpoint: string,
@@ -2567,23 +2646,19 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
| PathToRobotCommentsInfoMap
| undefined
> {
- /**
- * Fetches the comments for a given patchNum.
- * Helper function to make promises more legible.
- */
// We don't want to add accept header, since preloading of comments is
// working only without accept header.
const noAcceptHeader = true;
const fetchComments = (patchNum?: PatchSetNum) =>
- this._getChangeURLAndFetch(
- {
- changeNum,
- endpoint,
- revision: patchNum,
- reportEndpointAsIs: true,
- params,
- },
- noAcceptHeader
+ this._changeBaseURL(changeNum, patchNum).then(url =>
+ this._restApiHelper.fetchJSON(
+ {
+ url: `${url}${endpoint}`,
+ anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}${endpoint}`,
+ params,
+ },
+ noAcceptHeader
+ )
) as Promise<
{[path: string]: CommentInfo[]} | PathToRobotCommentsInfoMap | undefined
>;
@@ -2607,8 +2682,6 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
let fetchPromise;
fetchPromise = fetchComments(patchNum).then(response => {
comments = (response && path && response[path]) || [];
- // TODO(kaspern): Implement this on in the backend so this can
- // be removed.
// Sort comments by date so that parent ranges can be propagated
// in a single pass.
comments = this._setRanges(comments);
@@ -2642,15 +2715,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
);
}
- _getDiffCommentsFetchURL(
- changeNum: NumericChangeId,
- endpoint: string,
- patchNum?: RevisionId
- ) {
- return this._changeBaseURL(changeNum, patchNum).then(url => url + endpoint);
- }
-
- getPortedComments(
+ async getPortedComments(
changeNum: NumericChangeId,
revision: RevisionId
): Promise<{[path: string]: CommentInfo[]} | undefined> {
@@ -2659,10 +2724,9 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
if (response)
console.info(`Fetching ported comments failed, ${response.status}`);
};
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/ported_comments/',
- revision,
+ const url = await this._changeBaseURL(changeNum, revision);
+ return this._restApiHelper.fetchJSON({
+ url: `${url}/ported_comments/`,
errFn,
});
}
@@ -2678,10 +2742,9 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
};
const loggedIn = await this.getLoggedIn();
if (!loggedIn) return {};
- const comments = (await this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/ported_drafts/',
- revision,
+ const url = await this._changeBaseURL(changeNum, revision);
+ const comments = (await this._restApiHelper.fetchJSON({
+ url: `${url}/ported_drafts/`,
errFn,
})) as {[path: string]: CommentInfo[]} | undefined;
return addDraftProp(comments);
@@ -2753,25 +2816,25 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
endpoint += `/${draft.id}`;
anonymizedEndpoint += '/*';
}
- let body;
- if (method === HttpMethod.PUT) {
- body = draft;
- }
if (!this._pendingRequests[Requests.SEND_DIFF_DRAFT]) {
this._pendingRequests[Requests.SEND_DIFF_DRAFT] = [];
}
- const req = {
- changeNum,
- method,
- patchNum,
- endpoint,
- body,
- anonymizedEndpoint,
- };
+ const fetchOptions =
+ method === HttpMethod.PUT
+ ? getFetchOptions({method, body: draft})
+ : {method};
+
+ const promise = this._changeBaseURL(changeNum, patchNum).then(url =>
+ this._restApiHelper.fetch({
+ fetchOptions,
+ url: `${url}${endpoint}`,
+ anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}${anonymizedEndpoint}`,
+ reportServerError: true,
+ })
+ );
- const promise = this._getChangeURLAndSend(req);
this._pendingRequests[Requests.SEND_DIFF_DRAFT].push(promise);
if (isCreate) {
@@ -2888,11 +2951,8 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
changeNum: NumericChangeId,
revisionId?: RevisionId
): Promise<string> {
- return this.getFromProjectLookup(changeNum).then(project => {
- // TODO(TS): unclear why project can't be null here. Fix it
- let url = `/changes/${encodeURIComponent(
- project as RepoName
- )}~${changeNum}`;
+ return this.getRepoName(changeNum).then(project => {
+ let url = `/changes/${encodeURIComponent(project)}~${changeNum}`;
if (revisionId) {
url += `/revisions/${revisionId}`;
}
@@ -2900,115 +2960,136 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
});
}
- addToAttentionSet(
+ async addToAttentionSet(
changeNum: NumericChangeId,
user: AccountId | undefined | null,
reason: string
- ) {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.POST,
- endpoint: '/attention',
- body: {user, reason},
- reportUrlAsIs: true,
+ ): Promise<Response> {
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.POST,
+ body: {user, reason},
+ }),
+ url: `${url}/attention`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/attention`,
+ reportServerError: true,
});
}
- removeFromAttentionSet(
+ async removeFromAttentionSet(
changeNum: NumericChangeId,
user: AccountId,
reason: string
- ) {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.DELETE,
- endpoint: `/attention/${user}`,
- anonymizedEndpoint: '/attention/*',
- body: {reason},
+ ): Promise<Response> {
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.DELETE,
+ body: {reason},
+ }),
+ url: `${url}/attention/${user}`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/attention/*`,
+ reportServerError: true,
});
}
- setChangeTopic(changeNum: NumericChangeId, topic?: string): Promise<string> {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.PUT,
- endpoint: '/topic',
- body: {topic},
- parseResponse: true,
- reportUrlAsIs: true,
- }) as unknown as Promise<string>;
+ async setChangeTopic(
+ changeNum: NumericChangeId,
+ topic?: string,
+ errFn?: ErrorCallback
+ ): Promise<string | undefined> {
+ const url = await this._changeBaseURL(changeNum);
+ const response = await this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: {topic},
+ }),
+ url: `${url}/topic`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/topic`,
+ errFn,
+ });
+ if (!response.ok) {
+ return undefined;
+ }
+ if (response.status === 204) {
+ return '';
+ }
+ return (await readJSONResponsePayload(response))
+ .parsed as unknown as string;
}
- setChangeHashtag(
+ removeChangeTopic(
changeNum: NumericChangeId,
- hashtag: HashtagsInput
- ): Promise<Hashtag[]> {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.POST,
- endpoint: '/hashtags',
- body: hashtag,
- parseResponse: true,
- reportUrlAsIs: true,
- }) as unknown as Promise<Hashtag[]>;
+ errFn?: ErrorCallback
+ ): Promise<string | undefined> {
+ return this.setChangeTopic(changeNum, '', errFn);
+ }
+
+ async setChangeHashtag(
+ changeNum: NumericChangeId,
+ hashtag: HashtagsInput,
+ errFn?: ErrorCallback
+ ): Promise<Hashtag[] | undefined> {
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetchJSON({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.POST,
+ body: hashtag,
+ }),
+ url: `${url}/hashtags`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/hashtags`,
+ errFn,
+ }) as unknown as Promise<Hashtag[] | undefined>;
}
- deleteAccountHttpPassword() {
- return this._restApiHelper.send({
- method: HttpMethod.DELETE,
+ deleteAccountHttpPassword(): Promise<Response> {
+ return this._restApiHelper.fetch({
+ fetchOptions: {method: HttpMethod.DELETE},
url: '/accounts/self/password.http',
reportUrlAsIs: true,
+ reportServerError: true,
});
}
- generateAccountHttpPassword(): Promise<Password> {
- return this._restApiHelper.send({
- method: HttpMethod.PUT,
+ generateAccountHttpPassword(): Promise<Password | undefined> {
+ return this._restApiHelper.fetchJSON({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: {generate: true},
+ }),
url: '/accounts/self/password.http',
- body: {generate: true},
- parseResponse: true,
reportUrlAsIs: true,
}) as Promise<unknown> as Promise<Password>;
}
getAccountSSHKeys() {
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: '/accounts/self/sshkeys',
reportUrlAsIs: true,
}) as Promise<unknown> as Promise<SshKeyInfo[] | undefined>;
}
addAccountSSHKey(key: string): Promise<SshKeyInfo> {
- const req = {
- method: HttpMethod.POST,
+ // By passing throwingErrorCallback we guarantee that response is not-null.
+ return this._restApiHelper.fetchJSON({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.POST,
+ body: key,
+ contentType: 'text/plain',
+ }),
url: '/accounts/self/sshkeys',
- body: key,
- contentType: 'text/plain',
reportUrlAsIs: true,
- };
- return this._restApiHelper
- .send(req)
- .then((response: Response | undefined) => {
- if (!response || (response.status < 200 && response.status >= 300)) {
- return Promise.reject(new Error('error'));
- }
- return this.getResponseObject(
- response
- ) as unknown as Promise<SshKeyInfo>;
- })
- .then(obj => {
- if (!obj || !obj.valid) {
- return Promise.reject(new Error('error'));
- }
- return obj;
- });
+ errFn: throwingErrorCallback,
+ }) as Promise<unknown> as Promise<SshKeyInfo>;
}
- deleteAccountSSHKey(id: string) {
- return this._restApiHelper.send({
- method: HttpMethod.DELETE,
+ deleteAccountSSHKey(id: string): Promise<Response> {
+ return this._restApiHelper.fetch({
+ fetchOptions: {method: HttpMethod.DELETE},
url: '/accounts/self/sshkeys/' + id,
anonymizedUrl: '/accounts/self/sshkeys/*',
+ reportServerError: true,
});
}
@@ -3019,74 +3100,73 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
}) as Promise<unknown> as Promise<Record<string, GpgKeyInfo>>;
}
- addAccountGPGKey(key: GpgKeysInput) {
- const req = {
- method: HttpMethod.POST,
+ addAccountGPGKey(key: GpgKeysInput): Promise<Record<string, GpgKeyInfo>> {
+ // By passing throwingErrorCallback we guarantee that response is not-null.
+ return this._restApiHelper.fetchJSON({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.POST,
+ body: key,
+ }),
url: '/accounts/self/gpgkeys',
- body: key,
reportUrlAsIs: true,
- };
- return this._restApiHelper
- .send(req)
- .then(response => {
- if (!response || (response.status < 200 && response.status >= 300)) {
- return Promise.reject(new Error('error'));
- }
- return this.getResponseObject(response);
- })
- .then(obj => {
- if (!obj) {
- return Promise.reject(new Error('error'));
- }
- return obj;
- });
+ errFn: throwingErrorCallback,
+ }) as Promise<unknown> as Promise<Record<string, GpgKeyInfo>>;
}
deleteAccountGPGKey(id: GpgKeyId) {
- return this._restApiHelper.send({
- method: HttpMethod.DELETE,
+ return this._restApiHelper.fetch({
+ fetchOptions: {method: HttpMethod.DELETE},
url: `/accounts/self/gpgkeys/${id}`,
anonymizedUrl: '/accounts/self/gpgkeys/*',
+ reportServerError: true,
});
}
- deleteVote(changeNum: NumericChangeId, account: AccountId, label: string) {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.DELETE,
- endpoint: `/reviewers/${account}/votes/${encodeURIComponent(label)}`,
- anonymizedEndpoint: '/reviewers/*/votes/*',
+ async deleteVote(
+ changeNum: NumericChangeId,
+ account: AccountId,
+ label: string
+ ): Promise<Response> {
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetch({
+ fetchOptions: {method: HttpMethod.DELETE},
+ url: `${url}/reviewers/${account}/votes/${encodeURIComponent(label)}`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/reviewers/*/votes/*`,
+ reportServerError: true,
});
}
- setDescription(
+ async setDescription(
changeNum: NumericChangeId,
patchNum: PatchSetNum,
desc: string
- ) {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.PUT,
- patchNum,
- endpoint: '/description',
- body: {description: desc},
- reportUrlAsIs: true,
+ ): Promise<Response> {
+ const url = await this._changeBaseURL(changeNum, patchNum);
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: {description: desc},
+ }),
+ url: `${url}/description`,
+ anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}/description`,
+ reportServerError: true,
});
}
- confirmEmail(token: string): Promise<string | null> {
- const req = {
- method: HttpMethod.PUT,
+ async confirmEmail(token: string): Promise<string | null> {
+ const response = await this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.PUT,
+ body: {token},
+ }),
url: '/config/server/email.confirm',
- body: {token},
reportUrlAsIs: true,
- };
- return this._restApiHelper.send(req).then(response => {
- if (response?.status === 204) {
- return 'Email confirmed successfully.';
- }
- return null;
+ reportServerError: true,
});
+ if (response?.status === 204) {
+ return 'Email confirmed successfully.';
+ }
+ return null;
}
getCapabilities(
@@ -3100,7 +3180,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
}
getTopMenus(): Promise<TopMenuEntryInfo[] | undefined> {
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: '/config/server/top-menus',
reportUrlAsIs: true,
}) as Promise<TopMenuEntryInfo[] | undefined>;
@@ -3112,41 +3192,41 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
);
}
- startWorkInProgress(
+ async startWorkInProgress(
changeNum: NumericChangeId,
message?: string
): Promise<string | undefined> {
- const body = message ? {message} : {};
- const req: SendRawChangeRequest = {
- changeNum,
- method: HttpMethod.POST,
- endpoint: '/wip',
- body,
- reportUrlAsIs: true,
- };
- return this._getChangeURLAndSend(req).then(response => {
- if (response?.status === 204) {
- return 'Change marked as Work In Progress.';
- }
- return undefined;
- });
+ const url = await this._changeBaseURL(changeNum);
+ const response = await this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.POST,
+ body: message ? {message} : {},
+ }),
+ url: `${url}/wip`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/wip`,
+ reportServerError: true,
+ });
+ if (response.status === 204) {
+ return 'Change marked as Work In Progress.';
+ }
+ return undefined;
}
- deleteComment(
+ async deleteComment(
changeNum: NumericChangeId,
patchNum: PatchSetNum,
commentID: UrlEncodedCommentId,
reason: string
- ) {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.POST,
- patchNum,
- endpoint: `/comments/${commentID}/delete`,
- body: {reason},
- parseResponse: true,
- anonymizedEndpoint: '/comments/*/delete',
- }) as unknown as Promise<CommentInfo>;
+ ): Promise<CommentInfo | undefined> {
+ const url = await this._changeBaseURL(changeNum, patchNum);
+ return this._restApiHelper.fetchJSON({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.POST,
+ body: {reason},
+ }),
+ url: `${url}/comments/${commentID}/delete`,
+ anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}/comments/*/delete`,
+ }) as unknown as Promise<CommentInfo | undefined>;
}
getChange(
@@ -3193,14 +3273,12 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
* Then we don't need to make a dedicated REST API call or have a fallback,
* if that fails.
*/
- setInProjectLookup(changeNum: NumericChangeId, project: RepoName) {
+ addRepoNameToCache(changeNum: NumericChangeId, project: RepoName) {
this._projectLookup[changeNum] = Promise.resolve(project);
}
- getFromProjectLookup(
- changeNum: NumericChangeId
- ): Promise<RepoName | undefined> {
- // Hopefully setInProjectLookup() has already been called. Then we don't
+ getRepoName(changeNum: NumericChangeId): Promise<RepoName> {
+ // Hopefully addRepoNameToCache() has already been called. Then we don't
// have to make a dedicated REST API call to look up the project.
let projectPromise = this._projectLookup[changeNum];
if (projectPromise) return projectPromise;
@@ -3212,9 +3290,9 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
// In the very rare case that the change index cannot provide an answer
// (e.g. stale index) we should check, if the router has called
- // setInProjectLookup() in the meantime. Then we can fall back to that.
+ // addRepoNameToCache() in the meantime. Then we can fall back to that.
const currentProjectPromise = this._projectLookup[changeNum];
- if (currentProjectPromise !== projectPromise) {
+ if (currentProjectPromise && currentProjectPromise !== projectPromise) {
return currentProjectPromise;
}
@@ -3225,124 +3303,49 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
{status: 404}
)
);
- return undefined;
+ // Don't store failed lookups in the lookup.
+ this._projectLookup[changeNum] = undefined;
+ throw new Error(
+ `Failed to lookup the repo for change number ${changeNum}`
+ );
});
this._projectLookup[changeNum] = projectPromise;
return projectPromise;
}
- // if errFn is not set, then only Response possible
- _getChangeURLAndSend(
- req: SendRawChangeRequest & {errFn?: undefined}
- ): Promise<Response>;
-
- _getChangeURLAndSend(
- req: SendRawChangeRequest
- ): Promise<Response | undefined>;
-
- _getChangeURLAndSend(req: SendJSONChangeRequest): Promise<ParsedJSON>;
-
- _getChangeURLAndSend(
- req: SendChangeRequest
- ): Promise<ParsedJSON | Response | undefined> {
- const anonymizedBaseUrl = req.patchNum
- ? ANONYMIZED_REVISION_BASE_URL
- : ANONYMIZED_CHANGE_BASE_URL;
- const anonymizedEndpoint = req.reportEndpointAsIs
- ? req.endpoint
- : req.anonymizedEndpoint;
-
- return this._changeBaseURL(req.changeNum, req.patchNum).then(url => {
- const request: SendRequest = {
- method: req.method,
- url: url + req.endpoint,
- body: req.body,
- errFn: req.errFn,
- contentType: req.contentType,
- headers: req.headers,
- parseResponse: req.parseResponse,
- anonymizedUrl: anonymizedEndpoint
- ? `${anonymizedBaseUrl}${anonymizedEndpoint}`
- : undefined,
- };
- return this._restApiHelper.send(request);
- });
- }
-
- _getChangeURLAndFetch(
- req: FetchChangeJSON,
- noAcceptHeader?: boolean
- ): Promise<ParsedJSON | undefined> {
- const anonymizedEndpoint = req.reportEndpointAsIs
- ? req.endpoint
- : req.anonymizedEndpoint;
- const anonymizedBaseUrl = req.revision
- ? ANONYMIZED_REVISION_BASE_URL
- : ANONYMIZED_CHANGE_BASE_URL;
- return this._changeBaseURL(req.changeNum, req.revision).then(url =>
- this._restApiHelper.fetchJSON(
- {
- url: url + req.endpoint,
- errFn: req.errFn,
- params: req.params,
- fetchOptions: req.fetchOptions,
- anonymizedUrl: anonymizedEndpoint
- ? anonymizedBaseUrl + anonymizedEndpoint
- : undefined,
- },
- noAcceptHeader
- )
- );
- }
-
- executeChangeAction(
- changeNum: NumericChangeId,
- method: HttpMethod | undefined,
- endpoint: string,
- patchNum?: PatchSetNum,
- payload?: RequestPayload
- ): Promise<Response>;
-
- executeChangeAction(
- changeNum: NumericChangeId,
- method: HttpMethod | undefined,
- endpoint: string,
- patchNum: PatchSetNum | undefined,
- payload: RequestPayload | undefined,
- errFn: ErrorCallback
- ): Promise<Response | undefined>;
-
- executeChangeAction(
+ async executeChangeAction(
changeNum: NumericChangeId,
method: HttpMethod | undefined,
endpoint: string,
patchNum?: PatchSetNum,
payload?: RequestPayload,
errFn?: ErrorCallback
- ) {
- return this._getChangeURLAndSend({
- changeNum,
- method,
- patchNum,
- endpoint,
- body: payload,
+ ): Promise<Response> {
+ const url = await this._changeBaseURL(changeNum, patchNum);
+ // No anonymizedUrl specified so the request will not be logged.
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method,
+ body: payload,
+ }),
+ url: url + endpoint,
errFn,
+ reportServerError: true,
});
}
- getBlame(
+ async getBlame(
changeNum: NumericChangeId,
patchNum: PatchSetNum,
path: string,
base?: boolean
- ) {
+ ): Promise<BlameInfo[] | undefined> {
const encodedPath = encodeURIComponent(path);
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: `/files/${encodedPath}/blame`,
- revision: patchNum,
+ const url = await this._changeBaseURL(changeNum, patchNum);
+ return this._restApiHelper.fetchJSON({
+ url: `${url}/files/${encodedPath}/blame`,
params: base ? {base: 't'} : undefined,
- anonymizedEndpoint: '/files/*/blame',
+ anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}/files/*/blame`,
}) as Promise<BlameInfo[] | undefined>;
}
@@ -3390,7 +3393,7 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
encodeURIComponent(repo) +
'/dashboards/' +
encodeURIComponent(dashboard);
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url,
errFn,
anonymizedUrl: '/projects/*/dashboards/*',
@@ -3401,28 +3404,30 @@ export class GrRestApiServiceImpl implements RestApiService, Finalizable {
filter = filter.trim();
const encodedFilter = encodeURIComponent(filter);
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._fetchSharedCacheURL({
+ return this._restApiHelper.fetchCacheJSON({
url: `/Documentation/?q=${encodedFilter}`,
anonymizedUrl: '/Documentation/?*',
}) as Promise<DocResult[] | undefined>;
}
- getMergeable(changeNum: NumericChangeId) {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/revisions/current/mergeable',
- reportEndpointAsIs: true,
+ async getMergeable(
+ changeNum: NumericChangeId
+ ): Promise<MergeableInfo | undefined> {
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetchJSON({
+ url: `${url}/revisions/current/mergeable`,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/revisions/current/mergeable`,
}) as Promise<MergeableInfo | undefined>;
}
deleteDraftComments(query: string): Promise<Response> {
const body: DeleteDraftCommentsInput = {query};
- return this._restApiHelper.send({
- method: HttpMethod.POST,
+ return this._restApiHelper.fetch({
+ fetchOptions: getFetchOptions({
+ method: HttpMethod.POST,
+ body,
+ }),
url: '/accounts/self/drafts:delete',
- body,
});
}
}
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts
index fe52529539..af12a9cc0d 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts
@@ -7,6 +7,7 @@ import '../../test/common-test-setup';
import {
addListenerForTest,
assertFails,
+ makePrefixedJSON,
MockPromise,
mockPromise,
waitEventLoop,
@@ -15,17 +16,15 @@ import {GrReviewerUpdatesParser} from '../../elements/shared/gr-rest-api-interfa
import {listChangesOptionsToHex} from '../../utils/change-util';
import {
createAccountDetailWithId,
+ createAccountWithId,
createChange,
createComment,
+ createEditInfo,
createParsedChange,
createServerInfo,
+ TEST_PROJECT_NAME,
} from '../../test/test-data-generators';
import {CURRENT} from '../../utils/patch-set-util';
-import {
- parsePrefixedJSON,
- readResponsePayload,
- JSON_PREFIX,
-} from '../../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
import {GrRestApiServiceImpl} from './gr-rest-api-impl';
import {
CommentSide,
@@ -33,24 +32,21 @@ import {
HttpMethod,
} from '../../constants/constants';
import {
+ AccountDetailInfo,
BasePatchSetNum,
ChangeInfo,
ChangeMessageId,
CommentInfo,
+ CommentInput,
DashboardId,
- DiffPreferenceInput,
EDIT,
- EditPreferencesInfo,
Hashtag,
- HashtagsInput,
ListChangesOption,
NumericChangeId,
PARENT,
ParsedJSON,
PatchSetNum,
- PreferencesInfo,
RepoName,
- RevisionId,
RevisionPatchSetNum,
RobotCommentInfo,
Timestamp,
@@ -59,7 +55,6 @@ import {
import {assert} from '@open-wc/testing';
import {AuthService} from '../gr-auth/gr-auth';
import {GrAuthMock} from '../gr-auth/gr-auth_mock';
-import {getBaseUrl} from '../../utils/url-util';
import {FlagsServiceImplementation} from '../flags/flags_impl';
const EXPECTED_QUERY_OPTIONS = listChangesOptionsToHex(
@@ -103,6 +98,7 @@ suite('gr-rest-api-service-impl tests', () => {
});
test('parent diff comments are properly grouped', async () => {
+ element.addRepoNameToCache(42 as NumericChangeId, TEST_PROJECT_NAME);
sinon.stub(element._restApiHelper, 'fetchJSON').resolves({
'/COMMIT_MSG': [],
'sieve.go': [
@@ -246,7 +242,7 @@ suite('gr-rest-api-service-impl tests', () => {
});
test('differing patch diff comments are properly grouped', async () => {
- sinon.stub(element, 'getFromProjectLookup').resolves('test' as RepoName);
+ sinon.stub(element, 'getRepoName').resolves('test' as RepoName);
sinon.stub(element._restApiHelper, 'fetchJSON').callsFake(async request => {
const url = request.url;
if (url === '/changes/test~42/revisions/1/comments') {
@@ -313,20 +309,6 @@ suite('gr-rest-api-service-impl tests', () => {
} as RobotCommentInfo);
});
- test('server error', async () => {
- const getResponseObjectStub = sinon.stub(element, 'getResponseObject');
- sinon
- .stub(authService, 'fetch')
- .resolves(new Response(undefined, {status: 502}));
- const serverErrorEventPromise = new Promise(resolve => {
- addListenerForTest(document, 'server-error', resolve);
- });
- const response = await element._restApiHelper.fetchJSON({url: ''});
- assert.isUndefined(response);
- assert.isTrue(getResponseObjectStub.notCalled);
- await serverErrorEventPromise;
- });
-
test('legacy n,z key in change url is replaced', async () => {
const stub = sinon
.stub(element._restApiHelper, 'fetchJSON')
@@ -337,73 +319,85 @@ suite('gr-rest-api-service-impl tests', () => {
test('saveDiffPreferences invalidates cache line', () => {
const cacheKey = '/accounts/self/preferences.diff';
- const sendStub = sinon.stub(element._restApiHelper, 'send');
+ const fetchStub = sinon.stub(element._restApiHelper, 'fetch');
element._cache.set(cacheKey, {tab_size: 4} as unknown as ParsedJSON);
element.saveDiffPreferences({
tab_size: 8,
ignore_whitespace: 'IGNORE_NONE',
});
- assert.isTrue(sendStub.called);
+ assert.isTrue(fetchStub.called);
assert.isFalse(element._cache.has(cacheKey));
});
- suite('getAccountSuggestions', () => {
+ suite('queryAccounts', () => {
let fetchStub: sinon.SinonStub;
const testProject = 'testproject';
const testChangeNumber = 341682;
setup(() => {
fetchStub = sinon
.stub(element._restApiHelper, 'fetch')
- .resolves(new Response());
- element.setInProjectLookup(
+ .resolves(new Response(makePrefixedJSON(createAccountWithId())));
+ element.addRepoNameToCache(
testChangeNumber as NumericChangeId,
testProject as RepoName
);
});
test('url with just email', async () => {
- await element.getSuggestedAccounts('bro');
+ await element.queryAccounts('bro');
assert.isTrue(fetchStub.calledOnce);
- assert.equal(
- fetchStub.firstCall.args[0].url,
- `${getBaseUrl()}/accounts/?o=DETAILS&q=%22bro%22`
- );
+ assert.deepEqual(fetchStub.firstCall.args[0].params, {
+ o: 'DETAILS',
+ q: '"bro"',
+ });
});
test('url with email and canSee changeId', async () => {
- await element.getSuggestedAccounts(
+ await element.queryAccounts(
'bro',
undefined,
testChangeNumber as NumericChangeId
);
assert.isTrue(fetchStub.calledOnce);
- assert.equal(
- fetchStub.firstCall.args[0].url,
- `${getBaseUrl()}/accounts/?o=DETAILS&q=%22bro%22%20and%20cansee%3A${testProject}~${testChangeNumber}`
- );
+ assert.deepEqual(fetchStub.firstCall.args[0].params, {
+ o: 'DETAILS',
+ q: `"bro" and cansee:${testProject}~${testChangeNumber}`,
+ });
});
test('url with email and canSee changeId and isActive', async () => {
- await element.getSuggestedAccounts(
+ await element.queryAccounts(
'bro',
undefined,
testChangeNumber as NumericChangeId,
true
);
assert.isTrue(fetchStub.calledOnce);
- assert.equal(
- fetchStub.firstCall.args[0].url,
- `${getBaseUrl()}/accounts/?o=DETAILS&q=%22bro%22%20and%20cansee%3A${testProject}~${testChangeNumber}%20and%20is%3Aactive`
- );
+ assert.deepEqual(fetchStub.firstCall.args[0].params, {
+ o: 'DETAILS',
+ q: `"bro" and cansee:${testProject}~${testChangeNumber} and is:active`,
+ });
+ });
+ });
+
+ test('getAccountSuggestions using suggest query param', () => {
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetch')
+ .resolves(new Response());
+ element.getAccountSuggestions('user');
+ assert.isTrue(fetchStub.calledOnce);
+ assert.deepEqual(fetchStub.firstCall.args[0].params, {
+ suggest: undefined,
+ q: 'user',
});
});
test('getAccount when resp is undefined clears cache', async () => {
const cacheKey = '/accounts/self/detail';
const account = createAccountDetailWithId();
- element._cache.set(cacheKey, account);
+ element._cache.set(cacheKey, account as unknown as ParsedJSON);
const stub = sinon
- .stub(element._restApiHelper, 'fetchCacheURL')
+ .stub(element._restApiHelper, 'fetchCacheJSON')
.callsFake(async req => {
req.errFn!(undefined);
return undefined;
@@ -418,9 +412,9 @@ suite('gr-rest-api-service-impl tests', () => {
test('getAccount when status is 403 clears cache', async () => {
const cacheKey = '/accounts/self/detail';
const account = createAccountDetailWithId();
- element._cache.set(cacheKey, account);
+ element._cache.set(cacheKey, account as unknown as ParsedJSON);
const stub = sinon
- .stub(element._restApiHelper, 'fetchCacheURL')
+ .stub(element._restApiHelper, 'fetchCacheJSON')
.callsFake(async req => {
req.errFn!(new Response(undefined, {status: 403}));
return undefined;
@@ -436,16 +430,19 @@ suite('gr-rest-api-service-impl tests', () => {
const cacheKey = '/accounts/self/detail';
const account = createAccountDetailWithId();
const stub = sinon
- .stub(element._restApiHelper, 'fetchCacheURL')
+ .stub(element._restApiHelper, 'fetchCacheJSON')
.callsFake(async () => {
- element._cache.set(cacheKey, account);
+ element._cache.set(cacheKey, account as unknown as ParsedJSON);
return undefined;
});
assert.isFalse(element._cache.has(cacheKey));
await element.getAccount();
assert.isTrue(stub.called);
- assert.equal(element._cache.get(cacheKey), account);
+ assert.equal(
+ element._cache.get(cacheKey),
+ account as unknown as ParsedJSON
+ );
});
const preferenceSetup = function (testJSON: unknown, loggedIn: boolean) {
@@ -453,7 +450,7 @@ suite('gr-rest-api-service-impl tests', () => {
.stub(element, 'getLoggedIn')
.callsFake(() => Promise.resolve(loggedIn));
sinon
- .stub(element._restApiHelper, 'fetchCacheURL')
+ .stub(element._restApiHelper, 'fetchCacheJSON')
.callsFake(() => Promise.resolve(testJSON as ParsedJSON));
};
@@ -487,14 +484,14 @@ suite('gr-rest-api-service-impl tests', () => {
assert.equal(obj!.diff_view, 'SIDE_BY_SIDE');
});
- test('savPreferences normalizes download scheme', () => {
- const sendStub = sinon
- .stub(element._restApiHelper, 'send')
+ test('savePreferences normalizes download scheme', () => {
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetch')
.resolves(new Response());
element.savePreferences({download_scheme: 'HTTP'});
- assert.isTrue(sendStub.called);
+ assert.isTrue(fetchStub.called);
assert.equal(
- (sendStub.lastCall.args[0].body as Partial<PreferencesInfo>)
+ JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string)
.download_scheme,
'http'
);
@@ -518,14 +515,14 @@ suite('gr-rest-api-service-impl tests', () => {
});
test('saveDiffPreferences set show_tabs to false', () => {
- const sendStub = sinon.stub(element._restApiHelper, 'send');
+ const fetchStub = sinon.stub(element._restApiHelper, 'fetch');
element.saveDiffPreferences({
show_tabs: false,
ignore_whitespace: 'IGNORE_NONE',
});
- assert.isTrue(sendStub.called);
+ assert.isTrue(fetchStub.called);
assert.equal(
- (sendStub.lastCall.args[0].body as Partial<DiffPreferenceInput>)
+ JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string)
.show_tabs,
false
);
@@ -554,40 +551,53 @@ suite('gr-rest-api-service-impl tests', () => {
});
test('saveEditPreferences set show_tabs to false', () => {
- const sendStub = sinon.stub(element._restApiHelper, 'send');
+ const fetchStub = sinon.stub(element._restApiHelper, 'fetch');
element.saveEditPreferences({
...createDefaultEditPrefs(),
show_tabs: false,
});
- assert.isTrue(sendStub.called);
+ assert.isTrue(fetchStub.called);
assert.equal(
- (sendStub.lastCall.args[0].body as EditPreferencesInfo).show_tabs,
+ JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string)
+ .show_tabs,
false
);
});
test('confirmEmail', () => {
- const sendStub = sinon.spy(element._restApiHelper, 'send');
+ const fetchStub = sinon.stub(element._restApiHelper, 'fetch').resolves();
element.confirmEmail('foo');
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].method, HttpMethod.PUT);
- assert.equal(sendStub.lastCall.args[0].url, '/config/server/email.confirm');
- assert.deepEqual(sendStub.lastCall.args[0].body, {token: 'foo'});
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.lastCall.args[0].fetchOptions?.method,
+ HttpMethod.PUT
+ );
+ assert.equal(
+ fetchStub.lastCall.args[0].url,
+ '/config/server/email.confirm'
+ );
+ assert.deepEqual(
+ JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
+ {token: 'foo'}
+ );
});
test('setPreferredAccountEmail', async () => {
const email1 = 'email1@example.com';
const email2 = 'email2@example.com';
const encodedEmail = encodeURIComponent(email2);
- const sendStub = sinon.stub(element._restApiHelper, 'send').resolves();
+ const sendStub = sinon.stub(element._restApiHelper, 'fetch').resolves();
element._cache.set('/accounts/self/emails', [
{email: email1, preferred: true},
{email: email2, preferred: false},
- ]);
+ ] as unknown as ParsedJSON);
await element.setPreferredAccountEmail(email2);
assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].method, HttpMethod.PUT);
+ assert.equal(
+ sendStub.lastCall.args[0].fetchOptions?.method,
+ HttpMethod.PUT
+ );
assert.equal(
sendStub.lastCall.args[0].url,
`/accounts/self/emails/${encodedEmail}/preferred`
@@ -595,38 +605,223 @@ suite('gr-rest-api-service-impl tests', () => {
assert.deepEqual(element._cache.get('/accounts/self/emails'), [
{email: email1, preferred: false},
{email: email2, preferred: true},
- ]);
+ ] as unknown as ParsedJSON);
+ });
+
+ test('setAccountUsername', async () => {
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetch')
+ .resolves(new Response(makePrefixedJSON('john')));
+ element._cache.set(
+ '/accounts/self/detail',
+ createAccountDetailWithId() as unknown as ParsedJSON
+ );
+ await element.setAccountUsername('john');
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.lastCall.args[0].fetchOptions?.method,
+ HttpMethod.PUT
+ );
+ assert.equal(fetchStub.lastCall.args[0].url, '/accounts/self/username');
+ assert.deepEqual(
+ JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
+ {username: 'john'}
+ );
+ assert.deepEqual(
+ (element._cache.get(
+ '/accounts/self/detail'
+ ) as unknown as AccountDetailInfo)!.username,
+ 'john'
+ );
+ });
+
+ test('setAccountUsername empty unsets field', async () => {
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetch')
+ .resolves(new Response(undefined, {status: 204}));
+ element._cache.set('/accounts/self/detail', {
+ ...createAccountDetailWithId(),
+ username: 'john',
+ } as unknown as ParsedJSON);
+ await element.setAccountUsername('');
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.lastCall.args[0].fetchOptions?.method,
+ HttpMethod.PUT
+ );
+ assert.isUndefined(
+ (element._cache.get(
+ '/accounts/self/detail'
+ ) as unknown as AccountDetailInfo)!.username
+ );
+ });
+
+ test('setAccountDisplayName', async () => {
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetch')
+ .resolves(new Response(makePrefixedJSON('john')));
+ element._cache.set(
+ '/accounts/self/detail',
+ createAccountDetailWithId() as unknown as ParsedJSON
+ );
+ await element.setAccountDisplayName('john');
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.lastCall.args[0].fetchOptions?.method,
+ HttpMethod.PUT
+ );
+ assert.equal(fetchStub.lastCall.args[0].url, '/accounts/self/displayname');
+ assert.deepEqual(
+ JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
+ {display_name: 'john'}
+ );
+ assert.deepEqual(
+ (element._cache.get(
+ '/accounts/self/detail'
+ ) as unknown as AccountDetailInfo)!.display_name,
+ 'john'
+ );
+ });
+
+ test('setAccountDisplayName empty unsets field', async () => {
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetch')
+ .resolves(new Response(undefined, {status: 204}));
+ element._cache.set('/accounts/self/detail', {
+ ...createAccountDetailWithId(),
+ display_name: 'john',
+ } as unknown as ParsedJSON);
+ await element.setAccountDisplayName('');
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.lastCall.args[0].fetchOptions?.method,
+ HttpMethod.PUT
+ );
+ assert.isUndefined(
+ (element._cache.get(
+ '/accounts/self/detail'
+ ) as unknown as AccountDetailInfo)!.display_name
+ );
+ });
+
+ test('setAccountName', async () => {
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetch')
+ .resolves(new Response(makePrefixedJSON('john')));
+ element._cache.set(
+ '/accounts/self/detail',
+ createAccountDetailWithId() as unknown as ParsedJSON
+ );
+ await element.setAccountName('john');
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.lastCall.args[0].fetchOptions?.method,
+ HttpMethod.PUT
+ );
+ assert.equal(fetchStub.lastCall.args[0].url, '/accounts/self/name');
+ assert.deepEqual(
+ JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
+ {name: 'john'}
+ );
+ assert.deepEqual(
+ (element._cache.get(
+ '/accounts/self/detail'
+ ) as unknown as AccountDetailInfo)!.name,
+ 'john'
+ );
+ });
+
+ test('setAccountName empty unsets field', async () => {
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetch')
+ .resolves(new Response(undefined, {status: 204}));
+ element._cache.set('/accounts/self/detail', {
+ ...createAccountDetailWithId(),
+ name: 'john',
+ } as unknown as ParsedJSON);
+ await element.setAccountName('');
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.lastCall.args[0].fetchOptions?.method,
+ HttpMethod.PUT
+ );
+ assert.isUndefined(
+ (element._cache.get(
+ '/accounts/self/detail'
+ ) as unknown as AccountDetailInfo)!.name
+ );
});
test('setAccountStatus', async () => {
- const sendStub = sinon
- .stub(element._restApiHelper, 'send')
- .resolves('OOO' as unknown as ParsedJSON);
- element._cache.set('/accounts/self/detail', createAccountDetailWithId());
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetch')
+ .resolves(new Response(makePrefixedJSON('OOO')));
+ element._cache.set(
+ '/accounts/self/detail',
+ createAccountDetailWithId() as unknown as ParsedJSON
+ );
await element.setAccountStatus('OOO');
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].method, HttpMethod.PUT);
- assert.equal(sendStub.lastCall.args[0].url, '/accounts/self/status');
- assert.deepEqual(sendStub.lastCall.args[0].body, {status: 'OOO'});
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.lastCall.args[0].fetchOptions?.method,
+ HttpMethod.PUT
+ );
+ assert.equal(fetchStub.lastCall.args[0].url, '/accounts/self/status');
+ assert.deepEqual(
+ JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
+ {status: 'OOO'}
+ );
assert.deepEqual(
- element._cache.get('/accounts/self/detail')!.status,
+ (element._cache.get(
+ '/accounts/self/detail'
+ ) as unknown as AccountDetailInfo)!.status,
'OOO'
);
});
+ test('setAccountStatus empty unsets field', async () => {
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetch')
+ .resolves(new Response(undefined, {status: 204}));
+ element._cache.set('/accounts/self/detail', {
+ ...createAccountDetailWithId(),
+ status: 'OOO',
+ } as unknown as ParsedJSON);
+ await element.setAccountStatus('');
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.lastCall.args[0].fetchOptions?.method,
+ HttpMethod.PUT
+ );
+ assert.isUndefined(
+ (element._cache.get(
+ '/accounts/self/detail'
+ ) as unknown as AccountDetailInfo)!.status
+ );
+ });
+
suite('draft comments', () => {
test('_sendDiffDraftRequest pending requests tracked', async () => {
const obj = element._pendingRequests;
- sinon
- .stub(element, '_getChangeURLAndSend')
- .callsFake(() => mockPromise());
- assert.notOk(element.hasPendingDiffDrafts());
+ const promises: MockPromise<string>[] = [];
+ sinon.stub(element, '_changeBaseURL').callsFake(() => {
+ promises.push(mockPromise<string>());
+ return promises[promises.length - 1];
+ });
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetch')
+ .resolves(new Response(undefined, {status: 201}));
+ const draft: CommentInput = {
+ id: 'draft-id' as UrlEncodedCommentId,
+ message: 'draft message',
+ };
+ assert.isFalse(!!element.hasPendingDiffDrafts());
element._sendDiffDraftRequest(
HttpMethod.PUT,
123 as NumericChangeId,
1 as PatchSetNum,
- {}
+ draft
);
assert.equal(obj.sendDiffDraft.length, 1);
assert.isTrue(!!element.hasPendingDiffDrafts());
@@ -635,24 +830,30 @@ suite('gr-rest-api-service-impl tests', () => {
HttpMethod.PUT,
123 as NumericChangeId,
1 as PatchSetNum,
- {}
+ draft
);
assert.equal(obj.sendDiffDraft.length, 2);
assert.isTrue(!!element.hasPendingDiffDrafts());
- for (const promise of obj.sendDiffDraft) {
- (promise as MockPromise<void>).resolve();
+ for (const promise of promises) {
+ promise.resolve('');
}
await element.awaitPendingDiffDrafts();
assert.equal(obj.sendDiffDraft.length, 0);
assert.isFalse(!!element.hasPendingDiffDrafts());
+
+ assert.isTrue(fetchStub.called);
+ assert.deepEqual(
+ JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
+ draft
+ );
});
suite('_failForCreate200', () => {
test('_sendDiffDraftRequest checks for 200 on create', async () => {
- const sendPromise = Promise.resolve({} as unknown as ParsedJSON);
- sinon.stub(element, '_getChangeURLAndSend').returns(sendPromise);
+ element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
+ sinon.stub(element._restApiHelper, 'fetch').resolves(new Response());
const failStub = sinon.stub(element, '_failForCreate200').resolves();
await element._sendDiffDraftRequest(
HttpMethod.PUT,
@@ -661,11 +862,11 @@ suite('gr-rest-api-service-impl tests', () => {
{}
);
assert.isTrue(failStub.calledOnce);
- assert.isTrue(failStub.calledWithExactly(sendPromise));
});
test('_sendDiffDraftRequest no checks for 200 on non create', async () => {
- sinon.stub(element, '_getChangeURLAndSend').resolves();
+ element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
+ sinon.stub(element._restApiHelper, 'fetch').resolves(new Response());
const failStub = sinon.stub(element, '_failForCreate200').resolves();
await element._sendDiffDraftRequest(
HttpMethod.PUT,
@@ -707,100 +908,137 @@ suite('gr-rest-api-service-impl tests', () => {
const change_num = 1 as NumericChangeId;
const file_name = 'index.php';
const file_contents = '<?php';
- const sendStub = sinon
- .stub(element._restApiHelper, 'send')
- .resolves([
- change_num,
- file_name,
- file_contents,
- ] as unknown as ParsedJSON);
- sinon
- .stub(element, 'getResponseObject')
- .resolves([
- change_num,
- file_name,
- file_contents,
- ] as unknown as ParsedJSON);
+ const fetchStub = sinon.stub(element._restApiHelper, 'fetch').resolves();
element._cache.set(
`/changes/${change_num}/edit/${file_name}`,
{} as unknown as ParsedJSON
);
await element.saveChangeEdit(change_num, file_name, file_contents);
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].method, HttpMethod.PUT);
+ assert.isTrue(fetchStub.calledOnce);
assert.equal(
- sendStub.lastCall.args[0].url,
+ fetchStub.lastCall.args[0].fetchOptions?.method,
+ HttpMethod.PUT
+ );
+ assert.equal(
+ fetchStub.lastCall.args[0].url,
'/changes/test~1/edit/' + file_name
);
- assert.equal(sendStub.lastCall.args[0].body, file_contents);
+ assert.equal(fetchStub.lastCall.args[0].fetchOptions?.body, file_contents);
});
test('putChangeCommitMessage', async () => {
element._projectLookup = {1: Promise.resolve('test' as RepoName)};
const change_num = 1 as NumericChangeId;
const message = 'this is a commit message';
- const sendStub = sinon
- .stub(element._restApiHelper, 'send')
- .resolves([change_num, message] as unknown as ParsedJSON);
- sinon
- .stub(element, 'getResponseObject')
- .resolves([change_num, message] as unknown as ParsedJSON);
+ const committer_email = 'test@example.com';
+ const fetchStub = sinon.stub(element._restApiHelper, 'fetch').resolves();
element._cache.set(
`/changes/${change_num}/message`,
{} as unknown as ParsedJSON
);
- await element.putChangeCommitMessage(change_num, message);
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].method, HttpMethod.PUT);
- assert.equal(sendStub.lastCall.args[0].url, '/changes/test~1/message');
- assert.deepEqual(sendStub.lastCall.args[0].body, {
- message,
- });
+ await element.putChangeCommitMessage(change_num, message, committer_email);
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.lastCall.args[0].fetchOptions?.method,
+ HttpMethod.PUT
+ );
+ assert.equal(fetchStub.lastCall.args[0].url, '/changes/test~1/message');
+ assert.deepEqual(
+ JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
+ {
+ message,
+ committer_email,
+ }
+ );
+ });
+
+ test('updateIdentityInChangeEdit', async () => {
+ element._projectLookup = {1: Promise.resolve('test' as RepoName)};
+ const change_num = 1 as NumericChangeId;
+ const name = 'user';
+ const email = 'user@example.com';
+ const type = 'AUTHOR';
+ const fetchStub = sinon.stub(element._restApiHelper, 'fetch').resolves();
+ await element.updateIdentityInChangeEdit(change_num, name, email, type);
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.lastCall.args[0].fetchOptions?.method,
+ HttpMethod.PUT
+ );
+ assert.equal(
+ fetchStub.lastCall.args[0].url,
+ '/changes/test~1/edit:identity'
+ );
+ assert.deepEqual(
+ JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
+ {
+ email: 'user@example.com',
+ name: 'user',
+ type: 'AUTHOR',
+ }
+ );
});
test('deleteChangeCommitMessage', async () => {
element._projectLookup = {1: Promise.resolve('test' as RepoName)};
const change_num = 1 as NumericChangeId;
const messageId = 'abc' as ChangeMessageId;
- const sendStub = sinon
- .stub(element._restApiHelper, 'send')
- .resolves([change_num, messageId] as unknown as ParsedJSON);
- sinon
- .stub(element, 'getResponseObject')
- .resolves([change_num, messageId] as unknown as ParsedJSON);
+ const fetchStub = sinon.stub(element._restApiHelper, 'fetch').resolves();
await element.deleteChangeCommitMessage(change_num, messageId);
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].method, HttpMethod.DELETE);
- assert.equal(sendStub.lastCall.args[0].url, '/changes/test~1/messages/abc');
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.lastCall.args[0].fetchOptions?.method,
+ HttpMethod.DELETE
+ );
+ assert.equal(
+ fetchStub.lastCall.args[0].url,
+ '/changes/test~1/messages/abc'
+ );
});
- test('startWorkInProgress', () => {
- const sendStub = sinon
- .stub(element, '_getChangeURLAndSend')
- .resolves('ok' as unknown as ParsedJSON);
- element.startWorkInProgress(42 as NumericChangeId);
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].changeNum, 42 as NumericChangeId);
- assert.equal(sendStub.lastCall.args[0].method, HttpMethod.POST);
- assert.isNotOk(sendStub.lastCall.args[0].patchNum);
- assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
- assert.deepEqual(sendStub.lastCall.args[0].body, {});
-
- element.startWorkInProgress(42 as NumericChangeId, 'revising...');
- assert.isTrue(sendStub.calledTwice);
- assert.equal(sendStub.lastCall.args[0].changeNum, 42 as NumericChangeId);
- assert.equal(sendStub.lastCall.args[0].method, HttpMethod.POST);
- assert.isNotOk(sendStub.lastCall.args[0].patchNum);
- assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
- assert.deepEqual(sendStub.lastCall.args[0].body, {
- message: 'revising...',
- });
+ test('startWorkInProgress', async () => {
+ element.addRepoNameToCache(42 as NumericChangeId, TEST_PROJECT_NAME);
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetch')
+ .resolves(new Response());
+ const urlSpy = sinon.spy(element, '_changeBaseURL');
+ await element.startWorkInProgress(42 as NumericChangeId);
+ assert.isTrue(fetchStub.calledOnce);
+ assert.isTrue(urlSpy.calledOnce);
+ assert.equal(urlSpy.lastCall.args[0], 42 as NumericChangeId);
+ assert.isNotOk(urlSpy.lastCall.args[1]);
+ assert.equal(
+ fetchStub.lastCall.args[0].fetchOptions?.method,
+ HttpMethod.POST
+ );
+ assert.isTrue(fetchStub.lastCall.args[0].url.endsWith('/wip'));
+ assert.deepEqual(
+ JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
+ {}
+ );
+
+ await element.startWorkInProgress(42 as NumericChangeId, 'revising...');
+ assert.isTrue(fetchStub.calledTwice);
+ assert.equal(urlSpy.lastCall.args[0], 42 as NumericChangeId);
+ assert.isNotOk(urlSpy.lastCall.args[1]);
+ assert.equal(
+ fetchStub.lastCall.args[0].fetchOptions?.method,
+ HttpMethod.POST
+ );
+ assert.isTrue(fetchStub.lastCall.args[0].url.endsWith('/wip'));
+ assert.deepEqual(
+ JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
+ {
+ message: 'revising...',
+ }
+ );
});
test('deleteComment', async () => {
+ element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
const comment = createComment();
- const sendStub = sinon
- .stub(element, '_getChangeURLAndSend')
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetchJSON')
.resolves(comment as unknown as ParsedJSON);
const response = await element.deleteComment(
123 as NumericChangeId,
@@ -809,32 +1047,40 @@ suite('gr-rest-api-service-impl tests', () => {
'removal reason'
);
assert.equal(response, comment);
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].changeNum, 123 as NumericChangeId);
- assert.equal(sendStub.lastCall.args[0].method, HttpMethod.POST);
- assert.equal(sendStub.lastCall.args[0].patchNum, 1 as PatchSetNum);
- assert.equal(sendStub.lastCall.args[0].endpoint, '/comments/01234/delete');
- assert.deepEqual(sendStub.lastCall.args[0].body, {
- reason: 'removal reason',
- });
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.lastCall.args[0].fetchOptions?.method,
+ HttpMethod.POST
+ );
+ assert.equal(
+ fetchStub.lastCall.args[0].url,
+ '/changes/test-project~123/revisions/1/comments/01234/delete'
+ );
+ assert.deepEqual(
+ JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
+ {
+ reason: 'removal reason',
+ }
+ );
});
test('createRepo encodes name', async () => {
- const sendStub = sinon.stub(element._restApiHelper, 'send').resolves();
+ const fetchStub = sinon.stub(element._restApiHelper, 'fetch').resolves();
await element.createRepo({name: 'x/y' as RepoName});
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].url, '/projects/x%2Fy');
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].url, '/projects/x%2Fy');
});
test('queryChangeFiles', async () => {
- const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ element.addRepoNameToCache(42 as NumericChangeId, TEST_PROJECT_NAME);
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetchJSON')
+ .resolves();
await element.queryChangeFiles(42 as NumericChangeId, EDIT, 'test/path.js');
- assert.equal(fetchStub.lastCall.args[0].changeNum, 42 as NumericChangeId);
assert.equal(
- fetchStub.lastCall.args[0].endpoint,
- '/files?q=test%2Fpath.js'
+ fetchStub.lastCall.args[0].url,
+ '/changes/test-project~42/revisions/edit/files?q=test%2Fpath.js'
);
- assert.equal(fetchStub.lastCall.args[0].revision, EDIT);
});
test('normal use', () => {
@@ -887,29 +1133,29 @@ suite('gr-rest-api-service-impl tests', () => {
suite('getRepos', () => {
const defaultQuery = '';
- let fetchCacheURLStub: sinon.SinonStub;
+ let fetchCacheJSONStub: sinon.SinonStub;
setup(() => {
- fetchCacheURLStub = sinon
- .stub(element._restApiHelper, 'fetchCacheURL')
+ fetchCacheJSONStub = sinon
+ .stub(element._restApiHelper, 'fetchCacheJSON')
.resolves([] as unknown as ParsedJSON);
});
test('normal use', () => {
element.getRepos('test', 25);
assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
+ fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=0&d=&m=test'
);
element.getRepos(undefined, 25);
assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
+ fetchCacheJSONStub.lastCall.args[0].url,
`/projects/?n=26&S=0&d=&m=${defaultQuery}`
);
element.getRepos('test', 25, 25);
assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
+ fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=25&d=&m=test'
);
});
@@ -917,7 +1163,7 @@ suite('gr-rest-api-service-impl tests', () => {
test('with blank', () => {
element.getRepos('test/test', 25);
assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
+ fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=0&d=&m=test%2Ftest'
);
});
@@ -925,7 +1171,7 @@ suite('gr-rest-api-service-impl tests', () => {
test('with hyphen', () => {
element.getRepos('foo-bar', 25);
assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
+ fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=0&d=&m=foo-bar'
);
});
@@ -933,7 +1179,7 @@ suite('gr-rest-api-service-impl tests', () => {
test('with leading hyphen', () => {
element.getRepos('-bar', 25);
assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
+ fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=0&d=&m=-bar'
);
});
@@ -941,7 +1187,7 @@ suite('gr-rest-api-service-impl tests', () => {
test('with trailing hyphen', () => {
element.getRepos('foo-bar-', 25);
assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
+ fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=0&d=&m=foo-bar-'
);
});
@@ -949,7 +1195,7 @@ suite('gr-rest-api-service-impl tests', () => {
test('with underscore', () => {
element.getRepos('foo_bar', 25);
assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
+ fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=0&d=&m=foo_bar'
);
});
@@ -957,7 +1203,7 @@ suite('gr-rest-api-service-impl tests', () => {
test('with underscore', () => {
element.getRepos('foo_bar', 25);
assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
+ fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=0&d=&m=foo_bar'
);
});
@@ -965,7 +1211,7 @@ suite('gr-rest-api-service-impl tests', () => {
test('hyphen only', () => {
element.getRepos('-', 25);
assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
+ fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=0&d=&m=-'
);
});
@@ -973,7 +1219,7 @@ suite('gr-rest-api-service-impl tests', () => {
test('using query', () => {
element.getRepos('description:project', 25);
assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
+ fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=0&query=description%3Aproject'
);
});
@@ -1003,24 +1249,27 @@ suite('gr-rest-api-service-impl tests', () => {
});
suite('getGroups', () => {
- let fetchCacheURLStub: sinon.SinonStub;
+ let fetchCacheJSONStub: sinon.SinonStub;
setup(() => {
- fetchCacheURLStub = sinon.stub(element._restApiHelper, 'fetchCacheURL');
+ fetchCacheJSONStub = sinon.stub(element._restApiHelper, 'fetchCacheJSON');
});
test('normal use', () => {
element.getGroups('test', 25);
assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
+ fetchCacheJSONStub.lastCall.args[0].url,
'/groups/?n=26&S=0&m=test'
);
element.getGroups('', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url, '/groups/?n=26&S=0');
+ assert.equal(
+ fetchCacheJSONStub.lastCall.args[0].url,
+ '/groups/?n=26&S=0'
+ );
element.getGroups('test', 25, 25);
assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
+ fetchCacheJSONStub.lastCall.args[0].url,
'/groups/?n=26&S=25&m=test'
);
});
@@ -1028,13 +1277,13 @@ suite('gr-rest-api-service-impl tests', () => {
test('regex', () => {
element.getGroups('^test.*', 25);
assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
+ fetchCacheJSONStub.lastCall.args[0].url,
'/groups/?n=26&S=0&r=%5Etest.*'
);
element.getGroups('^test.*', 25, 25);
assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
+ fetchCacheJSONStub.lastCall.args[0].url,
'/groups/?n=26&S=25&r=%5Etest.*'
);
});
@@ -1046,18 +1295,18 @@ suite('gr-rest-api-service-impl tests', () => {
assert(fetchStub.called);
});
- test('getSuggestedAccounts does not return fetchJSON', async () => {
+ test('queryAccounts does not return fetchJSON', async () => {
const fetchJSONSpy = sinon.spy(element._restApiHelper, 'fetchJSON');
- const accts = await element.getSuggestedAccounts('');
+ const accts = await element.queryAccounts('');
assert.isFalse(fetchJSONSpy.called);
assert.equal(accts!.length, 0);
});
- test('fetchJSON gets called by getSuggestedAccounts', async () => {
+ test('fetchJSON gets called by queryAccounts', async () => {
const fetchJSONStub = sinon
.stub(element._restApiHelper, 'fetchJSON')
.resolves();
- await element.getSuggestedAccounts('own');
+ await element.queryAccounts('own');
assert.deepEqual(fetchJSONStub.lastCall.args[0].params, {
q: '"own"',
o: 'DETAILS',
@@ -1065,6 +1314,14 @@ suite('gr-rest-api-service-impl tests', () => {
});
suite('getChangeDetail', () => {
+ let getConfigStub: sinon.SinonStub;
+
+ setup(() => {
+ getConfigStub = sinon
+ .stub(element, 'getConfig')
+ .resolves(createServerInfo());
+ });
+
suite('change detail options', () => {
let changeDetailStub: sinon.SinonStub;
setup(() => {
@@ -1074,7 +1331,7 @@ suite('gr-rest-api-service-impl tests', () => {
});
test('signed pushes disabled', async () => {
- sinon.stub(element, 'getConfig').resolves({
+ getConfigStub.resolves({
...createServerInfo(),
receive: {enable_signed_push: undefined},
});
@@ -1087,7 +1344,7 @@ suite('gr-rest-api-service-impl tests', () => {
});
test('signed pushes enabled', async () => {
- sinon.stub(element, 'getConfig').resolves({
+ getConfigStub.resolves({
...createServerInfo(),
receive: {enable_signed_push: 'true'},
});
@@ -1101,6 +1358,7 @@ suite('gr-rest-api-service-impl tests', () => {
});
test('GrReviewerUpdatesParser.parse is used', async () => {
+ element.addRepoNameToCache(42 as NumericChangeId, TEST_PROJECT_NAME);
const changeInfo = createParsedChange();
const parseStub = sinon
.stub(GrReviewerUpdatesParser, 'parse')
@@ -1116,6 +1374,14 @@ suite('gr-rest-api-service-impl tests', () => {
const expectedUrl = `${window.CANONICAL_PATH}/changes/test~4321/detail?O=516714`;
const optionsStub = sinon.stub(element._etags, 'getOptions');
const collectStub = sinon.stub(element._etags, 'collect');
+ sinon.stub(element._restApiHelper, 'fetch').resolves(
+ new Response(
+ makePrefixedJSON({
+ ...createChange(),
+ _number: 123 as NumericChangeId,
+ })
+ )
+ );
await element._getChangeDetail(changeNum, '516714');
assert.isTrue(optionsStub.calledWithExactly(expectedUrl));
assert.equal(collectStub.lastCall.args[0], expectedUrl);
@@ -1125,7 +1391,7 @@ suite('gr-rest-api-service-impl tests', () => {
const errFn = sinon.stub();
sinon.stub(element, 'getChangeActionURL').resolves('');
sinon
- .stub(element._restApiHelper, 'fetchRawJSON')
+ .stub(element._restApiHelper, 'fetch')
.resolves(new Response(undefined, {status: 500}));
await element._getChangeDetail(123 as NumericChangeId, '516714', errFn);
assert.isTrue(errFn.called);
@@ -1133,7 +1399,7 @@ suite('gr-rest-api-service-impl tests', () => {
test('_getChangeDetail populates _projectLookup', async () => {
sinon.stub(element, 'getChangeActionURL').resolves('');
- sinon.stub(element._restApiHelper, 'fetchRawJSON').resolves(
+ sinon.stub(element._restApiHelper, 'fetch').resolves(
new Response(')]}\'{"_number":1,"project":"test"}', {
status: 200,
})
@@ -1152,7 +1418,7 @@ suite('gr-rest-api-service-impl tests', () => {
setup(() => {
requestUrl = '/foo/bar';
const mockResponse = {foo: 'bar', baz: 42};
- mockResponseSerial = JSON_PREFIX + JSON.stringify(mockResponse);
+ mockResponseSerial = makePrefixedJSON(mockResponse);
sinon.stub(element._restApiHelper, 'urlWithParams').returns(requestUrl);
sinon.stub(element, 'getChangeActionURL').resolves(requestUrl);
collectSpy = sinon.spy(element._etags, 'collect');
@@ -1160,7 +1426,7 @@ suite('gr-rest-api-service-impl tests', () => {
test('contributes to cache', async () => {
const getPayloadSpy = sinon.spy(element._etags, 'getCachedPayload');
- sinon.stub(element._restApiHelper, 'fetchRawJSON').resolves(
+ sinon.stub(element._restApiHelper, 'fetch').resolves(
new Response(mockResponseSerial, {
status: 200,
})
@@ -1176,7 +1442,7 @@ suite('gr-rest-api-service-impl tests', () => {
test('uses cache on HTTP 304', async () => {
const getPayloadStub = sinon.stub(element._etags, 'getCachedPayload');
getPayloadStub.returns(mockResponseSerial);
- sinon.stub(element._restApiHelper, 'fetchRawJSON').resolves(
+ sinon.stub(element._restApiHelper, 'fetch').resolves(
new Response(undefined, {
status: 304,
})
@@ -1189,13 +1455,13 @@ suite('gr-rest-api-service-impl tests', () => {
});
});
- test('setInProjectLookup', async () => {
- element.setInProjectLookup(555 as NumericChangeId, 'project' as RepoName);
- const project = await element.getFromProjectLookup(555 as NumericChangeId);
+ test('addRepoNameToCache', async () => {
+ element.addRepoNameToCache(555 as NumericChangeId, 'project' as RepoName);
+ const project = await element.getRepoName(555 as NumericChangeId);
assert.deepEqual(project, 'project' as RepoName);
});
- suite('getFromProjectLookup', () => {
+ suite('getRepoName', () => {
const changeNum = 555 as NumericChangeId;
const repo = 'test-repo' as RepoName;
@@ -1203,29 +1469,33 @@ suite('gr-rest-api-service-impl tests', () => {
const promise = mockPromise<undefined>();
sinon.stub(element, 'getChange').returns(promise);
- const projectLookup = element.getFromProjectLookup(changeNum);
+ const projectLookup = element.getRepoName(changeNum);
promise.resolve(undefined);
- assert.isUndefined(await projectLookup);
+ const err: Error = await assertFails(projectLookup);
+ assert.equal(
+ err.message,
+ 'Failed to lookup the repo for change number 555'
+ );
});
test('getChange succeeds with project', async () => {
const promise = mockPromise<undefined | ChangeInfo>();
sinon.stub(element, 'getChange').returns(promise);
- const projectLookup = element.getFromProjectLookup(changeNum);
+ const projectLookup = element.getRepoName(changeNum);
promise.resolve({...createChange(), project: repo});
assert.equal(await projectLookup, repo);
assert.deepEqual(element._projectLookup, {'555': projectLookup});
});
- test('getChange fails, but a setInProjectLookup() call is used as fallback', async () => {
+ test('getChange fails, but a addRepoNameToCache() call is used as fallback', async () => {
const promise = mockPromise<undefined>();
sinon.stub(element, 'getChange').returns(promise);
- const projectLookup = element.getFromProjectLookup(changeNum);
- element.setInProjectLookup(changeNum, repo);
+ const projectLookup = element.getRepoName(changeNum);
+ element.addRepoNameToCache(changeNum, repo);
promise.resolve(undefined);
assert.equal(await projectLookup, repo);
@@ -1245,11 +1515,11 @@ suite('gr-rest-api-service-impl tests', () => {
// Array<Array<Object>>.
await element.getChangesForMultipleQueries(undefined, []);
assert.equal(Object.keys(element._projectLookup).length, 3);
- const project1 = await element.getFromProjectLookup(1 as NumericChangeId);
+ const project1 = await element.getRepoName(1 as NumericChangeId);
assert.equal(project1, 'test' as RepoName);
- const project2 = await element.getFromProjectLookup(2 as NumericChangeId);
+ const project2 = await element.getRepoName(2 as NumericChangeId);
assert.equal(project2, 'test' as RepoName);
- const project3 = await element.getFromProjectLookup(3 as NumericChangeId);
+ const project3 = await element.getRepoName(3 as NumericChangeId);
assert.equal(project3, 'test/test' as RepoName);
});
@@ -1263,11 +1533,11 @@ suite('gr-rest-api-service-impl tests', () => {
// When query !instanceof Array, fetchJSON returns Array<Object>.
await element.getChanges();
assert.equal(Object.keys(element._projectLookup).length, 3);
- const project1 = await element.getFromProjectLookup(1 as NumericChangeId);
+ const project1 = await element.getRepoName(1 as NumericChangeId);
assert.equal(project1, 'test' as RepoName);
- const project2 = await element.getFromProjectLookup(2 as NumericChangeId);
+ const project2 = await element.getRepoName(2 as NumericChangeId);
assert.equal(project2, 'test' as RepoName);
- const project3 = await element.getFromProjectLookup(3 as NumericChangeId);
+ const project3 = await element.getRepoName(3 as NumericChangeId);
assert.equal(project3, 'test/test' as RepoName);
});
});
@@ -1290,122 +1560,95 @@ suite('gr-rest-api-service-impl tests', () => {
assert.isTrue(getChangesStub.calledOnce);
});
- test('_getChangeURLAndFetch', async () => {
- element._projectLookup = {1: Promise.resolve('test' as RepoName)};
- const fetchStub = sinon
- .stub(element._restApiHelper, 'fetchJSON')
- .resolves();
- const req = {
- changeNum: 1 as NumericChangeId,
- endpoint: '/test',
- revision: 1 as RevisionId,
- };
- await element._getChangeURLAndFetch(req);
- assert.equal(
- fetchStub.lastCall.args[0].url,
- '/changes/test~1/revisions/1/test'
- );
- });
-
- test('_getChangeURLAndSend', async () => {
- element._projectLookup = {1: Promise.resolve('test' as RepoName)};
- const sendStub = sinon.stub(element._restApiHelper, 'send').resolves();
-
- const req = {
- changeNum: 1 as NumericChangeId,
- method: HttpMethod.POST,
- patchNum: 1 as PatchSetNum,
- endpoint: '/test',
- };
- await element._getChangeURLAndSend(req);
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].method, HttpMethod.POST);
- assert.equal(
- sendStub.lastCall.args[0].url,
- '/changes/test~1/revisions/1/test'
- );
- });
-
- suite('reading responses', () => {
- test('_readResponsePayload', async () => {
- const mockObject = {foo: 'bar', baz: 'foo'} as unknown as ParsedJSON;
- const serial = JSON_PREFIX + JSON.stringify(mockObject);
- const response = new Response(serial);
- const payload = await readResponsePayload(response);
- assert.deepEqual(payload.parsed, mockObject);
- assert.equal(payload.raw, serial);
- });
-
- test('_parsePrefixedJSON', () => {
- const obj = {x: 3, y: {z: 4}, w: 23} as unknown as ParsedJSON;
- const serial = JSON_PREFIX + JSON.stringify(obj);
- const result = parsePrefixedJSON(serial);
- assert.deepEqual(result, obj);
- });
- });
-
test('setChangeTopic', async () => {
- const sendSpy = sinon.spy(element, '_getChangeURLAndSend');
+ element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetch')
+ .resolves(new Response(makePrefixedJSON('foo-bar')));
await element.setChangeTopic(123 as NumericChangeId, 'foo-bar');
- assert.isTrue(sendSpy.calledOnce);
- assert.deepEqual(sendSpy.lastCall.args[0].body, {topic: 'foo-bar'});
+ assert.isTrue(fetchStub.calledOnce);
+ assert.deepEqual(
+ JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
+ {topic: 'foo-bar'}
+ );
});
test('setChangeHashtag', async () => {
- const sendSpy = sinon.spy(element, '_getChangeURLAndSend');
+ element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
+ const fetchStub = sinon.stub(element._restApiHelper, 'fetchJSON');
await element.setChangeHashtag(123 as NumericChangeId, {
add: ['foo-bar' as Hashtag],
});
- assert.isTrue(sendSpy.calledOnce);
+ assert.isTrue(fetchStub.calledOnce);
assert.sameDeepMembers(
- (sendSpy.lastCall.args[0].body! as HashtagsInput).add!,
+ JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string).add!,
['foo-bar']
);
});
test('generateAccountHttpPassword', async () => {
- const sendSpy = sinon.spy(element._restApiHelper, 'send');
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetchJSON')
+ .resolves();
await element.generateAccountHttpPassword();
- assert.isTrue(sendSpy.calledOnce);
- assert.deepEqual(sendSpy.lastCall.args[0].body, {generate: true});
+ assert.isTrue(fetchStub.calledOnce);
+ assert.deepEqual(
+ JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
+ {generate: true}
+ );
});
suite('getChangeFiles', () => {
test('patch only', async () => {
- const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetchJSON')
+ .resolves();
const range = {basePatchNum: PARENT, patchNum: 2 as RevisionPatchSetNum};
await element.getChangeFiles(123 as NumericChangeId, range);
assert.isTrue(fetchStub.calledOnce);
assert.equal(
- fetchStub.lastCall.args[0].revision,
- 2 as RevisionPatchSetNum
+ fetchStub.lastCall.args[0].url,
+ '/changes/test-project~123/revisions/2/files'
);
assert.isNotOk(fetchStub.lastCall.args[0].params);
});
test('simple range', async () => {
- const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetchJSON')
+ .resolves();
const range = {
basePatchNum: 4 as BasePatchSetNum,
patchNum: 5 as RevisionPatchSetNum,
};
await element.getChangeFiles(123 as NumericChangeId, range);
assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].revision, 5 as RevisionId);
+ assert.equal(
+ fetchStub.lastCall.args[0].url,
+ '/changes/test-project~123/revisions/5/files'
+ );
assert.isOk(fetchStub.lastCall.args[0].params);
assert.equal(fetchStub.lastCall.args[0].params!.base, 4);
assert.isNotOk(fetchStub.lastCall.args[0].params!.parent);
});
test('parent index', async () => {
- const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetchJSON')
+ .resolves();
const range = {
basePatchNum: -3 as BasePatchSetNum,
patchNum: 5 as RevisionPatchSetNum,
};
await element.getChangeFiles(123 as NumericChangeId, range);
assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].revision, 5 as RevisionId);
+ assert.equal(
+ fetchStub.lastCall.args[0].url,
+ '/changes/test-project~123/revisions/5/files'
+ );
assert.isOk(fetchStub.lastCall.args[0].params);
assert.isNotOk(fetchStub.lastCall.args[0].params!.base);
assert.equal(fetchStub.lastCall.args[0].params!.parent, 3);
@@ -1414,7 +1657,10 @@ suite('gr-rest-api-service-impl tests', () => {
suite('getDiff', () => {
test('patchOnly', async () => {
- const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetchJSON')
+ .resolves();
await element.getDiff(
123 as NumericChangeId,
PARENT,
@@ -1422,14 +1668,20 @@ suite('gr-rest-api-service-impl tests', () => {
'foo/bar.baz'
);
assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].revision, 2 as RevisionId);
+ assert.equal(
+ fetchStub.lastCall.args[0].url,
+ '/changes/test-project~123/revisions/2/files/foo%2Fbar.baz/diff'
+ );
assert.isOk(fetchStub.lastCall.args[0].params);
assert.isNotOk(fetchStub.lastCall.args[0].params!.parent);
assert.isNotOk(fetchStub.lastCall.args[0].params!.base);
});
test('simple range', async () => {
- const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetchJSON')
+ .resolves();
await element.getDiff(
123 as NumericChangeId,
4 as PatchSetNum,
@@ -1437,14 +1689,20 @@ suite('gr-rest-api-service-impl tests', () => {
'foo/bar.baz'
);
assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].revision, 5 as RevisionId);
+ assert.equal(
+ fetchStub.lastCall.args[0].url,
+ '/changes/test-project~123/revisions/5/files/foo%2Fbar.baz/diff'
+ );
assert.isOk(fetchStub.lastCall.args[0].params);
assert.isNotOk(fetchStub.lastCall.args[0].params!.parent);
assert.equal(fetchStub.lastCall.args[0].params!.base, 4);
});
test('parent index', async () => {
- const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetchJSON')
+ .resolves();
await element.getDiff(
123 as NumericChangeId,
-3 as PatchSetNum,
@@ -1452,7 +1710,10 @@ suite('gr-rest-api-service-impl tests', () => {
'foo/bar.baz'
);
assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].revision, 5 as RevisionId);
+ assert.equal(
+ fetchStub.lastCall.args[0].url,
+ '/changes/test-project~123/revisions/5/files/foo%2Fbar.baz/diff'
+ );
assert.isOk(fetchStub.lastCall.args[0].params);
assert.isNotOk(fetchStub.lastCall.args[0].params!.base);
assert.equal(fetchStub.lastCall.args[0].params!.parent, 3);
@@ -1460,35 +1721,34 @@ suite('gr-rest-api-service-impl tests', () => {
});
test('getDashboard', () => {
- const fetchCacheURLStub = sinon.stub(
+ const fetchCacheJSONStub = sinon.stub(
element._restApiHelper,
- 'fetchCacheURL'
+ 'fetchCacheJSON'
);
element.getDashboard(
'gerrit/project' as RepoName,
'default:main' as DashboardId
);
- assert.isTrue(fetchCacheURLStub.calledOnce);
+ assert.isTrue(fetchCacheJSONStub.calledOnce);
assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
+ fetchCacheJSONStub.lastCall.args[0].url,
'/projects/gerrit%2Fproject/dashboards/default%3Amain'
);
});
test('getFileContent', async () => {
- sinon.stub(element, '_getChangeURLAndSend').resolves(
- new Response(undefined, {
- status: 200,
- headers: {
- 'X-FYI-Content-Type': 'text/java',
- },
- }) as unknown as ParsedJSON
+ element.addRepoNameToCache(1 as NumericChangeId, TEST_PROJECT_NAME);
+ sinon.stub(element._restApiHelper, 'fetch').callsFake(() =>
+ Promise.resolve(
+ new Response(makePrefixedJSON('new content'), {
+ status: 200,
+ headers: {
+ 'X-FYI-Content-Type': 'text/java',
+ },
+ })
+ )
);
- sinon
- .stub(element, 'getResponseObject')
- .resolves('new content' as unknown as ParsedJSON);
-
const edit = await element.getFileContent(
1 as NumericChangeId,
'tst/path',
@@ -1560,35 +1820,34 @@ suite('gr-rest-api-service-impl tests', () => {
assert.isTrue(getChangeFilesStub.calledOnce);
});
- test('_fetch forwards request and logs', async () => {
- const logStub = sinon.stub(element._restApiHelper, '_logCall');
- const response = new Response(undefined, {status: 404});
- const url = 'my url';
- const fetchOptions = {method: 'DELETE'};
- sinon.stub(authService, 'fetch').resolves(response);
- const startTime = 123;
- sinon.stub(Date, 'now').returns(startTime);
- const req = {url, fetchOptions};
- await element._restApiHelper.fetch(req);
- assert.isTrue(logStub.calledOnce);
- assert.isTrue(logStub.calledWith(req, startTime, response.status));
+ test('getChangeEdit not logged in returns undefined', async () => {
+ element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
+ sinon.stub(element, 'getLoggedIn').resolves(false);
+ const fetchSpy = sinon.spy(element._restApiHelper, 'fetch');
+ const edit = await element.getChangeEdit(123 as NumericChangeId);
+ assert.isUndefined(edit);
+ assert.isFalse(fetchSpy.called);
});
- test('_logCall only reports requests with anonymized URLss', async () => {
- sinon.stub(Date, 'now').returns(200);
- const handler = sinon.stub();
- addListenerForTest(document, 'gr-rpc-log', handler);
-
- element._restApiHelper._logCall({url: 'url'}, 100, 200);
- assert.isFalse(handler.called);
+ test('getChangeEdit no edit patchset returns undefined', async () => {
+ element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
+ sinon.stub(element, 'getLoggedIn').resolves(true);
+ sinon
+ .stub(element._restApiHelper, 'fetch')
+ .resolves(new Response(undefined, {status: 204}));
+ const edit = await element.getChangeEdit(123 as NumericChangeId);
+ assert.isUndefined(edit);
+ });
- element._restApiHelper._logCall(
- {url: 'url', anonymizedUrl: 'not url'},
- 100,
- 200
- );
- await waitEventLoop();
- assert.isTrue(handler.calledOnce);
+ test('getChangeEdit returns edit patchset', async () => {
+ element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
+ sinon.stub(element, 'getLoggedIn').resolves(true);
+ const expected = createEditInfo();
+ sinon
+ .stub(element._restApiHelper, 'fetch')
+ .resolves(new Response(makePrefixedJSON(expected)));
+ const edit = await element.getChangeEdit(123 as NumericChangeId);
+ assert.deepEqual(edit, expected);
});
test('ported comment errors do not trigger error dialog', () => {
@@ -1606,10 +1865,11 @@ suite('gr-rest-api-service-impl tests', () => {
test('ported drafts are not requested user is not logged in', () => {
const change = createChange();
+ element.addRepoNameToCache(change._number, TEST_PROJECT_NAME);
sinon.stub(element, 'getLoggedIn').resolves(false);
const getChangeURLAndFetchStub = sinon.stub(
- element,
- '_getChangeURLAndFetch'
+ element._restApiHelper,
+ 'fetchJSON'
);
element.getPortedDrafts(change._number, CURRENT);
@@ -1618,21 +1878,22 @@ suite('gr-rest-api-service-impl tests', () => {
});
test('saveChangeStarred', async () => {
- sinon.stub(element, 'getFromProjectLookup').resolves('test' as RepoName);
- const sendStub = sinon.stub(element._restApiHelper, 'send').resolves();
+ element.addRepoNameToCache(123 as NumericChangeId, 'test' as RepoName);
+ element.addRepoNameToCache(456 as NumericChangeId, 'test' as RepoName);
+ const fetchStub = sinon.stub(element._restApiHelper, 'fetch').resolves();
await element.saveChangeStarred(123 as NumericChangeId, true);
- assert.isTrue(sendStub.calledOnce);
- assert.deepEqual(sendStub.lastCall.args[0], {
- method: HttpMethod.PUT,
+ assert.isTrue(fetchStub.calledOnce);
+ assert.deepEqual(fetchStub.lastCall.args[0], {
+ fetchOptions: {method: HttpMethod.PUT},
url: '/accounts/self/starred.changes/test~123',
anonymizedUrl: '/accounts/self/starred.changes/*',
});
await element.saveChangeStarred(456 as NumericChangeId, false);
- assert.isTrue(sendStub.calledTwice);
- assert.deepEqual(sendStub.lastCall.args[0], {
- method: HttpMethod.DELETE,
+ assert.isTrue(fetchStub.calledTwice);
+ assert.deepEqual(fetchStub.lastCall.args[0], {
+ fetchOptions: {method: HttpMethod.DELETE},
url: '/accounts/self/starred.changes/test~456',
anonymizedUrl: '/accounts/self/starred.changes/*',
});
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
index 546f06a0b7..947952ce1f 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
@@ -40,7 +40,6 @@ import {
FileNameToFileInfoMap,
FilePathToDiffInfoMap,
FixId,
- FixReplacementInfo,
GitRef,
GpgKeyId,
GpgKeyInfo,
@@ -99,8 +98,7 @@ import {
} from '../../types/diff';
import {Finalizable, ParsedChangeInfo} from '../../types/types';
import {ErrorCallback} from '../../api/rest';
-
-export type CancelConditionCallback = () => boolean;
+import {FixReplacementInfo} from '../../api/rest-api';
export interface GetDiffCommentsOutput {
baseComments: CommentInfo[];
@@ -122,8 +120,8 @@ export interface RestApiService extends Finalizable {
params?: string[]
): Promise<AccountCapabilityInfo | undefined>;
getExternalIds(): Promise<AccountExternalIdInfo[] | undefined>;
- deleteAccountIdentity(id: string[]): Promise<unknown>;
- deleteAccount(): Promise<unknown>;
+ deleteAccountIdentity(id: string[]): Promise<Response>;
+ deleteAccount(): Promise<Response>;
getRepos(
filter: string | undefined,
reposPerPage: number,
@@ -149,6 +147,11 @@ export interface RestApiService extends Finalizable {
headers?: Record<string, string>
): Promise<Response | void>;
+ /**
+ * DEPRECATED: Use functions from gr-rest-api-helper directly.
+ *
+ * Preserved for plugins that use it.
+ */
getResponseObject(response: Response): Promise<ParsedJSON>;
getChangeSuggestedReviewers(
@@ -165,13 +168,14 @@ export interface RestApiService extends Finalizable {
* Request list of accounts via https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#query-account
* Operators defined here https://gerrit-review.googlesource.com/Documentation/user-search-accounts.html#_search_operators
*/
- getSuggestedAccounts(
+ queryAccounts(
input: string,
n?: number,
canSee?: NumericChangeId,
filterActive?: boolean,
errFn?: ErrorCallback
): Promise<AccountInfo[] | undefined>;
+ getAccountSuggestions(input: string): Promise<AccountInfo[] | undefined>;
getSuggestedGroups(
input: string,
project?: RepoName,
@@ -188,7 +192,7 @@ export interface RestApiService extends Finalizable {
patchNum?: PatchSetNum,
payload?: RequestPayload,
errFn?: ErrorCallback
- ): Promise<Response | undefined>;
+ ): Promise<Response>;
getRepoBranches(
filter: string,
repo: RepoName,
@@ -199,8 +203,7 @@ export interface RestApiService extends Finalizable {
getChangeDetail(
changeNum?: number | string,
- errFn?: ErrorCallback,
- cancelCondition?: Function
+ errFn?: ErrorCallback
): Promise<ParsedChangeInfo | undefined>;
/**
@@ -321,7 +324,7 @@ export interface RestApiService extends Finalizable {
setRepoAccessRightsForReview(
projectName: RepoName,
projectInfo: ProjectAccessInput
- ): Promise<ChangeInfo>;
+ ): Promise<ChangeInfo | undefined>;
getGroups(
filter: string,
@@ -390,14 +393,24 @@ export interface RestApiService extends Finalizable {
): Promise<IncludedInInfo | undefined>;
/**
- * Checks in projectLookup map shared across instances for the changeNum.
- * If it exists, returns the project. If not, calls the restAPI to get the
- * change, populates projectLookup with the project for that change, and
- * returns the project.
+ * Looks up repo name in which change is located.
+ *
+ * Change -> repo association is cached. This will only make restAPI call (and
+ * cache the result) if the repo name for the change is not already known.
+ *
+ * addRepoNameToCache can be used to add entry to the cache manually.
+ *
+ * If the lookup fails the promise rejects and result is not cached.
*/
- getFromProjectLookup(
- changeNum: NumericChangeId
- ): Promise<RepoName | undefined>;
+ getRepoName(changeNum: NumericChangeId): Promise<RepoName>;
+
+ /**
+ * Populates cache for the future getRepoName(changeNum) lookup.
+ *
+ * The repo name is used for constructing of url for all change-based
+ * endpoints.
+ */
+ addRepoNameToCache(changeNum: NumericChangeId, repo: RepoName): void;
saveDiffDraft(
changeNum: NumericChangeId,
@@ -505,7 +518,7 @@ export interface RestApiService extends Finalizable {
saveAccountAgreement(name: ContributorAgreementInput): Promise<Response>;
- generateAccountHttpPassword(): Promise<Password>;
+ generateAccountHttpPassword(): Promise<Password | undefined>;
setAccountName(name: string): Promise<void>;
@@ -515,7 +528,7 @@ export interface RestApiService extends Finalizable {
saveWatchedProjects(
projects: ProjectWatchInfo[]
- ): Promise<ProjectWatchInfo[]>;
+ ): Promise<ProjectWatchInfo[] | undefined>;
deleteWatchedProjects(projects: ProjectWatchInfo[]): Promise<Response>;
@@ -558,7 +571,7 @@ export interface RestApiService extends Finalizable {
patchNum: PatchSetNum,
commentID: UrlEncodedCommentId,
reason: string
- ): Promise<CommentInfo>;
+ ): Promise<CommentInfo | undefined>;
deleteDiffDraft(
changeNum: NumericChangeId,
patchNum: PatchSetNum,
@@ -589,7 +602,7 @@ export interface RestApiService extends Finalizable {
saveGroupMember(
groupName: GroupId | GroupName,
groupMember: AccountId
- ): Promise<AccountInfo>;
+ ): Promise<AccountInfo | undefined>;
saveIncludedGroup(
groupName: GroupId | GroupName,
@@ -784,10 +797,30 @@ export interface RestApiService extends Finalizable {
setChangeHashtag(
changeNum: NumericChangeId,
- hashtag: HashtagsInput
- ): Promise<Hashtag[]>;
+ hashtag: HashtagsInput,
+ errFn?: ErrorCallback
+ ): Promise<Hashtag[] | undefined>;
- setChangeTopic(changeNum: NumericChangeId, topic?: string): Promise<string>;
+ /**
+ * Set change topic.
+ *
+ * Returns topic that the change has after the requests.
+ */
+ setChangeTopic(
+ changeNum: NumericChangeId,
+ topic?: string,
+ errFn?: ErrorCallback
+ ): Promise<string | undefined>;
+
+ /**
+ * Remove change topic.
+ *
+ * Returns topic that the change has after the requests. (ie. '' on success)
+ */
+ removeChangeTopic(
+ changeNum: NumericChangeId,
+ errFn?: ErrorCallback
+ ): Promise<string | undefined>;
getChangeFiles(
changeNum: NumericChangeId,
@@ -813,14 +846,21 @@ export interface RestApiService extends Finalizable {
getTopMenus(): Promise<TopMenuEntryInfo[] | undefined>;
- setInProjectLookup(changeNum: NumericChangeId, repo: RepoName): void;
getMergeable(changeNum: NumericChangeId): Promise<MergeableInfo | undefined>;
putChangeCommitMessage(
changeNum: NumericChangeId,
- message: string
+ message: string,
+ committerEmail: string | null
): Promise<Response>;
+ updateIdentityInChangeEdit(
+ changeNum: NumericChangeId,
+ name: string,
+ email: string,
+ type: string
+ ): Promise<Response | undefined>;
+
getChangeCommitInfo(
changeNum: NumericChangeId,
patchNum: PatchSetNum
diff --git a/polygerrit-ui/app/services/scheduler/retry-scheduler.ts b/polygerrit-ui/app/services/scheduler/retry-scheduler.ts
index 04ced2f9ff..d96fc0ba95 100644
--- a/polygerrit-ui/app/services/scheduler/retry-scheduler.ts
+++ b/polygerrit-ui/app/services/scheduler/retry-scheduler.ts
@@ -15,6 +15,16 @@ function untilTimeout(ms: number) {
return new Promise(resolve => window.setTimeout(resolve, ms));
}
+/**
+ * The scheduler that retries tasks on RetryError.
+ *
+ * The task is only retried if the RetryError was thrown, all other errors cause
+ * the worker to stop and the error is re-thrown.
+ *
+ * The number of retries are limited by maxRetry, the retries are performed
+ * according to exponential backoff, configured by backoffIntervalMs
+ * and backoffFactor.
+ */
export class RetryScheduler<T> implements Scheduler<T> {
constructor(
private readonly base: Scheduler<T>,
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
index db9d8fe373..a352f68d7f 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
@@ -175,6 +175,12 @@ export class ShortcutsService implements Finalizable {
if (optPreventDefault) e.preventDefault();
if (optPreventDefault) e.stopPropagation();
this.reportTriggered(e);
+ if (shortcut.combo) {
+ // Do not reset immediately, otherwise other shortcut might be triggered.
+ setTimeout(() => {
+ this.comboKeyLastPressed = {};
+ });
+ }
listener(e);
};
element.addEventListener('keydown', wrappedListener);
diff --git a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
index 7969264741..77f2498e6f 100644
--- a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
+++ b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
@@ -61,7 +61,6 @@ import {
DraftInfo,
} from '../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../types/diff';
-import {readResponsePayload} from '../../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
import {
createAccountDetailWithId,
createChange,
@@ -77,6 +76,8 @@ import {
createDefaultPreferences,
} from '../../constants/constants';
import {ParsedChangeInfo} from '../../types/types';
+import {readJSONResponsePayload} from '../../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {ErrorCallback} from '../../api/rest';
export const grRestApiMock: RestApiService = {
addAccountEmail(): Promise<Response> {
@@ -127,14 +128,14 @@ export const grRestApiMock: RestApiService = {
deleteAccountGPGKey(): Promise<Response> {
return Promise.resolve(new Response());
},
- deleteAccountIdentity(): Promise<unknown> {
+ deleteAccountIdentity(): Promise<Response> {
return Promise.resolve(new Response());
},
deleteAccountSSHKey(): void {},
deleteChangeCommitMessage(): Promise<Response> {
return Promise.resolve(new Response());
},
- deleteComment(): Promise<CommentInfo> {
+ deleteComment(): Promise<CommentInfo | undefined> {
throw new Error('deleteComment() not implemented by RestApiMock.');
},
deleteDiffDraft(): Promise<Response> {
@@ -164,11 +165,11 @@ export const grRestApiMock: RestApiService = {
deleteWatchedProjects(): Promise<Response> {
return Promise.resolve(new Response());
},
- executeChangeAction(): Promise<Response | undefined> {
+ executeChangeAction(): Promise<Response> {
return Promise.resolve(new Response());
},
finalize(): void {},
- generateAccountHttpPassword(): Promise<Password> {
+ generateAccountHttpPassword(): Promise<Password | undefined> {
return Promise.resolve('asdf');
},
getAccount(): Promise<AccountDetailInfo | undefined> {
@@ -324,8 +325,8 @@ export const grRestApiMock: RestApiService = {
getFileContent(): Promise<Response | Base64FileContent | undefined> {
return Promise.resolve(new Response());
},
- getFromProjectLookup(): Promise<RepoName | undefined> {
- throw new Error('getFromProjectLookup() not implemented by RestApiMock.');
+ getRepoName(): Promise<RepoName> {
+ throw new Error('getRepoName() not implemented by RestApiMock.');
},
getGroupAuditLog(): Promise<GroupAuditEventInfo[] | undefined> {
return Promise.resolve([]);
@@ -402,7 +403,7 @@ export const grRestApiMock: RestApiService = {
return Promise.resolve([]);
},
getResponseObject(response: Response): Promise<ParsedJSON> {
- return readResponsePayload(response).then(payload => payload.parsed);
+ return readJSONResponsePayload(response).then(payload => payload.parsed);
},
getReviewedFiles(): Promise<string[] | undefined> {
return Promise.resolve([]);
@@ -413,7 +414,10 @@ export const grRestApiMock: RestApiService = {
getRobotCommentFixPreview(): Promise<FilePathToDiffInfoMap | undefined> {
return Promise.resolve({});
},
- getSuggestedAccounts(): Promise<AccountInfo[] | undefined> {
+ queryAccounts(): Promise<AccountInfo[] | undefined> {
+ return Promise.resolve([]);
+ },
+ getAccountSuggestions(): Promise<AccountInfo[] | undefined> {
return Promise.resolve([]);
},
getSuggestedGroups(): Promise<GroupNameToGroupInfoMap | undefined> {
@@ -492,7 +496,7 @@ export const grRestApiMock: RestApiService = {
saveGroupDescription(): Promise<Response> {
return Promise.resolve(new Response());
},
- saveGroupMember(): Promise<AccountInfo> {
+ saveGroupMember(): Promise<AccountInfo | undefined> {
return Promise.resolve({});
},
saveGroupName(): Promise<Response> {
@@ -514,7 +518,7 @@ export const grRestApiMock: RestApiService = {
saveRepoConfig(): Promise<Response> {
return Promise.resolve(new Response());
},
- saveWatchedProjects(): Promise<ProjectWatchInfo[]> {
+ saveWatchedProjects(): Promise<ProjectWatchInfo[] | undefined> {
return Promise.resolve([]);
},
send() {
@@ -532,26 +536,35 @@ export const grRestApiMock: RestApiService = {
setAccountUsername(): Promise<void> {
return Promise.resolve();
},
- setChangeHashtag(): Promise<Hashtag[]> {
+ setChangeHashtag(): Promise<Hashtag[] | undefined> {
return Promise.resolve([]);
},
- setChangeTopic(): Promise<string> {
+ setChangeTopic(): Promise<string | undefined> {
return Promise.resolve('');
},
+ removeChangeTopic(
+ changeNum: NumericChangeId,
+ errFn?: ErrorCallback
+ ): Promise<string | undefined> {
+ return this.setChangeTopic(changeNum, '', errFn);
+ },
setDescription(): Promise<Response> {
return Promise.resolve(new Response());
},
- setInProjectLookup(): void {},
+ addRepoNameToCache(): void {},
setPreferredAccountEmail(): Promise<void> {
return Promise.resolve();
},
setRepoAccessRights(): Promise<Response> {
return Promise.resolve(new Response());
},
- setRepoAccessRightsForReview(): Promise<ChangeInfo> {
+ setRepoAccessRightsForReview(): Promise<ChangeInfo | undefined> {
throw new Error('setRepoAccessRightsForReview() not implemented by mock.');
},
setRepoHead(): Promise<Response> {
return Promise.resolve(new Response());
},
+ updateIdentityInChangeEdit(): Promise<Response | undefined> {
+ return Promise.resolve(new Response());
+ },
};
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index d941c9f1f9..cba3a05047 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -105,7 +105,7 @@ import {
} from '../api/rest-api';
import {CheckResult, CheckRun, RunResult} from '../models/checks/checks-model';
import {Category, Fix, Link, LinkIcon, RunStatus} from '../api/checks';
-import {DiffInfo} from '../api/diff';
+import {DiffInfo, GrDiffLineType} from '../api/diff';
import {SearchViewState} from '../models/views/search';
import {ChangeChildView, ChangeViewState} from '../models/views/change';
import {NormalizedFileInfo} from '../models/change/files-model';
@@ -113,6 +113,11 @@ import {GroupViewState} from '../models/views/group';
import {RepoDetailView, RepoViewState} from '../models/views/repo';
import {AdminChildView, AdminViewState} from '../models/views/admin';
import {DashboardType, DashboardViewState} from '../models/views/dashboard';
+import {GrDiffLine} from '../embed/diff/gr-diff/gr-diff-line';
+import {
+ GrDiffGroup,
+ GrDiffGroupType,
+} from '../embed/diff/gr-diff/gr-diff-group';
const TEST_DEFAULT_EXPRESSION = 'label:Verified=MAX -label:Verified=MIN';
export const TEST_PROJECT_NAME: RepoName = 'test-project' as RepoName;
@@ -457,6 +462,7 @@ export function createAuth(): AuthInfo {
export function createChangeConfig(): ChangeConfigInfo {
return {
+ allow_blame: true,
large_change: 500,
// The default update_delay is 5 minutes, but we don't want to accidentally
// start polling in tests
@@ -662,6 +668,23 @@ export function createDiff(): DiffInfo {
};
}
+export function createContextGroup(options: {offset?: number; count?: number}) {
+ const offset = options.offset || 0;
+ const numLines = options.count || 10;
+ const lines = [];
+ for (let i = 0; i < numLines; i++) {
+ const line = new GrDiffLine(GrDiffLineType.BOTH);
+ line.beforeNumber = offset + i + 1;
+ line.afterNumber = offset + i + 1;
+ line.text = 'lorem upsum';
+ lines.push(line);
+ }
+ return new GrDiffGroup({
+ type: GrDiffGroupType.CONTEXT_CONTROL,
+ contextGroups: [new GrDiffGroup({type: GrDiffGroupType.BOTH, lines})],
+ });
+}
+
export function createBlame(): BlameInfo {
return {
author: 'test-author',
@@ -686,6 +709,7 @@ export function createPreferences(): PreferencesInfo {
changes_per_page: 10,
email_strategy: EmailStrategy.ENABLED,
allow_browser_notifications: true,
+ allow_suggest_code_while_commenting: true,
};
}
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 6e20dd41e9..571edf0936 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -17,6 +17,7 @@ import {assert} from '@open-wc/testing';
import {Route, ViewState} from '../models/views/base';
import {PageContext} from '../elements/core/gr-router/gr-page';
import {waitUntil} from '../utils/async-util';
+import {JSON_PREFIX} from '../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
export {query, queryAll, queryAndAssert} from '../utils/common-util';
export {mockPromise, waitUntil} from '../utils/async-util';
export type {MockPromise} from '../utils/async-util';
@@ -311,3 +312,7 @@ export function assertRouteFalse<T extends ViewState>(
const matches = ctx.match(route.urlPattern);
assert.isFalse(matches);
}
+
+export function makePrefixedJSON(obj: any) {
+ return JSON_PREFIX + JSON.stringify(obj);
+}
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 9992f8b4a0..0cf9cd9fe1 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -119,6 +119,9 @@ import {
isQuickLabelInfo,
Base64FileContent,
CommentRange,
+ FixReplacementInfo,
+ FixSuggestionInfo,
+ FixId,
} from '../api/rest-api';
import {DiffInfo, IgnoreWhitespaceType} from './diff';
import {PatchRange, LineNumber} from '../api/diff';
@@ -144,8 +147,8 @@ export type {
ChangeMessageId,
ChangeMessageInfo,
ChangeSubmissionId,
- CommentInfo,
CommentLinkInfo,
+ CommentInfo,
CommentLinks,
CommentRange,
CommitId,
@@ -163,6 +166,8 @@ export type {
EditPatchSet,
EmailAddress,
FileInfo,
+ FixId,
+ FixSuggestionInfo,
GerritInfo,
GitPersonInfo,
GitRef,
@@ -237,9 +242,6 @@ export type RobotRunId = BrandType<string, '_robotRunId'>;
// in our code, so it is not added here as a possible value.
export type RevisionId = 'current' | CommitId | PatchSetNum;
-// The UUID of the suggested fix.
-export type FixId = BrandType<string, '_fixId'>;
-
// The ID of the dashboard, in the form of '<ref>:<path>'
export type DashboardId = BrandType<string, '_dahsboardId'>;
@@ -1201,6 +1203,7 @@ export interface CommentInput {
message?: string;
tag?: string;
unresolved?: boolean;
+ fix_suggestions?: FixSuggestionInfo[];
}
/**
@@ -1335,6 +1338,7 @@ export interface PreferencesInfo {
// The email_format doesn't mentioned in doc, but exists in Java class GeneralPreferencesInfo
email_format?: EmailFormat;
allow_browser_notifications?: boolean;
+ allow_suggest_code_while_commenting?: boolean;
diff_page_sidebar?: DiffPageSidebar;
}
@@ -1428,26 +1432,10 @@ export interface RobotCommentInfo extends CommentInfo {
robot_run_id: RobotRunId;
url?: string;
properties: {[propertyName: string]: string};
- fix_suggestions: FixSuggestionInfo[];
}
export type PathToRobotCommentsInfoMap = {[path: string]: RobotCommentInfo[]};
/**
- * The FixSuggestionInfo entity represents a suggested fix
- * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#fix-suggestion-info
- */
-export interface FixSuggestionInfoInput {
- description: string;
- replacements: FixReplacementInfo[];
-}
-
-export interface FixSuggestionInfo extends FixSuggestionInfoInput {
- fix_id: FixId;
- description: string;
- replacements: FixReplacementInfo[];
-}
-
-/**
* The ApplyProvidedFixInput entity contains information for applying fixes, provided in the
* request body, to a revision.
* https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#apply-provided-fix
@@ -1457,16 +1445,6 @@ export interface ApplyProvidedFixInput {
}
/**
- * The FixReplacementInfo entity describes how the content of a file should be replaced by another content
- * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#fix-replacement-info
- */
-export interface FixReplacementInfo {
- path: string;
- range: CommentRange;
- replacement: string;
-}
-
-/**
* The NotifyInfo entity contains detailed information about who should be notified about an update
* https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#notify-info
*/
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index e59a066519..6e4ce8686e 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -118,6 +118,7 @@ export type DropEvent = DragEvent;
export interface EditableContentSaveEventDetail {
content: string;
+ committerEmail: string | null;
}
export type EditableContentSaveEvent =
CustomEvent<EditableContentSaveEventDetail>;
diff --git a/polygerrit-ui/app/utils/change-util.ts b/polygerrit-ui/app/utils/change-util.ts
index 5079abdd16..a78254d23f 100644
--- a/polygerrit-ui/app/utils/change-util.ts
+++ b/polygerrit-ui/app/utils/change-util.ts
@@ -87,10 +87,8 @@ export function changeStatuses(
change: ChangeInfo,
options?: ChangeStatusesOptions
): ChangeStates[] {
- const states = [];
- if (change.revert_of) {
- states.push(ChangeStates.REVERT);
- }
+ const states: ChangeStates[] = [];
+
if (change.status === ChangeStatus.MERGED) {
if (options?.revertingChangeStatus === ChangeStatus.MERGED) {
return [ChangeStates.MERGED, ChangeStates.REVERT_SUBMITTED];
@@ -103,6 +101,10 @@ export function changeStatuses(
if (change.status === ChangeStatus.ABANDONED) {
return [ChangeStates.ABANDONED];
}
+
+ if (change.revert_of) {
+ states.push(ChangeStates.REVERT);
+ }
if (change.mergeable === false || (options && options.mergeable === false)) {
// 'mergeable' prop may not always exist (@see Issue 6819)
states.push(ChangeStates.MERGE_CONFLICT);
@@ -116,15 +118,29 @@ export function changeStatuses(
states.push(ChangeStates.PRIVATE);
}
- // If there are any pre-defined statuses, only return those. Otherwise,
- // will determine the derived status.
- if (states.length || !options) {
+ // The gr-change-list table does not want READY TO SUBMIT or ACTIVE and it
+ // does not pass options.
+ if (!options) {
+ return states;
+ }
+
+ // The change is not submittable if there are conflicts or is WIP/private even
+ // if the submit requirements are ok.
+ if (
+ [
+ ChangeStates.MERGE_CONFLICT,
+ ChangeStates.GIT_CONFLICT,
+ ChangeStates.WIP,
+ ChangeStates.PRIVATE,
+ ].some(unsubmittableState => states.includes(unsubmittableState))
+ ) {
return states;
}
if (change.submittable) {
states.push(ChangeStates.READY_TO_SUBMIT);
- } else {
+ }
+ if (states.length === 0) {
states.push(ChangeStates.ACTIVE);
}
return states;
diff --git a/polygerrit-ui/app/utils/change-util_test.ts b/polygerrit-ui/app/utils/change-util_test.ts
index 6e53c160a5..51e935ef73 100644
--- a/polygerrit-ui/app/utils/change-util_test.ts
+++ b/polygerrit-ui/app/utils/change-util_test.ts
@@ -176,6 +176,18 @@ suite('change-util tests', () => {
]);
});
+ test('Revert that is submittable', () => {
+ const change = {
+ ...createChange(),
+ revert_of: 123 as NumericChangeId,
+ submittable: true,
+ };
+ assert.deepEqual(changeStatuses(change, {mergeable: true}), [
+ ChangeStates.REVERT,
+ ChangeStates.READY_TO_SUBMIT,
+ ]);
+ });
+
test('Open status with private and wip', () => {
const change = {
...createChange(),
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index 7fad329bd0..e82b3285c4 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -262,6 +262,15 @@ export function isRobotThread(thread: CommentThread): boolean {
return isRobot(getFirstComment(thread));
}
+export function hasSuggestion(thread: CommentThread): boolean {
+ const firstComment = getFirstComment(thread);
+ if (!firstComment) return false;
+ return (
+ hasUserSuggestion(firstComment) ||
+ firstComment.fix_suggestions?.[0] !== undefined
+ );
+}
+
export function hasHumanReply(thread: CommentThread): boolean {
return countComments(thread) > 1 && !isRobot(getLastComment(thread));
}
@@ -589,5 +598,8 @@ export function convertToCommentInput(comment: Comment): CommentInput {
if (comment.tag) {
output.tag = comment.tag;
}
+ if (comment.fix_suggestions) {
+ output.fix_suggestions = comment.fix_suggestions;
+ }
return output;
}
diff --git a/polygerrit-ui/app/utils/display-name-util.ts b/polygerrit-ui/app/utils/display-name-util.ts
index 850509fdc9..5f56e513d7 100644
--- a/polygerrit-ui/app/utils/display-name-util.ts
+++ b/polygerrit-ui/app/utils/display-name-util.ts
@@ -63,7 +63,7 @@ export function getAccountDisplayName(
.join(' ');
}
-function accountEmail(email?: string) {
+export function accountEmail(email?: string) {
if (typeof email !== 'undefined') {
return '<' + email + '>';
}
diff --git a/polygerrit-ui/app/utils/file-util.ts b/polygerrit-ui/app/utils/file-util.ts
index 246ac20bb6..a82ce10a9b 100644
--- a/polygerrit-ui/app/utils/file-util.ts
+++ b/polygerrit-ui/app/utils/file-util.ts
@@ -43,3 +43,11 @@ export function expandFileMode(input?: string) {
}
return input;
}
+
+export function getFileExtension(fileName: string): string {
+ const index = fileName.lastIndexOf('.');
+ if (index === -1) {
+ return '';
+ }
+ return fileName.substring(index + 1);
+}
diff --git a/polygerrit-ui/app/utils/file-util_test.ts b/polygerrit-ui/app/utils/file-util_test.ts
index aeab0268bb..f725041bfb 100644
--- a/polygerrit-ui/app/utils/file-util_test.ts
+++ b/polygerrit-ui/app/utils/file-util_test.ts
@@ -3,9 +3,14 @@
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {assert} from '@open-wc/testing';
import '../test/common-test-setup';
-import {expandFileMode, FileMode, fileModeToString} from './file-util';
+import {assert} from '@open-wc/testing';
+import {
+ expandFileMode,
+ FileMode,
+ fileModeToString,
+ getFileExtension,
+} from './file-util';
suite('file-util tests', () => {
test('fileModeToString', () => {
@@ -35,4 +40,15 @@ suite('file-util tests', () => {
['old mode regular (100644)', 'new mode executable (100755)']
);
});
+
+ suite('getFileExtension', () => {
+ test('returns an empty string when the file name does not have an extension', () => {
+ assert.equal(getFileExtension('my_file'), '');
+ });
+ test('returns the extension when the file name has an extension', () => {
+ assert.equal(getFileExtension('my_file.txt'), 'txt');
+ assert.equal(getFileExtension('folder/my_file.java'), 'java');
+ assert.equal(getFileExtension('.hidden_file.ts'), 'ts');
+ });
+ });
});
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index 27366d9542..bdc7cb030d 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -101,6 +101,32 @@ export function hasNeutralStatus(
return getLabelStatus(label, approvalInfo.value) === LabelStatus.NEUTRAL;
}
+export function hasApprovedVote(labelInfo: LabelInfo) {
+ if (isDetailedLabelInfo(labelInfo)) {
+ return getAllUniqueApprovals(labelInfo).some(
+ approval =>
+ getLabelStatus(labelInfo, approval.value) === LabelStatus.APPROVED
+ );
+ } else if (isQuickLabelInfo(labelInfo)) {
+ return getLabelStatus(labelInfo) === LabelStatus.APPROVED;
+ } else {
+ return false;
+ }
+}
+
+export function hasRejectedVote(labelInfo: LabelInfo) {
+ if (isDetailedLabelInfo(labelInfo)) {
+ return getAllUniqueApprovals(labelInfo).some(
+ approval =>
+ getLabelStatus(labelInfo, approval.value) === LabelStatus.REJECTED
+ );
+ } else if (isQuickLabelInfo(labelInfo)) {
+ return getLabelStatus(labelInfo) === LabelStatus.REJECTED;
+ } else {
+ return false;
+ }
+}
+
export function classForLabelStatus(status: LabelStatus) {
switch (status) {
case LabelStatus.APPROVED:
@@ -221,6 +247,21 @@ export function getCodeReviewLabel(
return;
}
+export function extractLabelsWithCountFrom(expression: string) {
+ const pattern = new RegExp(
+ 'label[0-9]*:([\\w-]+)[^,]*,count>?=?([0-9])',
+ 'g'
+ );
+ const labels = [];
+ let match;
+ while ((match = pattern.exec(expression)) !== null) {
+ if (match[2] && !isNaN(Number(match[2]))) {
+ labels.push({label: match[1], count: Number(match[2])});
+ }
+ }
+ return labels;
+}
+
function extractLabelsFrom(expression: string) {
const pattern = new RegExp('label[0-9]*:([\\w-]+)', 'g');
const labels = [];
diff --git a/polygerrit-ui/app/utils/label-util_test.ts b/polygerrit-ui/app/utils/label-util_test.ts
index c86bda9cf0..a0b6c5079f 100644
--- a/polygerrit-ui/app/utils/label-util_test.ts
+++ b/polygerrit-ui/app/utils/label-util_test.ts
@@ -26,6 +26,7 @@ import {
valueString,
hasVotes,
hasVoted,
+ extractLabelsWithCountFrom,
} from './label-util';
import {
AccountId,
@@ -505,6 +506,31 @@ suite('label-util', () => {
});
});
+ suite('extractLabelsWithCountFrom', () => {
+ test('returns an empty array when the expression does not match the pattern', () => {
+ assert.deepEqual(extractLabelsWithCountFrom('foo'), []);
+ assert.deepEqual(
+ extractLabelsWithCountFrom('label:Verified=MAX -label:Code-Review=MIN'),
+ []
+ );
+ });
+
+ test('returns an empty array when count is not number', () => {
+ assert.deepEqual(extractLabelsWithCountFrom('label:name,count>=a'), []);
+ });
+
+ test('returns an array with label and count object when the expression matches the pattern', () => {
+ assert.deepEqual(extractLabelsWithCountFrom('label1:name=MIN,count>=1'), [
+ {label: 'name', count: 1},
+ ]);
+
+ assert.deepEqual(
+ extractLabelsWithCountFrom('label:Code-Review=MAX,count>=2'),
+ [{label: 'Code-Review', count: 2}]
+ );
+ });
+ });
+
suite('getRequirements()', () => {
function createChangeInfoWith(
submit_requirements: SubmitRequirementResultInfo[]
diff --git a/polygerrit-ui/app/utils/location-util.ts b/polygerrit-ui/app/utils/location-util.ts
new file mode 100644
index 0000000000..d0eac74df8
--- /dev/null
+++ b/polygerrit-ui/app/utils/location-util.ts
@@ -0,0 +1,22 @@
+/**
+ * @license
+ * Copyright 2024 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// This file adds some simple checks to match internal Google rules.
+// Internally at Google it has different a implementation.
+
+import {safeLocation} from 'safevalues/dom';
+
+export function setHref(loc: Location, url: string) {
+ safeLocation.setHref(loc, url);
+}
+
+export function replace(loc: Location, url: string) {
+ safeLocation.replace(loc, url);
+}
+
+export function assign(loc: Location, url: string) {
+ safeLocation.assign(loc, url);
+}
diff --git a/polygerrit-ui/app/utils/url-util.ts b/polygerrit-ui/app/utils/url-util.ts
index 96edc7e35e..6c38e59116 100644
--- a/polygerrit-ui/app/utils/url-util.ts
+++ b/polygerrit-ui/app/utils/url-util.ts
@@ -203,6 +203,15 @@ export function generateAbsoluteUrl(url: string) {
export function sameOrigin(href: string) {
if (!href) return false;
- const url = new URL(href, window.location.origin);
+ let url;
+ try {
+ url = new URL(href, window.location.origin);
+ } catch (e) {
+ // If the link is not valid url consider to be not the same origin.
+ if (e instanceof TypeError) {
+ return false;
+ }
+ throw e;
+ }
return url.origin === window.location.origin;
}
diff --git a/polygerrit-ui/app/utils/url-util_test.ts b/polygerrit-ui/app/utils/url-util_test.ts
index a92d8b158c..265becf154 100644
--- a/polygerrit-ui/app/utils/url-util_test.ts
+++ b/polygerrit-ui/app/utils/url-util_test.ts
@@ -133,6 +133,7 @@ suite('url-util tests', () => {
assert.isTrue(sameOrigin('/asdf'));
assert.isTrue(sameOrigin(window.location.origin + '/asdf'));
assert.isFalse(sameOrigin('http://www.goole.com/asdf'));
+ assert.isFalse(sameOrigin('http://b]'));
});
test('toPathname', () => {
diff --git a/polygerrit-ui/app/workers/service-worker-class.ts b/polygerrit-ui/app/workers/service-worker-class.ts
index 260ee57ee1..fa9dfd52a5 100644
--- a/polygerrit-ui/app/workers/service-worker-class.ts
+++ b/polygerrit-ui/app/workers/service-worker-class.ts
@@ -5,7 +5,7 @@
*/
import {ParsedChangeInfo} from '../types/types';
import {getReason} from '../utils/attention-set-util';
-import {readResponsePayload} from '../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {readJSONResponsePayload} from '../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
import {filterAttentionChangesAfter} from '../utils/service-worker-util';
import {AccountDetailInfo} from '../api/rest-api';
import {
@@ -177,9 +177,19 @@ export class ServiceWorker {
const response = await fetch(
'/changes/?O=1000081&S=0&n=25&q=attention%3Aself'
);
- const payload = await readResponsePayload(response);
- const changes = payload.parsed as unknown as ParsedChangeInfo[] | undefined;
- return changes ?? [];
+
+ try {
+ // Throws an error if response payload is not prefixed JSON.
+ return (await readJSONResponsePayload(response))
+ .parsed as unknown as ParsedChangeInfo[];
+ } catch (err) {
+ if (err instanceof Error) {
+ console.warn(
+ `Request for latest attention set changes failed. Error: ${err.message}`
+ );
+ }
+ return [];
+ }
}
/**
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index 9cca523e65..dcc7479a58 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -2,17 +2,17 @@
# yarn lockfile v1
-"@lit-labs/ssr-dom-shim@^1.1.2-pre.0":
+"@lit-labs/ssr-dom-shim@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz#d693d972974a354034454ec1317eb6afd0b00312"
integrity sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g==
"@lit/reactive-element@^2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.0.0.tgz#da14a256ac5533873b935840f306d572bac4a2ab"
- integrity sha512-wn+2+uDcs62ROBmVAwssO4x5xue/uKD3MGGZOXL2sMxReTRIT0JXKyMXeu7gh0aJ4IJNEIG/3aOnUaQvM7BMzQ==
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.0.2.tgz#779ae9d265407daaf7737cb892df5ec2a86e22a0"
+ integrity sha512-SVOwLAWUQg3Ji1egtOt1UiFe4zdDpnWHyc5qctSceJ5XIu0Uc76YmGpIjZgx9YJ0XtdW0Jm507sDvjOu+HnB8w==
dependencies:
- "@lit-labs/ssr-dom-shim" "^1.1.2-pre.0"
+ "@lit-labs/ssr-dom-shim" "^1.1.2"
"@mapbox/node-pre-gyp@^1.0.0":
version "1.0.11"
@@ -430,20 +430,20 @@
dependencies:
"@webcomponents/shadycss" "^1.9.1"
-"@types/resemblejs@^4.1.0":
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/@types/resemblejs/-/resemblejs-4.1.0.tgz#1c150e0de4117b29f9d5d5231489edc7cef8263e"
- integrity sha512-+MIkKy/UngDfhTnvn2yK/KSzlbtLeB5BU73qqZrzIF24+e2r8enJ4cW3UbtkstByYSDV8pbheGAqg7zT8ZZ2pA==
+"@types/resemblejs@^4.1.3":
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/@types/resemblejs/-/resemblejs-4.1.3.tgz#46d16888952e377b9143484c206b63f6da56e91e"
+ integrity sha512-p0NA5aACdWCK+I4NJbwUvFoixwYxvfLu+UqaiZt/J3+3PJavMYOxRrdbeXbbiKiMGdKdDFjoxlFWkkaMU7SDxA==
-"@types/resize-observer-browser@^0.1.7":
- version "0.1.8"
- resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.8.tgz#db41a93f9f37dad8e6f0c7bd2f5bbc042bf714d1"
- integrity sha512-OpjAd26fD1G2OWlYzkrapJ12n+kyi0znYgE2AHfNccHY/am3kG+lfJ5brfcZ7+1CIybkPWGKrW+Wm97kbcOQaQ==
+"@types/resize-observer-browser@^0.1.11":
+ version "0.1.11"
+ resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.11.tgz#d3c98d788489d8376b7beac23863b1eebdd3c13c"
+ integrity sha512-cNw5iH8JkMkb3QkCoe7DaZiawbDQEUX8t7iuQaRTyLOyQCR2h+ibBD4GJt7p5yhUHrlOeL7ZtbxNHeipqNsBzQ==
"@types/trusted-types@^2.0.2":
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.4.tgz#2b38784cd16957d3782e8e2b31c03bc1d13b4d65"
- integrity sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==
+ version "2.0.7"
+ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
+ integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
"@webcomponents/shadycss@^1.11.2", "@webcomponents/shadycss@^1.9.1":
version "1.11.2"
@@ -610,10 +610,10 @@ highlight.js@^10.4.1:
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==
-"highlight.js@^11.5.0 || ^10.4.1", highlight.js@^11.8.0:
- version "11.8.0"
- resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.8.0.tgz#966518ea83257bae2e7c9a48596231856555bb65"
- integrity sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==
+"highlight.js@^11.5.0 || ^10.4.1", highlight.js@^11.9.0:
+ version "11.9.0"
+ resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0"
+ integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==
"highlightjs-closure-templates@https://github.com/highlightjs/highlightjs-closure-templates":
version "0.0.1"
@@ -659,29 +659,29 @@ is-fullwidth-code-point@^3.0.0:
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
lit-element@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-4.0.0.tgz#8343891bc9159a5fcb7f534914b37f2c0161e036"
- integrity sha512-N6+f7XgusURHl69DUZU6sTBGlIN+9Ixfs3ykkNDfgfTkDYGGOWwHAYBhDqVswnFGyWgQYR2KiSpu4J76Kccs/A==
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-4.0.2.tgz#1a519896d5ab7c7be7a8729f400499e38779c093"
+ integrity sha512-/W6WQZUa5VEXwC7H9tbtDMdSs9aWil3Ou8hU6z2cOKWbsm/tXPAcsoaHVEtrDo0zcOIE5GF6QgU55tlGL2Nihg==
dependencies:
- "@lit-labs/ssr-dom-shim" "^1.1.2-pre.0"
+ "@lit-labs/ssr-dom-shim" "^1.1.2"
"@lit/reactive-element" "^2.0.0"
- lit-html "^3.0.0"
+ lit-html "^3.1.0"
-lit-html@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-3.0.0.tgz#77d6776ee488642c74c5575315ef81aa09d24ea9"
- integrity sha512-DNJIE8dNY0dQF2Gs0sdMNUppMQT2/CvV4OVnSdg7BXAsGqkVwsE5bqQ04POfkYH5dBIuGnJYdFz5fYYyNnOxiA==
+lit-html@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-3.1.0.tgz#a7b93dd682073f2e2029656f4e9cd91e8034c196"
+ integrity sha512-FwAjq3iNsaO6SOZXEIpeROlJLUlrbyMkn4iuv4f4u1H40Jw8wkeR/OUXZUHUoiYabGk8Y4Y0F/rgq+R4MrOLmA==
dependencies:
"@types/trusted-types" "^2.0.2"
-lit@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/lit/-/lit-3.0.0.tgz#204bd65935892a73670471e893ee8ca55d2f9a3b"
- integrity sha512-nQ0teRzU1Kdj++VdmttS2WvIen8M79wChJ6guRKIIym2M3Ansg3Adj9O6yuQh2IpjxiUXlNuS81WKlQ4iL3BmA==
+lit@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/lit/-/lit-3.1.0.tgz#76429b85dc1f5169fed499a0f7e89e2e619010c9"
+ integrity sha512-rzo/hmUqX8zmOdamDAeydfjsGXbbdtAFqMhmocnh2j9aDYqbu0fjXygjCa0T99Od9VQ/2itwaGrjZz/ZELVl7w==
dependencies:
"@lit/reactive-element" "^2.0.0"
lit-element "^4.0.0"
- lit-html "^3.0.0"
+ lit-html "^3.1.0"
lru-cache@^6.0.0:
version "6.0.0"
@@ -927,10 +927,10 @@ util-deprecate@^1.0.1:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
-web-vitals@^3.4.0:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-3.4.0.tgz#45ed33a3a2e029dc38d36547eb5d71d1c7e2552d"
- integrity sha512-n9fZ5/bG1oeDkyxLWyep0eahrNcPDF6bFqoyispt7xkW0xhDzpUBTgyDKqWDi1twT0MgH4HvvqzpUyh0ZxZV4A==
+web-vitals@^3.5.1:
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-3.5.1.tgz#af7a9dc60708b81007922ab55a23d963676ba30a"
+ integrity sha512-xQ9lvIpfLxUj0eSmT79ZjRoU5wIRfIr7pNukL7ZE4EcWZSmfZQqOlhuAGfkVa3EFmzPHZhWhXfm2i5ys+THVPg==
webidl-conversions@^3.0.0:
version "3.0.1"
diff --git a/polygerrit-ui/yarn.lock b/polygerrit-ui/yarn.lock
index 6e8259c2a1..6f53c643a4 100644
--- a/polygerrit-ui/yarn.lock
+++ b/polygerrit-ui/yarn.lock
@@ -18,46 +18,46 @@
"@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9"
-"@babel/code-frame@^7.12.11", "@babel/code-frame@^7.22.13":
- version "7.22.13"
- resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e"
- integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==
+"@babel/code-frame@^7.12.11", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.23.5":
+ version "7.23.5"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244"
+ integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==
dependencies:
- "@babel/highlight" "^7.22.13"
+ "@babel/highlight" "^7.23.4"
chalk "^2.4.2"
-"@babel/compat-data@^7.22.20", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9":
- version "7.22.20"
- resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.20.tgz#8df6e96661209623f1975d66c35ffca66f3306d0"
- integrity sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==
+"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.3", "@babel/compat-data@^7.23.5":
+ version "7.23.5"
+ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98"
+ integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==
"@babel/core@^7.11.1":
- version "7.22.20"
- resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.20.tgz#e3d0eed84c049e2a2ae0a64d27b6a37edec385b7"
- integrity sha512-Y6jd1ahLubuYweD/zJH+vvOY141v4f9igNQAQ+MBgq9JlHS2iTsZKn1aMsb3vGccZsXI16VzTBw52Xx0DWmtnA==
+ version "7.23.7"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.7.tgz#4d8016e06a14b5f92530a13ed0561730b5c6483f"
+ integrity sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==
dependencies:
"@ampproject/remapping" "^2.2.0"
- "@babel/code-frame" "^7.22.13"
- "@babel/generator" "^7.22.15"
- "@babel/helper-compilation-targets" "^7.22.15"
- "@babel/helper-module-transforms" "^7.22.20"
- "@babel/helpers" "^7.22.15"
- "@babel/parser" "^7.22.16"
+ "@babel/code-frame" "^7.23.5"
+ "@babel/generator" "^7.23.6"
+ "@babel/helper-compilation-targets" "^7.23.6"
+ "@babel/helper-module-transforms" "^7.23.3"
+ "@babel/helpers" "^7.23.7"
+ "@babel/parser" "^7.23.6"
"@babel/template" "^7.22.15"
- "@babel/traverse" "^7.22.20"
- "@babel/types" "^7.22.19"
- convert-source-map "^1.7.0"
+ "@babel/traverse" "^7.23.7"
+ "@babel/types" "^7.23.6"
+ convert-source-map "^2.0.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
json5 "^2.2.3"
semver "^6.3.1"
-"@babel/generator@^7.22.15", "@babel/generator@^7.4.0":
- version "7.22.15"
- resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.15.tgz#1564189c7ec94cb8f77b5e8a90c4d200d21b2339"
- integrity sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==
+"@babel/generator@^7.23.6", "@babel/generator@^7.4.0":
+ version "7.23.6"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e"
+ integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==
dependencies:
- "@babel/types" "^7.22.15"
+ "@babel/types" "^7.23.6"
"@jridgewell/gen-mapping" "^0.3.2"
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
@@ -69,40 +69,40 @@
dependencies:
"@babel/types" "^7.22.5"
-"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.5":
+"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.15":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz#5426b109cf3ad47b91120f8328d8ab1be8b0b956"
integrity sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==
dependencies:
"@babel/types" "^7.22.15"
-"@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6":
- version "7.22.15"
- resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz#0698fc44551a26cf29f18d4662d5bf545a6cfc52"
- integrity sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==
+"@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.23.6":
+ version "7.23.6"
+ resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991"
+ integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==
dependencies:
- "@babel/compat-data" "^7.22.9"
- "@babel/helper-validator-option" "^7.22.15"
- browserslist "^4.21.9"
+ "@babel/compat-data" "^7.23.5"
+ "@babel/helper-validator-option" "^7.23.5"
+ browserslist "^4.22.2"
lru-cache "^5.1.1"
semver "^6.3.1"
-"@babel/helper-create-class-features-plugin@^7.22.11", "@babel/helper-create-class-features-plugin@^7.22.5":
- version "7.22.15"
- resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz#97a61b385e57fe458496fad19f8e63b63c867de4"
- integrity sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==
+"@babel/helper-create-class-features-plugin@^7.22.15":
+ version "7.23.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.7.tgz#b2e6826e0e20d337143655198b79d58fdc9bd43d"
+ integrity sha512-xCoqR/8+BoNnXOY7RVSgv6X+o7pmT5q1d+gGcRlXYkI+9B31glE4jeejhKVpA04O1AtzOt7OSQ6VYKP5FcRl9g==
dependencies:
"@babel/helper-annotate-as-pure" "^7.22.5"
- "@babel/helper-environment-visitor" "^7.22.5"
- "@babel/helper-function-name" "^7.22.5"
- "@babel/helper-member-expression-to-functions" "^7.22.15"
+ "@babel/helper-environment-visitor" "^7.22.20"
+ "@babel/helper-function-name" "^7.23.0"
+ "@babel/helper-member-expression-to-functions" "^7.23.0"
"@babel/helper-optimise-call-expression" "^7.22.5"
- "@babel/helper-replace-supers" "^7.22.9"
+ "@babel/helper-replace-supers" "^7.22.20"
"@babel/helper-skip-transparent-expression-wrappers" "^7.22.5"
"@babel/helper-split-export-declaration" "^7.22.6"
semver "^6.3.1"
-"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.5":
+"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.15", "@babel/helper-create-regexp-features-plugin@^7.22.5":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz#5ee90093914ea09639b01c711db0d6775e558be1"
integrity sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==
@@ -111,10 +111,10 @@
regexpu-core "^5.3.1"
semver "^6.3.1"
-"@babel/helper-define-polyfill-provider@^0.4.2":
- version "0.4.2"
- resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz#82c825cadeeeee7aad237618ebbe8fa1710015d7"
- integrity sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==
+"@babel/helper-define-polyfill-provider@^0.4.4":
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz#64df615451cb30e94b59a9696022cffac9a10088"
+ integrity sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==
dependencies:
"@babel/helper-compilation-targets" "^7.22.6"
"@babel/helper-plugin-utils" "^7.22.5"
@@ -122,18 +122,18 @@
lodash.debounce "^4.0.8"
resolve "^1.14.2"
-"@babel/helper-environment-visitor@^7.22.20", "@babel/helper-environment-visitor@^7.22.5":
+"@babel/helper-environment-visitor@^7.22.20":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167"
integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==
-"@babel/helper-function-name@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be"
- integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==
+"@babel/helper-function-name@^7.22.5", "@babel/helper-function-name@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759"
+ integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==
dependencies:
- "@babel/template" "^7.22.5"
- "@babel/types" "^7.22.5"
+ "@babel/template" "^7.22.15"
+ "@babel/types" "^7.23.0"
"@babel/helper-hoist-variables@^7.22.5":
version "7.22.5"
@@ -142,24 +142,24 @@
dependencies:
"@babel/types" "^7.22.5"
-"@babel/helper-member-expression-to-functions@^7.22.15":
- version "7.22.15"
- resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.15.tgz#b95a144896f6d491ca7863576f820f3628818621"
- integrity sha512-qLNsZbgrNh0fDQBCPocSL8guki1hcPvltGDv/NxvUoABwFq7GkKSu1nRXeJkVZc+wJvne2E0RKQz+2SQrz6eAA==
+"@babel/helper-member-expression-to-functions@^7.22.15", "@babel/helper-member-expression-to-functions@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz#9263e88cc5e41d39ec18c9a3e0eced59a3e7d366"
+ integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==
dependencies:
- "@babel/types" "^7.22.15"
+ "@babel/types" "^7.23.0"
-"@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.22.5":
+"@babel/helper-module-imports@^7.22.15":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0"
integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==
dependencies:
"@babel/types" "^7.22.15"
-"@babel/helper-module-transforms@^7.22.15", "@babel/helper-module-transforms@^7.22.20", "@babel/helper-module-transforms@^7.22.5", "@babel/helper-module-transforms@^7.22.9":
- version "7.22.20"
- resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.20.tgz#da9edc14794babbe7386df438f3768067132f59e"
- integrity sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==
+"@babel/helper-module-transforms@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1"
+ integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==
dependencies:
"@babel/helper-environment-visitor" "^7.22.20"
"@babel/helper-module-imports" "^7.22.15"
@@ -179,7 +179,7 @@
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295"
integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==
-"@babel/helper-remap-async-to-generator@^7.22.5", "@babel/helper-remap-async-to-generator@^7.22.9":
+"@babel/helper-remap-async-to-generator@^7.22.20":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz#7b68e1cb4fa964d2996fd063723fb48eca8498e0"
integrity sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==
@@ -188,7 +188,7 @@
"@babel/helper-environment-visitor" "^7.22.20"
"@babel/helper-wrap-function" "^7.22.20"
-"@babel/helper-replace-supers@^7.22.5", "@babel/helper-replace-supers@^7.22.9":
+"@babel/helper-replace-supers@^7.22.20":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz#e37d367123ca98fe455a9887734ed2e16eb7a793"
integrity sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==
@@ -218,20 +218,20 @@
dependencies:
"@babel/types" "^7.22.5"
-"@babel/helper-string-parser@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f"
- integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==
+"@babel/helper-string-parser@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83"
+ integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==
-"@babel/helper-validator-identifier@^7.22.19", "@babel/helper-validator-identifier@^7.22.20", "@babel/helper-validator-identifier@^7.22.5":
+"@babel/helper-validator-identifier@^7.22.20":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
-"@babel/helper-validator-option@^7.22.15":
- version "7.22.15"
- resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz#694c30dfa1d09a6534cdfcafbe56789d36aba040"
- integrity sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==
+"@babel/helper-validator-option@^7.23.5":
+ version "7.23.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307"
+ integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==
"@babel/helper-wrap-function@^7.22.20":
version "7.22.20"
@@ -242,44 +242,52 @@
"@babel/template" "^7.22.15"
"@babel/types" "^7.22.19"
-"@babel/helpers@^7.22.15":
- version "7.22.15"
- resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.15.tgz#f09c3df31e86e3ea0b7ff7556d85cdebd47ea6f1"
- integrity sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw==
+"@babel/helpers@^7.23.7":
+ version "7.23.7"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.7.tgz#eb543c36f81da2873e47b76ee032343ac83bba60"
+ integrity sha512-6AMnjCoC8wjqBzDHkuqpa7jAKwvMo4dC+lr/TFBz+ucfulO1XMpDnwWPGBNwClOKZ8h6xn5N81W/R5OrcKtCbQ==
dependencies:
"@babel/template" "^7.22.15"
- "@babel/traverse" "^7.22.15"
- "@babel/types" "^7.22.15"
+ "@babel/traverse" "^7.23.7"
+ "@babel/types" "^7.23.6"
-"@babel/highlight@^7.22.13":
- version "7.22.20"
- resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54"
- integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==
+"@babel/highlight@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b"
+ integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==
dependencies:
"@babel/helper-validator-identifier" "^7.22.20"
chalk "^2.4.2"
js-tokens "^4.0.0"
-"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.22.15", "@babel/parser@^7.22.16", "@babel/parser@^7.4.3":
- version "7.22.16"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.16.tgz#180aead7f247305cce6551bea2720934e2fa2c95"
- integrity sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==
+"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.22.15", "@babel/parser@^7.23.6", "@babel/parser@^7.4.3":
+ version "7.23.6"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.6.tgz#ba1c9e512bda72a47e285ae42aff9d2a635a9e3b"
+ integrity sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==
-"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.15":
- version "7.22.15"
- resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz#02dc8a03f613ed5fdc29fb2f728397c78146c962"
- integrity sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==
+"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz#5cd1c87ba9380d0afb78469292c954fee5d2411a"
+ integrity sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.15":
- version "7.22.15"
- resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz#2aeb91d337d4e1a1e7ce85b76a37f5301781200f"
- integrity sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==
+"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz#f6652bb16b94f8f9c20c50941e16e9756898dc5d"
+ integrity sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/helper-skip-transparent-expression-wrappers" "^7.22.5"
- "@babel/plugin-transform-optional-chaining" "^7.22.15"
+ "@babel/plugin-transform-optional-chaining" "^7.23.3"
+
+"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.23.7":
+ version "7.23.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz#516462a95d10a9618f197d39ad291a9b47ae1d7b"
+ integrity sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==
+ dependencies:
+ "@babel/helper-environment-visitor" "^7.22.20"
+ "@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-proposal-dynamic-import@^7.10.4":
version "7.18.6"
@@ -346,17 +354,17 @@
dependencies:
"@babel/helper-plugin-utils" "^7.8.3"
-"@babel/plugin-syntax-import-assertions@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz#07d252e2aa0bc6125567f742cd58619cb14dce98"
- integrity sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==
+"@babel/plugin-syntax-import-assertions@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz#9c05a7f592982aff1a2768260ad84bcd3f0c77fc"
+ integrity sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-syntax-import-attributes@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz#ab840248d834410b829f569f5262b9e517555ecb"
- integrity sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==
+"@babel/plugin-syntax-import-attributes@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz#992aee922cf04512461d7dae3ff6951b90a2dc06"
+ integrity sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
@@ -438,211 +446,212 @@
"@babel/helper-create-regexp-features-plugin" "^7.18.6"
"@babel/helper-plugin-utils" "^7.18.6"
-"@babel/plugin-transform-arrow-functions@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz#e5ba566d0c58a5b2ba2a8b795450641950b71958"
- integrity sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==
+"@babel/plugin-transform-arrow-functions@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz#94c6dcfd731af90f27a79509f9ab7fb2120fc38b"
+ integrity sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-async-generator-functions@^7.22.15":
- version "7.22.15"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz#3b153af4a6b779f340d5b80d3f634f55820aefa3"
- integrity sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==
+"@babel/plugin-transform-async-generator-functions@^7.23.7":
+ version "7.23.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.7.tgz#3aa0b4f2fa3788b5226ef9346cf6d16ec61f99cd"
+ integrity sha512-PdxEpL71bJp1byMG0va5gwQcXHxuEYC/BgI/e88mGTtohbZN28O5Yit0Plkkm/dBzCF/BxmbNcses1RH1T+urA==
dependencies:
- "@babel/helper-environment-visitor" "^7.22.5"
+ "@babel/helper-environment-visitor" "^7.22.20"
"@babel/helper-plugin-utils" "^7.22.5"
- "@babel/helper-remap-async-to-generator" "^7.22.9"
+ "@babel/helper-remap-async-to-generator" "^7.22.20"
"@babel/plugin-syntax-async-generators" "^7.8.4"
-"@babel/plugin-transform-async-to-generator@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz#c7a85f44e46f8952f6d27fe57c2ed3cc084c3775"
- integrity sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==
+"@babel/plugin-transform-async-to-generator@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz#d1f513c7a8a506d43f47df2bf25f9254b0b051fa"
+ integrity sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==
dependencies:
- "@babel/helper-module-imports" "^7.22.5"
+ "@babel/helper-module-imports" "^7.22.15"
"@babel/helper-plugin-utils" "^7.22.5"
- "@babel/helper-remap-async-to-generator" "^7.22.5"
+ "@babel/helper-remap-async-to-generator" "^7.22.20"
-"@babel/plugin-transform-block-scoped-functions@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz#27978075bfaeb9fa586d3cb63a3d30c1de580024"
- integrity sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==
+"@babel/plugin-transform-block-scoped-functions@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz#fe1177d715fb569663095e04f3598525d98e8c77"
+ integrity sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-block-scoping@^7.22.15":
- version "7.22.15"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.15.tgz#494eb82b87b5f8b1d8f6f28ea74078ec0a10a841"
- integrity sha512-G1czpdJBZCtngoK1sJgloLiOHUnkb/bLZwqVZD8kXmq0ZnVfTTWUcs9OWtp0mBtYJ+4LQY1fllqBkOIPhXmFmw==
+"@babel/plugin-transform-block-scoping@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz#b2d38589531c6c80fbe25e6b58e763622d2d3cf5"
+ integrity sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-class-properties@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz#97a56e31ad8c9dc06a0b3710ce7803d5a48cca77"
- integrity sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==
+"@babel/plugin-transform-class-properties@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz#35c377db11ca92a785a718b6aa4e3ed1eb65dc48"
+ integrity sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==
dependencies:
- "@babel/helper-create-class-features-plugin" "^7.22.5"
+ "@babel/helper-create-class-features-plugin" "^7.22.15"
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-class-static-block@^7.22.11":
- version "7.22.11"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz#dc8cc6e498f55692ac6b4b89e56d87cec766c974"
- integrity sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==
+"@babel/plugin-transform-class-static-block@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz#2a202c8787a8964dd11dfcedf994d36bfc844ab5"
+ integrity sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==
dependencies:
- "@babel/helper-create-class-features-plugin" "^7.22.11"
+ "@babel/helper-create-class-features-plugin" "^7.22.15"
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-class-static-block" "^7.14.5"
-"@babel/plugin-transform-classes@^7.22.15":
- version "7.22.15"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz#aaf4753aee262a232bbc95451b4bdf9599c65a0b"
- integrity sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==
+"@babel/plugin-transform-classes@^7.23.5":
+ version "7.23.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.5.tgz#e7a75f815e0c534cc4c9a39c56636c84fc0d64f2"
+ integrity sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg==
dependencies:
"@babel/helper-annotate-as-pure" "^7.22.5"
"@babel/helper-compilation-targets" "^7.22.15"
- "@babel/helper-environment-visitor" "^7.22.5"
- "@babel/helper-function-name" "^7.22.5"
+ "@babel/helper-environment-visitor" "^7.22.20"
+ "@babel/helper-function-name" "^7.23.0"
"@babel/helper-optimise-call-expression" "^7.22.5"
"@babel/helper-plugin-utils" "^7.22.5"
- "@babel/helper-replace-supers" "^7.22.9"
+ "@babel/helper-replace-supers" "^7.22.20"
"@babel/helper-split-export-declaration" "^7.22.6"
globals "^11.1.0"
-"@babel/plugin-transform-computed-properties@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz#cd1e994bf9f316bd1c2dafcd02063ec261bb3869"
- integrity sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==
+"@babel/plugin-transform-computed-properties@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz#652e69561fcc9d2b50ba4f7ac7f60dcf65e86474"
+ integrity sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
- "@babel/template" "^7.22.5"
+ "@babel/template" "^7.22.15"
-"@babel/plugin-transform-destructuring@^7.22.15":
- version "7.22.15"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.15.tgz#e7404ea5bb3387073b9754be654eecb578324694"
- integrity sha512-HzG8sFl1ZVGTme74Nw+X01XsUTqERVQ6/RLHo3XjGRzm7XD6QTtfS3NJotVgCGy8BzkDqRjRBD8dAyJn5TuvSQ==
+"@babel/plugin-transform-destructuring@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz#8c9ee68228b12ae3dff986e56ed1ba4f3c446311"
+ integrity sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-dotall-regex@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz#dbb4f0e45766eb544e193fb00e65a1dd3b2a4165"
- integrity sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==
+"@babel/plugin-transform-dotall-regex@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz#3f7af6054882ede89c378d0cf889b854a993da50"
+ integrity sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==
dependencies:
- "@babel/helper-create-regexp-features-plugin" "^7.22.5"
+ "@babel/helper-create-regexp-features-plugin" "^7.22.15"
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-duplicate-keys@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz#b6e6428d9416f5f0bba19c70d1e6e7e0b88ab285"
- integrity sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==
+"@babel/plugin-transform-duplicate-keys@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz#664706ca0a5dfe8d066537f99032fc1dc8b720ce"
+ integrity sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-dynamic-import@^7.22.11":
- version "7.22.11"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz#2c7722d2a5c01839eaf31518c6ff96d408e447aa"
- integrity sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==
+"@babel/plugin-transform-dynamic-import@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz#c7629e7254011ac3630d47d7f34ddd40ca535143"
+ integrity sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-dynamic-import" "^7.8.3"
-"@babel/plugin-transform-exponentiation-operator@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz#402432ad544a1f9a480da865fda26be653e48f6a"
- integrity sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==
+"@babel/plugin-transform-exponentiation-operator@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz#ea0d978f6b9232ba4722f3dbecdd18f450babd18"
+ integrity sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==
dependencies:
- "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.5"
+ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.15"
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-export-namespace-from@^7.22.11":
- version "7.22.11"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz#b3c84c8f19880b6c7440108f8929caf6056db26c"
- integrity sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==
+"@babel/plugin-transform-export-namespace-from@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz#084c7b25e9a5c8271e987a08cf85807b80283191"
+ integrity sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-export-namespace-from" "^7.8.3"
-"@babel/plugin-transform-for-of@^7.22.15":
- version "7.22.15"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz#f64b4ccc3a4f131a996388fae7680b472b306b29"
- integrity sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==
+"@babel/plugin-transform-for-of@^7.23.6":
+ version "7.23.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz#81c37e24171b37b370ba6aaffa7ac86bcb46f94e"
+ integrity sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
+ "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5"
-"@babel/plugin-transform-function-name@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz#935189af68b01898e0d6d99658db6b164205c143"
- integrity sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==
+"@babel/plugin-transform-function-name@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz#8f424fcd862bf84cb9a1a6b42bc2f47ed630f8dc"
+ integrity sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==
dependencies:
- "@babel/helper-compilation-targets" "^7.22.5"
- "@babel/helper-function-name" "^7.22.5"
+ "@babel/helper-compilation-targets" "^7.22.15"
+ "@babel/helper-function-name" "^7.23.0"
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-json-strings@^7.22.11":
- version "7.22.11"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz#689a34e1eed1928a40954e37f74509f48af67835"
- integrity sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==
+"@babel/plugin-transform-json-strings@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz#a871d9b6bd171976efad2e43e694c961ffa3714d"
+ integrity sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-json-strings" "^7.8.3"
-"@babel/plugin-transform-literals@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz#e9341f4b5a167952576e23db8d435849b1dd7920"
- integrity sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==
+"@babel/plugin-transform-literals@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz#8214665f00506ead73de157eba233e7381f3beb4"
+ integrity sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-logical-assignment-operators@^7.22.11":
- version "7.22.11"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz#24c522a61688bde045b7d9bc3c2597a4d948fc9c"
- integrity sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==
+"@babel/plugin-transform-logical-assignment-operators@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz#e599f82c51d55fac725f62ce55d3a0886279ecb5"
+ integrity sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
-"@babel/plugin-transform-member-expression-literals@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz#4fcc9050eded981a468347dd374539ed3e058def"
- integrity sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==
+"@babel/plugin-transform-member-expression-literals@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz#e37b3f0502289f477ac0e776b05a833d853cabcc"
+ integrity sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-modules-amd@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz#4e045f55dcf98afd00f85691a68fc0780704f526"
- integrity sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==
+"@babel/plugin-transform-modules-amd@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz#e19b55436a1416829df0a1afc495deedfae17f7d"
+ integrity sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==
dependencies:
- "@babel/helper-module-transforms" "^7.22.5"
+ "@babel/helper-module-transforms" "^7.23.3"
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-modules-commonjs@^7.22.15":
- version "7.22.15"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.15.tgz#b11810117ed4ee7691b29bd29fd9f3f98276034f"
- integrity sha512-jWL4eh90w0HQOTKP2MoXXUpVxilxsB2Vl4ji69rSjS3EcZ/v4sBmn+A3NpepuJzBhOaEBbR7udonlHHn5DWidg==
+"@babel/plugin-transform-modules-commonjs@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz#661ae831b9577e52be57dd8356b734f9700b53b4"
+ integrity sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==
dependencies:
- "@babel/helper-module-transforms" "^7.22.15"
+ "@babel/helper-module-transforms" "^7.23.3"
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/helper-simple-access" "^7.22.5"
-"@babel/plugin-transform-modules-systemjs@^7.22.11":
- version "7.22.11"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz#3386be5875d316493b517207e8f1931d93154bb1"
- integrity sha512-rIqHmHoMEOhI3VkVf5jQ15l539KrwhzqcBO6wdCNWPWc/JWt9ILNYNUssbRpeq0qWns8svuw8LnMNCvWBIJ8wA==
+"@babel/plugin-transform-modules-systemjs@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz#fa7e62248931cb15b9404f8052581c302dd9de81"
+ integrity sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==
dependencies:
"@babel/helper-hoist-variables" "^7.22.5"
- "@babel/helper-module-transforms" "^7.22.9"
+ "@babel/helper-module-transforms" "^7.23.3"
"@babel/helper-plugin-utils" "^7.22.5"
- "@babel/helper-validator-identifier" "^7.22.5"
+ "@babel/helper-validator-identifier" "^7.22.20"
-"@babel/plugin-transform-modules-umd@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz#4694ae40a87b1745e3775b6a7fe96400315d4f98"
- integrity sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==
+"@babel/plugin-transform-modules-umd@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz#5d4395fccd071dfefe6585a4411aa7d6b7d769e9"
+ integrity sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==
dependencies:
- "@babel/helper-module-transforms" "^7.22.5"
+ "@babel/helper-module-transforms" "^7.23.3"
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-transform-named-capturing-groups-regex@^7.22.5":
@@ -653,198 +662,199 @@
"@babel/helper-create-regexp-features-plugin" "^7.22.5"
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-new-target@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz#1b248acea54ce44ea06dfd37247ba089fcf9758d"
- integrity sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==
+"@babel/plugin-transform-new-target@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz#5491bb78ed6ac87e990957cea367eab781c4d980"
+ integrity sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-nullish-coalescing-operator@^7.22.11":
- version "7.22.11"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz#debef6c8ba795f5ac67cd861a81b744c5d38d9fc"
- integrity sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==
+"@babel/plugin-transform-nullish-coalescing-operator@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz#45556aad123fc6e52189ea749e33ce090637346e"
+ integrity sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
-"@babel/plugin-transform-numeric-separator@^7.22.11":
- version "7.22.11"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz#498d77dc45a6c6db74bb829c02a01c1d719cbfbd"
- integrity sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==
+"@babel/plugin-transform-numeric-separator@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz#03d08e3691e405804ecdd19dd278a40cca531f29"
+ integrity sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-numeric-separator" "^7.10.4"
-"@babel/plugin-transform-object-rest-spread@^7.22.15":
- version "7.22.15"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz#21a95db166be59b91cde48775310c0df6e1da56f"
- integrity sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==
+"@babel/plugin-transform-object-rest-spread@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz#2b9c2d26bf62710460bdc0d1730d4f1048361b83"
+ integrity sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==
dependencies:
- "@babel/compat-data" "^7.22.9"
+ "@babel/compat-data" "^7.23.3"
"@babel/helper-compilation-targets" "^7.22.15"
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-object-rest-spread" "^7.8.3"
- "@babel/plugin-transform-parameters" "^7.22.15"
+ "@babel/plugin-transform-parameters" "^7.23.3"
-"@babel/plugin-transform-object-super@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz#794a8d2fcb5d0835af722173c1a9d704f44e218c"
- integrity sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==
+"@babel/plugin-transform-object-super@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz#81fdb636dcb306dd2e4e8fd80db5b2362ed2ebcd"
+ integrity sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
- "@babel/helper-replace-supers" "^7.22.5"
+ "@babel/helper-replace-supers" "^7.22.20"
-"@babel/plugin-transform-optional-catch-binding@^7.22.11":
- version "7.22.11"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz#461cc4f578a127bb055527b3e77404cad38c08e0"
- integrity sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==
+"@babel/plugin-transform-optional-catch-binding@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz#318066de6dacce7d92fa244ae475aa8d91778017"
+ integrity sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
-"@babel/plugin-transform-optional-chaining@^7.22.15":
- version "7.22.15"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.15.tgz#d7a5996c2f7ca4ad2ad16dbb74444e5c4385b1ba"
- integrity sha512-ngQ2tBhq5vvSJw2Q2Z9i7ealNkpDMU0rGWnHPKqRZO0tzZ5tlaoz4hDvhXioOoaE0X2vfNss1djwg0DXlfu30A==
+"@babel/plugin-transform-optional-chaining@^7.23.3", "@babel/plugin-transform-optional-chaining@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz#6acf61203bdfc4de9d4e52e64490aeb3e52bd017"
+ integrity sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/helper-skip-transparent-expression-wrappers" "^7.22.5"
"@babel/plugin-syntax-optional-chaining" "^7.8.3"
-"@babel/plugin-transform-parameters@^7.22.15":
- version "7.22.15"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz#719ca82a01d177af358df64a514d64c2e3edb114"
- integrity sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==
+"@babel/plugin-transform-parameters@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz#83ef5d1baf4b1072fa6e54b2b0999a7b2527e2af"
+ integrity sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-private-methods@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz#21c8af791f76674420a147ae62e9935d790f8722"
- integrity sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==
+"@babel/plugin-transform-private-methods@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz#b2d7a3c97e278bfe59137a978d53b2c2e038c0e4"
+ integrity sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==
dependencies:
- "@babel/helper-create-class-features-plugin" "^7.22.5"
+ "@babel/helper-create-class-features-plugin" "^7.22.15"
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-private-property-in-object@^7.22.11":
- version "7.22.11"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz#ad45c4fc440e9cb84c718ed0906d96cf40f9a4e1"
- integrity sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==
+"@babel/plugin-transform-private-property-in-object@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz#3ec711d05d6608fd173d9b8de39872d8dbf68bf5"
+ integrity sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==
dependencies:
"@babel/helper-annotate-as-pure" "^7.22.5"
- "@babel/helper-create-class-features-plugin" "^7.22.11"
+ "@babel/helper-create-class-features-plugin" "^7.22.15"
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/plugin-syntax-private-property-in-object" "^7.14.5"
-"@babel/plugin-transform-property-literals@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz#b5ddabd73a4f7f26cd0e20f5db48290b88732766"
- integrity sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==
+"@babel/plugin-transform-property-literals@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz#54518f14ac4755d22b92162e4a852d308a560875"
+ integrity sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-regenerator@^7.22.10":
- version "7.22.10"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz#8ceef3bd7375c4db7652878b0241b2be5d0c3cca"
- integrity sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==
+"@babel/plugin-transform-regenerator@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz#141afd4a2057298602069fce7f2dc5173e6c561c"
+ integrity sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
regenerator-transform "^0.15.2"
-"@babel/plugin-transform-reserved-words@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz#832cd35b81c287c4bcd09ce03e22199641f964fb"
- integrity sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==
+"@babel/plugin-transform-reserved-words@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz#4130dcee12bd3dd5705c587947eb715da12efac8"
+ integrity sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-shorthand-properties@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz#6e277654be82b5559fc4b9f58088507c24f0c624"
- integrity sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==
+"@babel/plugin-transform-shorthand-properties@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz#97d82a39b0e0c24f8a981568a8ed851745f59210"
+ integrity sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-spread@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz#6487fd29f229c95e284ba6c98d65eafb893fea6b"
- integrity sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==
+"@babel/plugin-transform-spread@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz#41d17aacb12bde55168403c6f2d6bdca563d362c"
+ integrity sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/helper-skip-transparent-expression-wrappers" "^7.22.5"
-"@babel/plugin-transform-sticky-regex@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz#295aba1595bfc8197abd02eae5fc288c0deb26aa"
- integrity sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==
+"@babel/plugin-transform-sticky-regex@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz#dec45588ab4a723cb579c609b294a3d1bd22ff04"
+ integrity sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-template-literals@^7.22.5", "@babel/plugin-transform-template-literals@^7.8.3":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz#8f38cf291e5f7a8e60e9f733193f0bcc10909bff"
- integrity sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==
+"@babel/plugin-transform-template-literals@^7.23.3", "@babel/plugin-transform-template-literals@^7.8.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz#5f0f028eb14e50b5d0f76be57f90045757539d07"
+ integrity sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-typeof-symbol@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz#5e2ba478da4b603af8673ff7c54f75a97b716b34"
- integrity sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==
+"@babel/plugin-transform-typeof-symbol@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz#9dfab97acc87495c0c449014eb9c547d8966bca4"
+ integrity sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-unicode-escapes@^7.22.10":
- version "7.22.10"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz#c723f380f40a2b2f57a62df24c9005834c8616d9"
- integrity sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==
+"@babel/plugin-transform-unicode-escapes@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz#1f66d16cab01fab98d784867d24f70c1ca65b925"
+ integrity sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-unicode-property-regex@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz#098898f74d5c1e86660dc112057b2d11227f1c81"
- integrity sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==
+"@babel/plugin-transform-unicode-property-regex@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz#19e234129e5ffa7205010feec0d94c251083d7ad"
+ integrity sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==
dependencies:
- "@babel/helper-create-regexp-features-plugin" "^7.22.5"
+ "@babel/helper-create-regexp-features-plugin" "^7.22.15"
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-unicode-regex@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz#ce7e7bb3ef208c4ff67e02a22816656256d7a183"
- integrity sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==
+"@babel/plugin-transform-unicode-regex@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz#26897708d8f42654ca4ce1b73e96140fbad879dc"
+ integrity sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==
dependencies:
- "@babel/helper-create-regexp-features-plugin" "^7.22.5"
+ "@babel/helper-create-regexp-features-plugin" "^7.22.15"
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-unicode-sets-regex@^7.22.5":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz#77788060e511b708ffc7d42fdfbc5b37c3004e91"
- integrity sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==
+"@babel/plugin-transform-unicode-sets-regex@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz#4fb6f0a719c2c5859d11f6b55a050cc987f3799e"
+ integrity sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==
dependencies:
- "@babel/helper-create-regexp-features-plugin" "^7.22.5"
+ "@babel/helper-create-regexp-features-plugin" "^7.22.15"
"@babel/helper-plugin-utils" "^7.22.5"
"@babel/preset-env@^7.9.0":
- version "7.22.20"
- resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.20.tgz#de9e9b57e1127ce0a2f580831717f7fb677ceedb"
- integrity sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg==
+ version "7.23.7"
+ resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.23.7.tgz#e5d69b9f14db8a13bae4d8e5ce7f360973626241"
+ integrity sha512-SY27X/GtTz/L4UryMNJ6p4fH4nsgWbz84y9FE0bQeWJP6O5BhgVCt53CotQKHCOeXJel8VyhlhujhlltKms/CA==
dependencies:
- "@babel/compat-data" "^7.22.20"
- "@babel/helper-compilation-targets" "^7.22.15"
+ "@babel/compat-data" "^7.23.5"
+ "@babel/helper-compilation-targets" "^7.23.6"
"@babel/helper-plugin-utils" "^7.22.5"
- "@babel/helper-validator-option" "^7.22.15"
- "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.22.15"
- "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.22.15"
+ "@babel/helper-validator-option" "^7.23.5"
+ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.23.3"
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.23.3"
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.23.7"
"@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2"
"@babel/plugin-syntax-async-generators" "^7.8.4"
"@babel/plugin-syntax-class-properties" "^7.12.13"
"@babel/plugin-syntax-class-static-block" "^7.14.5"
"@babel/plugin-syntax-dynamic-import" "^7.8.3"
"@babel/plugin-syntax-export-namespace-from" "^7.8.3"
- "@babel/plugin-syntax-import-assertions" "^7.22.5"
- "@babel/plugin-syntax-import-attributes" "^7.22.5"
+ "@babel/plugin-syntax-import-assertions" "^7.23.3"
+ "@babel/plugin-syntax-import-attributes" "^7.23.3"
"@babel/plugin-syntax-import-meta" "^7.10.4"
"@babel/plugin-syntax-json-strings" "^7.8.3"
"@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
@@ -856,59 +866,58 @@
"@babel/plugin-syntax-private-property-in-object" "^7.14.5"
"@babel/plugin-syntax-top-level-await" "^7.14.5"
"@babel/plugin-syntax-unicode-sets-regex" "^7.18.6"
- "@babel/plugin-transform-arrow-functions" "^7.22.5"
- "@babel/plugin-transform-async-generator-functions" "^7.22.15"
- "@babel/plugin-transform-async-to-generator" "^7.22.5"
- "@babel/plugin-transform-block-scoped-functions" "^7.22.5"
- "@babel/plugin-transform-block-scoping" "^7.22.15"
- "@babel/plugin-transform-class-properties" "^7.22.5"
- "@babel/plugin-transform-class-static-block" "^7.22.11"
- "@babel/plugin-transform-classes" "^7.22.15"
- "@babel/plugin-transform-computed-properties" "^7.22.5"
- "@babel/plugin-transform-destructuring" "^7.22.15"
- "@babel/plugin-transform-dotall-regex" "^7.22.5"
- "@babel/plugin-transform-duplicate-keys" "^7.22.5"
- "@babel/plugin-transform-dynamic-import" "^7.22.11"
- "@babel/plugin-transform-exponentiation-operator" "^7.22.5"
- "@babel/plugin-transform-export-namespace-from" "^7.22.11"
- "@babel/plugin-transform-for-of" "^7.22.15"
- "@babel/plugin-transform-function-name" "^7.22.5"
- "@babel/plugin-transform-json-strings" "^7.22.11"
- "@babel/plugin-transform-literals" "^7.22.5"
- "@babel/plugin-transform-logical-assignment-operators" "^7.22.11"
- "@babel/plugin-transform-member-expression-literals" "^7.22.5"
- "@babel/plugin-transform-modules-amd" "^7.22.5"
- "@babel/plugin-transform-modules-commonjs" "^7.22.15"
- "@babel/plugin-transform-modules-systemjs" "^7.22.11"
- "@babel/plugin-transform-modules-umd" "^7.22.5"
+ "@babel/plugin-transform-arrow-functions" "^7.23.3"
+ "@babel/plugin-transform-async-generator-functions" "^7.23.7"
+ "@babel/plugin-transform-async-to-generator" "^7.23.3"
+ "@babel/plugin-transform-block-scoped-functions" "^7.23.3"
+ "@babel/plugin-transform-block-scoping" "^7.23.4"
+ "@babel/plugin-transform-class-properties" "^7.23.3"
+ "@babel/plugin-transform-class-static-block" "^7.23.4"
+ "@babel/plugin-transform-classes" "^7.23.5"
+ "@babel/plugin-transform-computed-properties" "^7.23.3"
+ "@babel/plugin-transform-destructuring" "^7.23.3"
+ "@babel/plugin-transform-dotall-regex" "^7.23.3"
+ "@babel/plugin-transform-duplicate-keys" "^7.23.3"
+ "@babel/plugin-transform-dynamic-import" "^7.23.4"
+ "@babel/plugin-transform-exponentiation-operator" "^7.23.3"
+ "@babel/plugin-transform-export-namespace-from" "^7.23.4"
+ "@babel/plugin-transform-for-of" "^7.23.6"
+ "@babel/plugin-transform-function-name" "^7.23.3"
+ "@babel/plugin-transform-json-strings" "^7.23.4"
+ "@babel/plugin-transform-literals" "^7.23.3"
+ "@babel/plugin-transform-logical-assignment-operators" "^7.23.4"
+ "@babel/plugin-transform-member-expression-literals" "^7.23.3"
+ "@babel/plugin-transform-modules-amd" "^7.23.3"
+ "@babel/plugin-transform-modules-commonjs" "^7.23.3"
+ "@babel/plugin-transform-modules-systemjs" "^7.23.3"
+ "@babel/plugin-transform-modules-umd" "^7.23.3"
"@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5"
- "@babel/plugin-transform-new-target" "^7.22.5"
- "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.11"
- "@babel/plugin-transform-numeric-separator" "^7.22.11"
- "@babel/plugin-transform-object-rest-spread" "^7.22.15"
- "@babel/plugin-transform-object-super" "^7.22.5"
- "@babel/plugin-transform-optional-catch-binding" "^7.22.11"
- "@babel/plugin-transform-optional-chaining" "^7.22.15"
- "@babel/plugin-transform-parameters" "^7.22.15"
- "@babel/plugin-transform-private-methods" "^7.22.5"
- "@babel/plugin-transform-private-property-in-object" "^7.22.11"
- "@babel/plugin-transform-property-literals" "^7.22.5"
- "@babel/plugin-transform-regenerator" "^7.22.10"
- "@babel/plugin-transform-reserved-words" "^7.22.5"
- "@babel/plugin-transform-shorthand-properties" "^7.22.5"
- "@babel/plugin-transform-spread" "^7.22.5"
- "@babel/plugin-transform-sticky-regex" "^7.22.5"
- "@babel/plugin-transform-template-literals" "^7.22.5"
- "@babel/plugin-transform-typeof-symbol" "^7.22.5"
- "@babel/plugin-transform-unicode-escapes" "^7.22.10"
- "@babel/plugin-transform-unicode-property-regex" "^7.22.5"
- "@babel/plugin-transform-unicode-regex" "^7.22.5"
- "@babel/plugin-transform-unicode-sets-regex" "^7.22.5"
+ "@babel/plugin-transform-new-target" "^7.23.3"
+ "@babel/plugin-transform-nullish-coalescing-operator" "^7.23.4"
+ "@babel/plugin-transform-numeric-separator" "^7.23.4"
+ "@babel/plugin-transform-object-rest-spread" "^7.23.4"
+ "@babel/plugin-transform-object-super" "^7.23.3"
+ "@babel/plugin-transform-optional-catch-binding" "^7.23.4"
+ "@babel/plugin-transform-optional-chaining" "^7.23.4"
+ "@babel/plugin-transform-parameters" "^7.23.3"
+ "@babel/plugin-transform-private-methods" "^7.23.3"
+ "@babel/plugin-transform-private-property-in-object" "^7.23.4"
+ "@babel/plugin-transform-property-literals" "^7.23.3"
+ "@babel/plugin-transform-regenerator" "^7.23.3"
+ "@babel/plugin-transform-reserved-words" "^7.23.3"
+ "@babel/plugin-transform-shorthand-properties" "^7.23.3"
+ "@babel/plugin-transform-spread" "^7.23.3"
+ "@babel/plugin-transform-sticky-regex" "^7.23.3"
+ "@babel/plugin-transform-template-literals" "^7.23.3"
+ "@babel/plugin-transform-typeof-symbol" "^7.23.3"
+ "@babel/plugin-transform-unicode-escapes" "^7.23.3"
+ "@babel/plugin-transform-unicode-property-regex" "^7.23.3"
+ "@babel/plugin-transform-unicode-regex" "^7.23.3"
+ "@babel/plugin-transform-unicode-sets-regex" "^7.23.3"
"@babel/preset-modules" "0.1.6-no-external-plugins"
- "@babel/types" "^7.22.19"
- babel-plugin-polyfill-corejs2 "^0.4.5"
- babel-plugin-polyfill-corejs3 "^0.8.3"
- babel-plugin-polyfill-regenerator "^0.5.2"
+ babel-plugin-polyfill-corejs2 "^0.4.7"
+ babel-plugin-polyfill-corejs3 "^0.8.7"
+ babel-plugin-polyfill-regenerator "^0.5.4"
core-js-compat "^3.31.0"
semver "^6.3.1"
@@ -927,13 +936,13 @@
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
"@babel/runtime@^7.8.4":
- version "7.22.15"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.15.tgz#38f46494ccf6cf020bd4eed7124b425e83e523b8"
- integrity sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==
+ version "7.23.7"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.7.tgz#dd7c88deeb218a0f8bd34d5db1aa242e0f203193"
+ integrity sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==
dependencies:
regenerator-runtime "^0.14.0"
-"@babel/template@^7.22.15", "@babel/template@^7.22.5", "@babel/template@^7.4.0":
+"@babel/template@^7.22.15", "@babel/template@^7.4.0":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==
@@ -942,29 +951,29 @@
"@babel/parser" "^7.22.15"
"@babel/types" "^7.22.15"
-"@babel/traverse@^7.22.15", "@babel/traverse@^7.22.20", "@babel/traverse@^7.4.3":
- version "7.22.20"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.20.tgz#db572d9cb5c79e02d83e5618b82f6991c07584c9"
- integrity sha512-eU260mPZbU7mZ0N+X10pxXhQFMGTeLb9eFS0mxehS8HZp9o1uSnFeWQuG1UPrlxgA7QoUzFhOnilHDp0AXCyHw==
+"@babel/traverse@^7.23.7", "@babel/traverse@^7.4.3":
+ version "7.23.7"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.7.tgz#9a7bf285c928cb99b5ead19c3b1ce5b310c9c305"
+ integrity sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==
dependencies:
- "@babel/code-frame" "^7.22.13"
- "@babel/generator" "^7.22.15"
+ "@babel/code-frame" "^7.23.5"
+ "@babel/generator" "^7.23.6"
"@babel/helper-environment-visitor" "^7.22.20"
- "@babel/helper-function-name" "^7.22.5"
+ "@babel/helper-function-name" "^7.23.0"
"@babel/helper-hoist-variables" "^7.22.5"
"@babel/helper-split-export-declaration" "^7.22.6"
- "@babel/parser" "^7.22.16"
- "@babel/types" "^7.22.19"
- debug "^4.1.0"
+ "@babel/parser" "^7.23.6"
+ "@babel/types" "^7.23.6"
+ debug "^4.3.1"
globals "^11.1.0"
-"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.4.0", "@babel/types@^7.4.4":
- version "7.22.19"
- resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.19.tgz#7425343253556916e440e662bb221a93ddb75684"
- integrity sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==
+"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.4.0", "@babel/types@^7.4.4":
+ version "7.23.6"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.6.tgz#be33fdb151e1f5a56877d704492c240fc71c7ccd"
+ integrity sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==
dependencies:
- "@babel/helper-string-parser" "^7.22.5"
- "@babel/helper-validator-identifier" "^7.22.19"
+ "@babel/helper-string-parser" "^7.23.4"
+ "@babel/helper-validator-identifier" "^7.22.20"
to-fast-properties "^2.0.0"
"@colors/colors@1.5.0":
@@ -1114,9 +1123,9 @@
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9":
- version "0.3.19"
- resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811"
- integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==
+ version "0.3.20"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f"
+ integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==
dependencies:
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
@@ -1128,17 +1137,17 @@
dependencies:
vary "^1.1.2"
-"@lit-labs/ssr-dom-shim@^1.0.0", "@lit-labs/ssr-dom-shim@^1.1.0":
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz#64df34e2f12e68e78ac57e571d25ec07fa460ca9"
- integrity sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ==
+"@lit-labs/ssr-dom-shim@^1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz#d693d972974a354034454ec1317eb6afd0b00312"
+ integrity sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g==
-"@lit/reactive-element@^1.0.0", "@lit/reactive-element@^1.3.0", "@lit/reactive-element@^1.6.0":
- version "1.6.3"
- resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.6.3.tgz#25b4eece2592132845d303e091bad9b04cdcfe03"
- integrity sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==
+"@lit/reactive-element@^1.0.0 || ^2.0.0", "@lit/reactive-element@^2.0.0":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.0.2.tgz#779ae9d265407daaf7737cb892df5ec2a86e22a0"
+ integrity sha512-SVOwLAWUQg3Ji1egtOt1UiFe4zdDpnWHyc5qctSceJ5XIu0Uc76YmGpIjZgx9YJ0XtdW0Jm507sDvjOu+HnB8w==
dependencies:
- "@lit-labs/ssr-dom-shim" "^1.0.0"
+ "@lit-labs/ssr-dom-shim" "^1.1.2"
"@mdn/browser-compat-data@^4.0.0":
version "4.2.1"
@@ -1198,14 +1207,6 @@
whatwg-fetch "^3.5.0"
whatwg-url "^7.1.0"
-"@open-wc/chai-dom-equals@^0.12.36":
- version "0.12.36"
- resolved "https://registry.yarnpkg.com/@open-wc/chai-dom-equals/-/chai-dom-equals-0.12.36.tgz#ed0eb56b9e98c4d7f7280facce6215654aae9f4c"
- integrity sha512-Gt1fa37h4rtWPQGETSU4n1L678NmMi9KwHM1sH+JCGcz45rs8DBPx7MUVeGZ+HxRlbEI5t9LU2RGGv6xT2OlyA==
- dependencies:
- "@open-wc/semantic-dom-diff" "^0.13.16"
- "@types/chai" "^4.1.7"
-
"@open-wc/dedupe-mixin@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@open-wc/dedupe-mixin/-/dedupe-mixin-1.4.0.tgz#b3c58f8699b197bb5e923d624c720e67c9f324d6"
@@ -1227,19 +1228,14 @@
portfinder "^1.0.21"
request "^2.88.0"
-"@open-wc/scoped-elements@^2.2.0":
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/@open-wc/scoped-elements/-/scoped-elements-2.2.0.tgz#4d65d7ba796c2bb76ef7934068532ca1795ea7b6"
- integrity sha512-Qe+vWsuVHFzUkdChwlmJGuQf9cA3I+QOsSHULV/6qf6wsqLM2/32svNRH+rbBIMwiPEwzZprZlkvkqQRucYnVA==
+"@open-wc/scoped-elements@^2.2.4":
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/@open-wc/scoped-elements/-/scoped-elements-2.2.4.tgz#081559b62d885ac0ec043546f17f1f680294d500"
+ integrity sha512-12X4F4QGPWcvPbxAiJ4v8wQFCOu+laZHRGfTrkoj+3JzACCtuxHG49YbuqVzQ135QPKCuhP9wA0kpGGEfUegyg==
dependencies:
- "@lit/reactive-element" "^1.0.0"
+ "@lit/reactive-element" "^1.0.0 || ^2.0.0"
"@open-wc/dedupe-mixin" "^1.4.0"
-"@open-wc/semantic-dom-diff@^0.13.16":
- version "0.13.21"
- resolved "https://registry.yarnpkg.com/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.13.21.tgz#718b9ec5f9a98935fc775e577ad094ae8d8b7dea"
- integrity sha512-BONpjHcGX2zFa9mfnwBCLEmlDsOHzT+j6Qt1yfK3MzFXFtAykfzFjAgaxPetu0YbBlCfXuMlfxI4vlRGCGMvFg==
-
"@open-wc/semantic-dom-diff@^0.19.9":
version "0.19.9"
resolved "https://registry.yarnpkg.com/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.19.9.tgz#fd27659cbace40c6a59078233f4fa14a308a45b1"
@@ -1249,32 +1245,30 @@
"@web/test-runner-commands" "^0.6.5"
"@open-wc/semantic-dom-diff@^0.20.0":
- version "0.20.0"
- resolved "https://registry.yarnpkg.com/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.20.0.tgz#3766aa88f67df624db0494adf82c8035216a2493"
- integrity sha512-qGHl3nkXluXsjpLY9bSZka/cnlrybPtJMs6RjmV/OP4ID7Gcz1uNWQks05pAhptDB1R47G6PQjdwxG8dXl1zGA==
+ version "0.20.1"
+ resolved "https://registry.yarnpkg.com/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.20.1.tgz#b1bb78be455bd99fb034d9baadbb959d7d124030"
+ integrity sha512-mPF/RPT2TU7Dw41LEDdaeP6eyTOWBD4z0+AHP4/d0SbgcfJZVRymlIB6DQmtz0fd2CImIS9kszaMmwMt92HBPA==
dependencies:
"@types/chai" "^4.3.1"
- "@web/test-runner-commands" "^0.7.0"
+ "@web/test-runner-commands" "^0.9.0"
-"@open-wc/testing-helpers@^2.3.0":
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/@open-wc/testing-helpers/-/testing-helpers-2.3.0.tgz#6ee88baaf316a6217c43e7ba536cb187d15cb6f4"
- integrity sha512-wkDipkia/OMWq5Z1KkAgvqNLfIOCiPGrrtfoCKuQje8u7F0Bz9Un44EwBtWcCdYtLc40quWP7XFpFsW8poIfUA==
+"@open-wc/testing-helpers@^2.3.1":
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/@open-wc/testing-helpers/-/testing-helpers-2.3.2.tgz#c2bfa82cedd833608effa2d2367fe9524ddf4434"
+ integrity sha512-uZMGC/C1m5EiwQsff6KMmCW25TYMQlJt4ilAWIjnelWGFg9HPUiLnlFvAas3ESUP+4OXLO8Oft7p4mHvbYvAEQ==
dependencies:
- "@open-wc/scoped-elements" "^2.2.0"
- lit "^2.0.0"
- lit-html "^2.0.0"
+ "@open-wc/scoped-elements" "^2.2.4"
+ lit "^2.0.0 || ^3.0.0"
+ lit-html "^2.0.0 || ^3.0.0"
"@open-wc/testing@^3.2.0":
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/@open-wc/testing/-/testing-3.2.0.tgz#884ca348861a116829ce5657fccff11a1a9a07bd"
- integrity sha512-9geTbFq8InbcfniPtS8KCfb5sbQ9WE6QMo1Tli8XMnfllnkZok7Az4kTRAskGQeMeQN/I2I//jE5xY/60qhrHg==
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/@open-wc/testing/-/testing-3.2.2.tgz#c952f4b20af0d201cc8cc436c2c3cdd338bf8177"
+ integrity sha512-byN4dJTd6ZyI9mWmI4lVj30uiu+rYvQr93g64Pd7UFBdAUgb02DHLj6fkJ1gjxA6LC/MeFd7K7mOZ4+vKrMptw==
dependencies:
"@esm-bundle/chai" "^4.3.4-fix.0"
- "@open-wc/chai-dom-equals" "^0.12.36"
"@open-wc/semantic-dom-diff" "^0.20.0"
- "@open-wc/testing-helpers" "^2.3.0"
- "@types/chai" "^4.2.11"
+ "@open-wc/testing-helpers" "^2.3.1"
"@types/chai-dom" "^1.11.0"
"@types/sinon-chai" "^3.2.3"
chai-a11y-axe "^1.5.0"
@@ -1366,21 +1360,21 @@
integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
"@types/accepts@*":
- version "1.3.5"
- resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575"
- integrity sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==
+ version "1.3.7"
+ resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.7.tgz#3b98b1889d2b2386604c2bbbe62e4fb51e95b265"
+ integrity sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==
dependencies:
"@types/node" "*"
"@types/babel__code-frame@^7.0.2":
- version "7.0.4"
- resolved "https://registry.yarnpkg.com/@types/babel__code-frame/-/babel__code-frame-7.0.4.tgz#0d14543f70ca91f4d2b0513a60f1eb31432c42e1"
- integrity sha512-WBxINLlATjvmpCgBbb9tOPrKtcPfu4A/Yz2iRzmdaodfvjAS/Z0WZJClV9/EXvoC9viI3lgUs7B9Uo7G/RmMGg==
+ version "7.0.6"
+ resolved "https://registry.yarnpkg.com/@types/babel__code-frame/-/babel__code-frame-7.0.6.tgz#20a899c0d29fba1ddf5c2156a10a2bda75ee6f29"
+ integrity sha512-Anitqkl3+KrzcW2k77lRlg/GfLZLWXBuNgbEcIOU6M92yw42vsd3xV/Z/yAHEj8m+KUjL6bWOVOFqX8PFPJ4LA==
"@types/babel__core@^7.1.3":
- version "7.20.2"
- resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.2.tgz#215db4f4a35d710256579784a548907237728756"
- integrity sha512-pNpr1T1xLUc2l3xJKuPtsEky3ybxN3m4fJkknfIpTCTfIZCDW57oAg+EfCgIIp2rvCe0Wn++/FfodDS4YXxBwA==
+ version "7.20.5"
+ resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017"
+ integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==
dependencies:
"@babel/parser" "^7.20.7"
"@babel/types" "^7.20.7"
@@ -1389,39 +1383,39 @@
"@types/babel__traverse" "*"
"@types/babel__generator@*":
- version "7.6.5"
- resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.5.tgz#281f4764bcbbbc51fdded0f25aa587b4ce14da95"
- integrity sha512-h9yIuWbJKdOPLJTbmSpPzkF67e659PbQDba7ifWm5BJ8xTv+sDmS7rFmywkWOvXedGTivCdeGSIIX8WLcRTz8w==
+ version "7.6.8"
+ resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.8.tgz#f836c61f48b1346e7d2b0d93c6dacc5b9535d3ab"
+ integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==
dependencies:
"@babel/types" "^7.0.0"
"@types/babel__template@*":
- version "7.4.2"
- resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.2.tgz#843e9f1f47c957553b0c374481dc4772921d6a6b"
- integrity sha512-/AVzPICMhMOMYoSx9MoKpGDKdBRsIXMNByh1PXSZoa+v6ZoLa8xxtsT/uLQ/NJm0XVAWl/BvId4MlDeXJaeIZQ==
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f"
+ integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==
dependencies:
"@babel/parser" "^7.1.0"
"@babel/types" "^7.0.0"
"@types/babel__traverse@*":
- version "7.20.2"
- resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.2.tgz#4ddf99d95cfdd946ff35d2b65c978d9c9bf2645d"
- integrity sha512-ojlGK1Hsfce93J0+kn3H5R73elidKUaZonirN33GSmgTUMpzI/MIFfSpF3haANe3G1bEBS9/9/QEqwTzwqFsKw==
+ version "7.20.5"
+ resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.5.tgz#7b7502be0aa80cc4ef22978846b983edaafcd4dd"
+ integrity sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==
dependencies:
"@babel/types" "^7.20.7"
"@types/body-parser@*":
- version "1.19.3"
- resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.3.tgz#fb558014374f7d9e56c8f34bab2042a3a07d25cd"
- integrity sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==
+ version "1.19.5"
+ resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4"
+ integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==
dependencies:
"@types/connect" "*"
"@types/node" "*"
"@types/browserslist-useragent@^3.0.0":
- version "3.0.5"
- resolved "https://registry.yarnpkg.com/@types/browserslist-useragent/-/browserslist-useragent-3.0.5.tgz#406d83d09688c83b42dd7f2ccf984aa41b13f243"
- integrity sha512-CLrJk4px6W5KY/7bmSkUMpWN1qOLFxZZ9+oWvSzarxJsOnMauvY6Tblf4ePpXv/3gEZx6j2iBpH0Ow3Wp8Z8+Q==
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/@types/browserslist-useragent/-/browserslist-useragent-3.0.7.tgz#4c9783bf3c31aa71f8040dc74eef7d59bc3b0e05"
+ integrity sha512-rVvdB0HoQvHDkS8SPgUv2tKfnf0zKIzBh8oisvnq82R3asgpnF857UTAUJuh+3VXPqMYdZ13VWfdIDUN/1iFmQ==
"@types/browserslist@^4.8.0":
version "4.15.0"
@@ -1431,56 +1425,56 @@
browserslist "*"
"@types/caniuse-api@^3.0.0":
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/@types/caniuse-api/-/caniuse-api-3.0.3.tgz#17899e2fa5d2443bd2576b05b7c1296b2b16cae0"
- integrity sha512-nOcaDp0Qa1i5T0IUeW5y8jiGD2VaOj9RV5FzfV5fpMBJ0vkPIC+NV9ELKHwooxBVEN2+mI0J+v6NC7oiEXpnLQ==
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/@types/caniuse-api/-/caniuse-api-3.0.6.tgz#68902fe2d809f65aa2b23a053f5c071e0044c443"
+ integrity sha512-yMGwHJaqwIEXc3x7EyY3CeS73QG9WeC00w2nZ0/inoRv9DiLIhfvrY6vmXMSKlpRLFxrLcAWJh3JTwYNPl3ihg==
"@types/chai-dom@^1.11.0":
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/@types/chai-dom/-/chai-dom-1.11.1.tgz#5f91fb34a612ccef177c70100c7c1b98a684d696"
- integrity sha512-q+fs4jdKZFDhXOWBehY0jDGCp8nxVe11Ia8MxqlIsJC3Y2JU149PSBYF2li2F3uxJFSAl2Rf8XeLWonHglpcGw==
+ version "1.11.3"
+ resolved "https://registry.yarnpkg.com/@types/chai-dom/-/chai-dom-1.11.3.tgz#1659ace2698cdcd9ed8b2c007876f53e37d9cc89"
+ integrity sha512-EUEZI7uID4ewzxnU7DJXtyvykhQuwe+etJ1wwOiJyQRTH/ifMWKX+ghiXkxCUvNJ6IQDodf0JXhuP6zZcy2qXQ==
dependencies:
"@types/chai" "*"
-"@types/chai@*", "@types/chai@^4.1.7", "@types/chai@^4.2.11", "@types/chai@^4.2.12", "@types/chai@^4.3.1":
- version "4.3.6"
- resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.6.tgz#7b489e8baf393d5dd1266fb203ddd4ea941259e6"
- integrity sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==
+"@types/chai@*", "@types/chai@^4.2.12", "@types/chai@^4.3.1":
+ version "4.3.11"
+ resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.11.tgz#e95050bf79a932cb7305dd130254ccdf9bde671c"
+ integrity sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==
"@types/co-body@^6.1.0":
- version "6.1.1"
- resolved "https://registry.yarnpkg.com/@types/co-body/-/co-body-6.1.1.tgz#28d253c95cfbe30c8e8c5d69d4c0dbbcffc101c2"
- integrity sha512-I9A1k7o4m8m6YPYJIGb1JyNTLqRWtSPg1JOZPWlE19w8Su2VRgRVp/SkKftQSwoxWHGUxGbON4jltONMumC8bQ==
+ version "6.1.3"
+ resolved "https://registry.yarnpkg.com/@types/co-body/-/co-body-6.1.3.tgz#201796c6389066b400cfcb4e1ec5c3db798265a2"
+ integrity sha512-UhuhrQ5hclX6UJctv5m4Rfp52AfG9o9+d9/HwjxhVB5NjXxr5t9oKgJxN8xRHgr35oo8meUEHUPFWiKg6y71aA==
dependencies:
"@types/node" "*"
"@types/qs" "*"
"@types/command-line-args@^5.0.0":
- version "5.2.1"
- resolved "https://registry.yarnpkg.com/@types/command-line-args/-/command-line-args-5.2.1.tgz#233bd1ba687e84ecbec0388e09f9ec9ebf63c55b"
- integrity sha512-U2OcmS2tj36Yceu+mRuPyUV0ILfau/h5onStcSCzqTENsq0sBiAp2TmaXu1k8fY4McLcPKSYl9FRzn2hx5bI+w==
+ version "5.2.3"
+ resolved "https://registry.yarnpkg.com/@types/command-line-args/-/command-line-args-5.2.3.tgz#553ce2fd5acf160b448d307649b38ffc60d39639"
+ integrity sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==
"@types/command-line-usage@^5.0.1":
- version "5.0.2"
- resolved "https://registry.yarnpkg.com/@types/command-line-usage/-/command-line-usage-5.0.2.tgz#ba5e3f6ae5a2009d466679cc431b50635bf1a064"
- integrity sha512-n7RlEEJ+4x4TS7ZQddTmNSxP+zziEG0TNsMfiRIxcIVXt71ENJ9ojeXmGO3wPoTdn7pJcU2xc3CJYMktNT6DPg==
+ version "5.0.4"
+ resolved "https://registry.yarnpkg.com/@types/command-line-usage/-/command-line-usage-5.0.4.tgz#374e4c62d78fbc5a670a0f36da10235af879a0d5"
+ integrity sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==
"@types/connect@*":
- version "3.4.36"
- resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.36.tgz#e511558c15a39cb29bd5357eebb57bd1459cd1ab"
- integrity sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==
+ version "3.4.38"
+ resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858"
+ integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==
dependencies:
"@types/node" "*"
"@types/content-disposition@*":
- version "0.5.6"
- resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.6.tgz#0f5fa03609f308a7a1a57e0b0afe4b95f1d19740"
- integrity sha512-GmShTb4qA9+HMPPaV2+Up8tJafgi38geFi7vL4qAM7k8BwjoelgHZqEUKJZLvughUw22h6vD/wvwN4IUCaWpDA==
+ version "0.5.8"
+ resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.8.tgz#6742a5971f490dc41e59d277eee71361fea0b537"
+ integrity sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==
"@types/convert-source-map@^2.0.0":
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/@types/convert-source-map/-/convert-source-map-2.0.1.tgz#e72e8a3de9d6fe3d8e43d5c101c346de2ff6abdf"
- integrity sha512-tm5Eb3AwhibN6ULRaad5TbNO83WoXVZLh2YRGAFH+qWkUz48l9Hu1jc+wJswB7T+ACWAG0cFnTeeQGpwedvlNw==
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@types/convert-source-map/-/convert-source-map-2.0.3.tgz#e586c22ca4af2d670d47d32d7fe365d5c5558695"
+ integrity sha512-ag0BfJLZf6CQz8VIuRIEYQ5Ggwk/82uvTQf27RcpyDNbY0Vw49LIPqAxk5tqYfrCs9xDaIMvl4aj7ZopnYL8bA==
"@types/cookie@^0.4.1":
version "0.4.1"
@@ -1488,9 +1482,9 @@
integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==
"@types/cookies@*":
- version "0.7.8"
- resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.8.tgz#16fccd6d58513a9833c527701a90cc96d216bc18"
- integrity sha512-y6KhF1GtsLERUpqOV+qZJrjUGzc0GE6UTa0b5Z/LZ7Nm2mKSdCXmS6Kdnl7fctPNnMSouHjxqEWI12/YqQfk5w==
+ version "0.7.10"
+ resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.10.tgz#c4881dca4dd913420c488508d192496c46eb4fd0"
+ integrity sha512-hmUCjAk2fwZVPPkkPBcI7jGLIR5mg4OVoNMBwU6aVsMm/iNPY7z9/R+x2fSwLt/ZXoGua6C5Zy2k5xOo9jUyhQ==
dependencies:
"@types/connect" "*"
"@types/express" "*"
@@ -1498,16 +1492,16 @@
"@types/node" "*"
"@types/cors@^2.8.12":
- version "2.8.14"
- resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.14.tgz#94eeb1c95eda6a8ab54870a3bf88854512f43a92"
- integrity sha512-RXHUvNWYICtbP6s18PnOCaqToK8y14DnLd75c6HfyKf228dxy7pHNOQkxPtvXKp/hINFMDjbYzsj63nnpPMSRQ==
+ version "2.8.17"
+ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b"
+ integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==
dependencies:
"@types/node" "*"
"@types/debounce@^1.2.0":
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/@types/debounce/-/debounce-1.2.1.tgz#79b65710bc8b6d44094d286aecf38e44f9627852"
- integrity sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/@types/debounce/-/debounce-1.2.4.tgz#cb7e85d9ad5ababfac2f27183e8ac8b576b2abb3"
+ integrity sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==
"@types/estree@0.0.39":
version "0.0.39"
@@ -1515,16 +1509,16 @@
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
"@types/etag@*":
- version "1.8.1"
- resolved "https://registry.yarnpkg.com/@types/etag/-/etag-1.8.1.tgz#593ca8ddb43acb3db049bd0955fd64d281ab58b9"
- integrity sha512-bsKkeSqN7HYyYntFRAmzcwx/dKW4Wa+KVMTInANlI72PWLQmOpZu96j0OqHZGArW4VQwCmJPteQlXaUDeOB0WQ==
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@types/etag/-/etag-1.8.3.tgz#0321c878a1ac1069131e4d90deab06db5ea2a0db"
+ integrity sha512-QYHv9Yeh1ZYSMPQOoxY4XC4F1r+xRUiAriB303F4G6uBsT3KKX60DjiogvVv+2VISVDuJhcIzMdbjT+Bm938QQ==
dependencies:
"@types/node" "*"
"@types/express-serve-static-core@^4.17.33":
- version "4.17.36"
- resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.36.tgz#baa9022119bdc05a4adfe740ffc97b5f9360e545"
- integrity sha512-zbivROJ0ZqLAtMzgzIUC4oNqDG9iF0lSsAqpOD9kbs5xcIM3dTiyuHvBc7R8MtWBp3AAWGaovJa+wzWPjLYW7Q==
+ version "4.17.41"
+ resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz#5077defa630c2e8d28aa9ffc2c01c157c305bef6"
+ integrity sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==
dependencies:
"@types/node" "*"
"@types/qs" "*"
@@ -1532,9 +1526,9 @@
"@types/send" "*"
"@types/express@*":
- version "4.17.17"
- resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4"
- integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d"
+ integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==
dependencies:
"@types/body-parser" "*"
"@types/express-serve-static-core" "^4.17.33"
@@ -1542,43 +1536,43 @@
"@types/serve-static" "*"
"@types/http-assert@*":
- version "1.5.3"
- resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.3.tgz#ef8e3d1a8d46c387f04ab0f2e8ab8cb0c5078661"
- integrity sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA==
+ version "1.5.5"
+ resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.5.tgz#dfb1063eb7c240ee3d3fe213dac5671cfb6a8dbf"
+ integrity sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==
"@types/http-errors@*":
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.2.tgz#a86e00bbde8950364f8e7846687259ffcd96e8c2"
- integrity sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f"
+ integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.3":
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
- integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7"
+ integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==
"@types/istanbul-lib-report@*":
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686"
- integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf"
+ integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==
dependencies:
"@types/istanbul-lib-coverage" "*"
"@types/istanbul-reports@^3.0.0":
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff"
- integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54"
+ integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==
dependencies:
"@types/istanbul-lib-report" "*"
"@types/keygrip@*":
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.3.tgz#2286b16ef71d8dea74dab00902ef419a54341bfe"
- integrity sha512-tfzBBb7OV2PbUfKbG6zRE5UbmtdLVCKT/XT364Z9ny6pXNbd9GnIB6aFYpq2A5lZ6mq9bhXgK6h5MFGNwhMmuQ==
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.6.tgz#1749535181a2a9b02ac04a797550a8787345b740"
+ integrity sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==
"@types/koa-compose@*":
- version "3.2.6"
- resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.6.tgz#17a077786d0ac5eee04c37a7d6c207b3252f6de9"
- integrity sha512-PHiciWxH3NRyAaxUdEDE1NIZNfvhgtPlsdkjRPazHC6weqt90Jr0uLhIQs+SDwC8HQ/jnA7UQP6xOqGFB7ugWw==
+ version "3.2.8"
+ resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.8.tgz#dec48de1f6b3d87f87320097686a915f1e954b57"
+ integrity sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==
dependencies:
"@types/koa" "*"
@@ -1591,32 +1585,32 @@
"@types/node" "*"
"@types/koa-etag@^3.0.0":
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/@types/koa-etag/-/koa-etag-3.0.1.tgz#291025c16380dd20648c219bd2281d4721e40194"
- integrity sha512-UkpP45FxOlwb33SPeCulTs2cIJg+tiQw/ea6vXp4JYJfMNNGUovEa/K1Id4+O2XNQe3rhCgagHMExjJfv9PhJQ==
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@types/koa-etag/-/koa-etag-3.0.3.tgz#67adf67b18057e4ecdc65efd705a979ba638d521"
+ integrity sha512-pOtRwgJOyP5XWoKUQpvvGbihep7ZoCQwJiFhZ2PdAjmXhgDFm5bbX9vYbXCwLIWG+WUb8CsOCf+78uae0Ho5sg==
dependencies:
"@types/etag" "*"
"@types/koa" "*"
"@types/koa-send@*":
- version "4.1.4"
- resolved "https://registry.yarnpkg.com/@types/koa-send/-/koa-send-4.1.4.tgz#c11fd792bbcf55d2c0117f975316c3f47ef2546e"
- integrity sha512-+ttyO5T1T1cLRUtk9etg/4E7ZIplJJUANkuzYptCPysWX5LRfGHsv9YOCiB7+gkAuedjEgZrl4K02RWJ2gaJ6Q==
+ version "4.1.6"
+ resolved "https://registry.yarnpkg.com/@types/koa-send/-/koa-send-4.1.6.tgz#15d90e95e3ccce669a15b6a3c56c3a650a167cea"
+ integrity sha512-vgnNGoOJkx7FrF0Jl6rbK1f8bBecqAchKpXtKuXzqIEdXTDO6dsSTjr+eZ5m7ltSjH4K/E7auNJEQCAd0McUPA==
dependencies:
"@types/koa" "*"
"@types/koa-static@^4.0.1":
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/@types/koa-static/-/koa-static-4.0.2.tgz#a199d2d64d2930755eb3ea370aeaf2cb6f501d67"
- integrity sha512-ns/zHg+K6XVPMuohjpOlpkR1WLa4VJ9czgUP9bxkCDn0JZBtUWbD/wKDZzPGDclkQK1bpAEScufCHOy8cbfL0w==
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/@types/koa-static/-/koa-static-4.0.4.tgz#ce6f2a5d14cc7ef19f9bf6ee8e4f3eadfcc77323"
+ integrity sha512-j1AUzzl7eJYEk9g01hNTlhmipFh8RFbOQmaMNLvLcNNAkPw0bdTs3XTa3V045XFlrWN0QYnblbDJv2RzawTn6A==
dependencies:
"@types/koa" "*"
"@types/koa-send" "*"
"@types/koa@*", "@types/koa@^2.0.48", "@types/koa@^2.11.6":
- version "2.13.9"
- resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.9.tgz#8d989ac17d7f033475fbe34c4f906c9287c2041a"
- integrity sha512-tPX3cN1dGrMn+sjCDEiQqXH2AqlPoPd594S/8zxwUm/ZbPsQXKqHPUypr2gjCPhHUc+nDJLduhh5lXI/1olnGQ==
+ version "2.13.12"
+ resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.12.tgz#70d87a9061a81909e0ee11ca50168416e8d3e795"
+ integrity sha512-vAo1KuDSYWFDB4Cs80CHvfmzSQWeUb909aQib0C0aFx4sw0K9UZFz2m5jaEP+b3X1+yr904iQiruS0hXi31jbw==
dependencies:
"@types/accepts" "*"
"@types/content-disposition" "*"
@@ -1640,19 +1634,19 @@
integrity sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==
"@types/mime-types@^2.1.0":
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.1.tgz#d9ba43490fa3a3df958759adf69396c3532cf2c1"
- integrity sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.4.tgz#93a1933e24fed4fb9e4adc5963a63efcbb3317a2"
+ integrity sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==
"@types/mime@*":
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"
- integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.4.tgz#2198ac274de6017b44d941e00261d5bc6a0e0a45"
+ integrity sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==
"@types/mime@^1":
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
- integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690"
+ integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==
"@types/minimatch@^3.0.3":
version "3.0.5"
@@ -1672,9 +1666,11 @@
integrity sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw==
"@types/node@*", "@types/node@>=10.0.0":
- version "20.6.3"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-20.6.3.tgz#5b763b321cd3b80f6b8dde7a37e1a77ff9358dd9"
- integrity sha512-HksnYH4Ljr4VQgEy2lTStbCKv/P590tmPe5HqOnv9Gprffgv5WXAY+Y5Gqniu0GGqeTCUdBnzC3QSrzPkBkAMA==
+ version "20.10.6"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.6.tgz#a3ec84c22965802bf763da55b2394424f22bfbb5"
+ integrity sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==
+ dependencies:
+ undici-types "~5.26.4"
"@types/parse5@^6.0.1":
version "6.0.3"
@@ -1682,33 +1678,33 @@
integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==
"@types/path-is-inside@^1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@types/path-is-inside/-/path-is-inside-1.0.0.tgz#02d6ff38975d684bdec96204494baf9f29f0e17f"
- integrity sha512-hfnXRGugz+McgX2jxyy5qz9sB21LRzlGn24zlwN2KEgoPtEvjzNRrLtUkOOebPDPZl3Rq7ywKxYvylVcEZDnEw==
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/@types/path-is-inside/-/path-is-inside-1.0.3.tgz#bda44a673de87bae62943ed617d0a246f7a034d0"
+ integrity sha512-xZoKJ7TQYIBc/ry4CHIV3M4V96zLMdTIGPT7Du+yYWevnfoaiW5bEPpkCL1RuEySw7k+JnlL1VcLZfyOg6Sp5g==
"@types/pixelmatch@^5.2.2":
- version "5.2.4"
- resolved "https://registry.yarnpkg.com/@types/pixelmatch/-/pixelmatch-5.2.4.tgz#ca145cc5ede1388c71c68edf2d1f5190e5ddd0f6"
- integrity sha512-HDaSHIAv9kwpMN7zlmwfTv6gax0PiporJOipcrGsVNF3Ba+kryOZc0Pio5pn6NhisgWr7TaajlPEKTbTAypIBQ==
+ version "5.2.6"
+ resolved "https://registry.yarnpkg.com/@types/pixelmatch/-/pixelmatch-5.2.6.tgz#fba6de304ac958495f27d85989f5c6bb7499a686"
+ integrity sha512-wC83uexE5KGuUODn6zkm9gMzTwdY5L0chiK+VrKcDfEjzxh1uadlWTvOmAbCpnM9zx/Ww3f8uKlYQVnO/TrqVg==
dependencies:
"@types/node" "*"
"@types/pngjs@^6.0.0":
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-6.0.1.tgz#c711ec3fbbf077fed274ecccaf85dd4673130072"
- integrity sha512-J39njbdW1U/6YyVXvC9+1iflZghP8jgRf2ndYghdJb5xL49LYDB+1EuAxfbuJ2IBbWIL3AjHPQhgaTxT3YaYeg==
+ version "6.0.4"
+ resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-6.0.4.tgz#9a457aebabd944efde1a773a0fa1d74933e8021b"
+ integrity sha512-atAK9xLKOnxiuArxcHovmnOUUGBZOQ3f0vCf43FnoKs6XnqiambT1kkJWmdo71IR+BoXSh+CueeFR0GfH3dTlQ==
dependencies:
"@types/node" "*"
"@types/qs@*":
- version "6.9.8"
- resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.8.tgz#f2a7de3c107b89b441e071d5472e6b726b4adf45"
- integrity sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==
+ version "6.9.11"
+ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.11.tgz#208d8a30bc507bd82e03ada29e4732ea46a6bbda"
+ integrity sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==
"@types/range-parser@*":
- version "1.2.4"
- resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
- integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
+ integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
"@types/resolve@0.0.8":
version "0.0.8"
@@ -1725,34 +1721,34 @@
"@types/node" "*"
"@types/send@*":
- version "0.17.1"
- resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.1.tgz#ed4932b8a2a805f1fe362a70f4e62d0ac994e301"
- integrity sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==
+ version "0.17.4"
+ resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a"
+ integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==
dependencies:
"@types/mime" "^1"
"@types/node" "*"
"@types/serve-static@*":
- version "1.15.2"
- resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.2.tgz#3e5419ecd1e40e7405d34093f10befb43f63381a"
- integrity sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==
+ version "1.15.5"
+ resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.5.tgz#15e67500ec40789a1e8c9defc2d32a896f05b033"
+ integrity sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==
dependencies:
"@types/http-errors" "*"
"@types/mime" "*"
"@types/node" "*"
"@types/sinon-chai@^3.2.3":
- version "3.2.9"
- resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.9.tgz#71feb938574bbadcb176c68e5ff1a6014c5e69d4"
- integrity sha512-/19t63pFYU0ikrdbXKBWj9PCdnKyTd0Qkz0X91Ta081cYsq90OxYdcWwK/dwEoDa6dtXgj2HJfmzgq+QZTHdmQ==
+ version "3.2.12"
+ resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.12.tgz#c7cb06bee44a534ec84f3a5534c3a3a46fd779b6"
+ integrity sha512-9y0Gflk3b0+NhQZ/oxGtaAJDvRywCa5sIyaVnounqLvmf93yBF4EgIRspePtkMs3Tr844nCclYMlcCNmLCvjuQ==
dependencies:
"@types/chai" "*"
"@types/sinon" "*"
"@types/sinon@*":
- version "10.0.16"
- resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.16.tgz#4bf10313bd9aa8eef1e50ec9f4decd3dd455b4d3"
- integrity sha512-j2Du5SYpXZjJVJtXBokASpPRj+e2z+VUhCPHmM6WMfe3dpHu6iVKJMU6AiBcMp/XTAYnEj6Wc1trJUWwZ0QaAQ==
+ version "17.0.2"
+ resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-17.0.2.tgz#9a769f67e62b45b7233f1fe01cb1f231d2393e1c"
+ integrity sha512-Zt6heIGsdqERkxctIpvN5Pv3edgBrhoeb3yHyxffd4InN0AX2SVNKSrhdDZKGQICVOxWP/q4DyhpfPNMSrpIiA==
dependencies:
"@types/sinonjs__fake-timers" "*"
@@ -1764,14 +1760,14 @@
"@types/sinonjs__fake-timers" "*"
"@types/sinonjs__fake-timers@*":
- version "8.1.2"
- resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz#bf2e02a3dbd4aecaf95942ecd99b7402e03fad5e"
- integrity sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==
+ version "8.1.5"
+ resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz#5fd3592ff10c1e9695d377020c033116cc2889f2"
+ integrity sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==
"@types/trusted-types@^2.0.2":
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.4.tgz#2b38784cd16957d3782e8e2b31c03bc1d13b4d65"
- integrity sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==
+ version "2.0.7"
+ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
+ integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
"@types/whatwg-url@^6.4.0":
version "6.4.0"
@@ -1788,9 +1784,9 @@
"@types/node" "*"
"@types/yauzl@^2.9.1":
- version "2.10.0"
- resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599"
- integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==
+ version "2.10.3"
+ resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999"
+ integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==
dependencies:
"@types/node" "*"
@@ -1806,10 +1802,10 @@
dependencies:
errorstacks "^2.2.0"
-"@web/browser-logs@^0.3.2":
- version "0.3.3"
- resolved "https://registry.yarnpkg.com/@web/browser-logs/-/browser-logs-0.3.3.tgz#121e5b662db2707c4b8cd1628d86903f059f5031"
- integrity sha512-wt8arj0x7ghXbnipgCvLR+xQ90cFg16ae23cFbInCrJvAxvyI22bAtT24W4XOXMPXwWLBVUJwBgBcXo3oKIvDw==
+"@web/browser-logs@^0.4.0":
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/@web/browser-logs/-/browser-logs-0.4.0.tgz#8c4adddac46be02dff1a605312132053b3737d0a"
+ integrity sha512-/EBiDAUCJ2DzZhaFxTPRIznEPeafdLbXShIL6aTu7x73x7ZoxSDv7DGuTsh2rWNMUa4+AKli4UORrpyv6QBOiA==
dependencies:
errorstacks "^2.2.0"
@@ -1844,14 +1840,14 @@
picomatch "^2.2.2"
ws "^7.4.2"
-"@web/dev-server-core@^0.5.1":
- version "0.5.2"
- resolved "https://registry.yarnpkg.com/@web/dev-server-core/-/dev-server-core-0.5.2.tgz#27fe5448e587a87272b556b44ce84c6453655cdb"
- integrity sha512-7YjWmwzM+K5fPvBCXldUIMTK4EnEufi1aWQWinQE81oW1CqzEwmyUNCtnWV9fcPA4kJC4qrpcjWNGF4YDWxuSg==
+"@web/dev-server-core@^0.7.0":
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/@web/dev-server-core/-/dev-server-core-0.7.0.tgz#ffe71dd272ecb73a2b0c1ee23f3fad812780b998"
+ integrity sha512-1FJe6cJ3r0x0ZmxY/FnXVduQD4lKX7QgYhyS6N+VmIpV+tBU4sGRbcrmeoYeY+nlnPa6p2oNuonk3X5ln/W95g==
dependencies:
"@types/koa" "^2.11.6"
"@types/ws" "^7.4.0"
- "@web/parse5-utils" "^2.0.0"
+ "@web/parse5-utils" "^2.1.0"
chokidar "^3.4.3"
clone "^2.1.2"
es-module-lexer "^1.0.0"
@@ -1919,10 +1915,10 @@
"@types/parse5" "^6.0.1"
parse5 "^6.0.1"
-"@web/parse5-utils@^2.0.0":
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/@web/parse5-utils/-/parse5-utils-2.0.1.tgz#11b91417165a838954dcf228383cfd8e1bdaf914"
- integrity sha512-FQI72BU5CXhpp7gLRskOQGGCcwvagLZnMnDwAfjrxo3pm1KOQzr8Vl+438IGpHV62xvjNdF1pjXwXcf7eekWGw==
+"@web/parse5-utils@^2.1.0":
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/@web/parse5-utils/-/parse5-utils-2.1.0.tgz#3d33aca62c66773492f2fba89d23a45f8b57ba4a"
+ integrity sha512-GzfK5disEJ6wEjoPwx8AVNwUe9gYIiwc+x//QYxYDAFKUp4Xb1OJAGLc2l2gVrSQmtPGLKrTRcW90Hv4pEq1qA==
dependencies:
"@types/parse5" "^6.0.1"
parse5 "^6.0.1"
@@ -1945,12 +1941,12 @@
"@web/test-runner-core" "^0.10.29"
mkdirp "^1.0.4"
-"@web/test-runner-commands@^0.7.0":
- version "0.7.0"
- resolved "https://registry.yarnpkg.com/@web/test-runner-commands/-/test-runner-commands-0.7.0.tgz#c9693e4e8b05ef06a2102e03ac924bcbf7985312"
- integrity sha512-3aXeGrkynOdJ5jgZu5ZslcWmWuPVY9/HNdWDUqPyNePG08PKmLV9Ij342ODDL6OVsxF5dvYn1312PhDqu5AQNw==
+"@web/test-runner-commands@^0.9.0":
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/@web/test-runner-commands/-/test-runner-commands-0.9.0.tgz#ed15a021249948204bb27559eb437ff6ceeee067"
+ integrity sha512-zeLI6QdH0jzzJMDV5O42Pd8WLJtYqovgdt0JdytgHc0d1EpzXDsc7NTCJSImboc2NcayIsWAvvGGeRF69SMMYg==
dependencies:
- "@web/test-runner-core" "^0.11.0"
+ "@web/test-runner-core" "^0.13.0"
mkdirp "^1.0.4"
"@web/test-runner-core@^0.10.20", "@web/test-runner-core@^0.10.27", "@web/test-runner-core@^0.10.29":
@@ -1985,10 +1981,10 @@
picomatch "^2.2.2"
source-map "^0.7.3"
-"@web/test-runner-core@^0.11.0":
- version "0.11.4"
- resolved "https://registry.yarnpkg.com/@web/test-runner-core/-/test-runner-core-0.11.4.tgz#590994c3fc69337e2c5411bf11c293dd061cc07a"
- integrity sha512-E7BsKAP8FAAEsfj4viASjmuaYfOw4UlKP9IFqo4W20eVyd1nbUWU3Amq4Jksh7W/j811qh3VaFNjDfCwklQXMg==
+"@web/test-runner-core@^0.13.0":
+ version "0.13.0"
+ resolved "https://registry.yarnpkg.com/@web/test-runner-core/-/test-runner-core-0.13.0.tgz#a3799461002fcb969b0baa100d88be6c1ff504f4"
+ integrity sha512-mUrETPg9n4dHWEk+D46BU3xVhQf+ljT4cG7FSpmF7AIOsXWgWHoaXp6ReeVcEmM5fmznXec2O/apTb9hpGrP3w==
dependencies:
"@babel/code-frame" "^7.12.11"
"@types/babel__code-frame" "^7.0.2"
@@ -1997,8 +1993,8 @@
"@types/debounce" "^1.2.0"
"@types/istanbul-lib-coverage" "^2.0.3"
"@types/istanbul-reports" "^3.0.0"
- "@web/browser-logs" "^0.3.2"
- "@web/dev-server-core" "^0.5.1"
+ "@web/browser-logs" "^0.4.0"
+ "@web/dev-server-core" "^0.7.0"
chokidar "^3.4.3"
cli-cursor "^3.1.0"
co-body "^6.1.0"
@@ -2259,9 +2255,9 @@ aws4@^1.8.0:
integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==
axe-core@^4.3.3:
- version "4.8.1"
- resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.8.1.tgz#6948854183ee7e7eae336b9877c5bafa027998ea"
- integrity sha512-9l850jDDPnKq48nbad8SiEelCv4OrUWrKab/cPj0GScVg6cb6NbCCt/Ulk26QEq5jP9NnGr04Bit1BHyV6r5CQ==
+ version "4.8.3"
+ resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.8.3.tgz#205df863dd9917d5979e9435dab4d47692759051"
+ integrity sha512-d5ZQHPSPkF9Tw+yfyDcRoUOc4g/8UloJJe5J8m4L5+c7AtDdjDLRxew/knnI4CxvtdxEUVgWz4x3OIQUIFiMfw==
babel-plugin-istanbul@^5.1.4:
version "5.2.0"
@@ -2273,29 +2269,29 @@ babel-plugin-istanbul@^5.1.4:
istanbul-lib-instrument "^3.3.0"
test-exclude "^5.2.3"
-babel-plugin-polyfill-corejs2@^0.4.5:
- version "0.4.5"
- resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz#8097b4cb4af5b64a1d11332b6fb72ef5e64a054c"
- integrity sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==
+babel-plugin-polyfill-corejs2@^0.4.7:
+ version "0.4.7"
+ resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz#679d1b94bf3360f7682e11f2cb2708828a24fe8c"
+ integrity sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ==
dependencies:
"@babel/compat-data" "^7.22.6"
- "@babel/helper-define-polyfill-provider" "^0.4.2"
+ "@babel/helper-define-polyfill-provider" "^0.4.4"
semver "^6.3.1"
-babel-plugin-polyfill-corejs3@^0.8.3:
- version "0.8.3"
- resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz#b4f719d0ad9bb8e0c23e3e630c0c8ec6dd7a1c52"
- integrity sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA==
+babel-plugin-polyfill-corejs3@^0.8.7:
+ version "0.8.7"
+ resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz#941855aa7fdaac06ed24c730a93450d2b2b76d04"
+ integrity sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==
dependencies:
- "@babel/helper-define-polyfill-provider" "^0.4.2"
- core-js-compat "^3.31.0"
+ "@babel/helper-define-polyfill-provider" "^0.4.4"
+ core-js-compat "^3.33.1"
-babel-plugin-polyfill-regenerator@^0.5.2:
- version "0.5.2"
- resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz#80d0f3e1098c080c8b5a65f41e9427af692dc326"
- integrity sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==
+babel-plugin-polyfill-regenerator@^0.5.4:
+ version "0.5.4"
+ resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.4.tgz#c6fc8eab610d3a11eb475391e52584bacfc020f4"
+ integrity sha512-S/x2iOCvDaCASLYsOOgWOq4bCfKYVqvO/uxjkaYyZ3rVsVE3CeAI/c84NpyuBBymEgNvHgjEot3a9/Z/kXvqsg==
dependencies:
- "@babel/helper-define-polyfill-provider" "^0.4.2"
+ "@babel/helper-define-polyfill-provider" "^0.4.4"
balanced-match@^1.0.0:
version "1.0.2"
@@ -2389,15 +2385,15 @@ browserslist-useragent@^3.0.2:
useragent "^2.3.0"
yamlparser "^0.0.2"
-browserslist@*, browserslist@^4.0.0, browserslist@^4.16.5, browserslist@^4.19.1, browserslist@^4.21.10, browserslist@^4.21.9, browserslist@^4.9.1:
- version "4.21.10"
- resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0"
- integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==
+browserslist@*, browserslist@^4.0.0, browserslist@^4.16.5, browserslist@^4.19.1, browserslist@^4.22.2, browserslist@^4.9.1:
+ version "4.22.2"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.2.tgz#704c4943072bd81ea18997f3bd2180e89c77874b"
+ integrity sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==
dependencies:
- caniuse-lite "^1.0.30001517"
- electron-to-chromium "^1.4.477"
- node-releases "^2.0.13"
- update-browserslist-db "^1.0.11"
+ caniuse-lite "^1.0.30001565"
+ electron-to-chromium "^1.4.601"
+ node-releases "^2.0.14"
+ update-browserslist-db "^1.0.13"
buffer-crc32@~0.2.3:
version "0.2.13"
@@ -2436,12 +2432,13 @@ cache-content-type@^1.0.0:
ylru "^1.2.0"
call-bind@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
- integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513"
+ integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==
dependencies:
- function-bind "^1.1.1"
- get-intrinsic "^1.0.2"
+ function-bind "^1.1.2"
+ get-intrinsic "^1.2.1"
+ set-function-length "^1.1.1"
camel-case@^4.1.1:
version "4.1.2"
@@ -2471,10 +2468,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
-caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001033, caniuse-lite@^1.0.30001517:
- version "1.0.30001538"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001538.tgz#9dbc6b9af1ff06b5eb12350c2012b3af56744f3f"
- integrity sha512-HWJnhnID+0YMtGlzcp3T9drmBJUVDchPJ08tpUGFLs9CYlwWPH2uLgpHn8fND5pCgXVtnGS3H4QR9XLMHVNkHw==
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001033, caniuse-lite@^1.0.30001565:
+ version "1.0.30001572"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz#1ccf7dc92d2ee2f92ed3a54e11b7b4a3041acfa0"
+ integrity sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw==
caseless@~0.12.0:
version "0.12.0"
@@ -2550,9 +2547,9 @@ clean-css@^4.2.3:
source-map "~0.6.0"
clean-css@^5.3.1:
- version "5.3.2"
- resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.2.tgz#70ecc7d4d4114921f5d298349ff86a31a9975224"
- integrity sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==
+ version "5.3.3"
+ resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd"
+ integrity sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==
dependencies:
source-map "~0.6.0"
@@ -2712,25 +2709,25 @@ cookie@~0.4.1:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
-cookies@~0.8.0:
- version "0.8.0"
- resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90"
- integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==
+cookies@~0.9.0:
+ version "0.9.1"
+ resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.9.1.tgz#3ffed6f60bb4fb5f146feeedba50acc418af67e3"
+ integrity sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==
dependencies:
depd "~2.0.0"
keygrip "~1.1.0"
core-js-bundle@^3.6.0, core-js-bundle@^3.8.1:
- version "3.32.2"
- resolved "https://registry.yarnpkg.com/core-js-bundle/-/core-js-bundle-3.32.2.tgz#3a7736797ef483ff5ced565864f7b0a09cbeded2"
- integrity sha512-USljqWm24S8dyZdUEh8pHBxUsHcsVQaWmkZsR8e5ZHdpnGEO1XDxCZHP6/ACtgjkFQ/I/1SnTuWEBFPThMHfMQ==
+ version "3.35.0"
+ resolved "https://registry.yarnpkg.com/core-js-bundle/-/core-js-bundle-3.35.0.tgz#dd87c465625b52a35dd891a930789d11cae00fe0"
+ integrity sha512-gyqx4VKhV1tGhoxeYoxVchR1vMxbQJrC8BrCPnIU163Oyf9//GYQNU2eH7lmjav2K+5WGAWgLyZGxAfGggwJKA==
-core-js-compat@^3.31.0:
- version "3.32.2"
- resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.32.2.tgz#8047d1a8b3ac4e639f0d4f66d4431aa3b16e004c"
- integrity sha512-+GjlguTDINOijtVRUxrQOv3kfu9rl+qPNdX2LTbJ/ZyVTuxK+ksVSAGX1nHstu4hrv1En/uPTtWgq2gI5wt4AQ==
+core-js-compat@^3.31.0, core-js-compat@^3.33.1:
+ version "3.35.0"
+ resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.35.0.tgz#c149a3d1ab51e743bc1da61e39cb51f461a41873"
+ integrity sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==
dependencies:
- browserslist "^4.21.10"
+ browserslist "^4.22.2"
core-util-is@1.0.2:
version "1.0.2"
@@ -2781,7 +2778,7 @@ debug@2.6.9, debug@^2.6.9:
dependencies:
ms "2.0.0"
-debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2:
+debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -2822,6 +2819,15 @@ deepmerge@^4.2.2:
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
+define-data-property@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3"
+ integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==
+ dependencies:
+ get-intrinsic "^1.2.1"
+ gopd "^1.0.1"
+ has-property-descriptors "^1.0.0"
+
define-lazy-prop@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
@@ -2920,10 +2926,10 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
-electron-to-chromium@^1.4.477, electron-to-chromium@^1.4.67:
- version "1.4.526"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.526.tgz#1bcda5f2b8238e497c20fcdb41af5da907a770e2"
- integrity sha512-tjjTMjmZAx1g6COrintLTa2/jcafYKxKoiEkdQOrVdbLaHh2wCt2nsAF8ZHweezkrP+dl/VG9T5nabcYoo0U5Q==
+electron-to-chromium@^1.4.601, electron-to-chromium@^1.4.67:
+ version "1.4.617"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.617.tgz#3b0dde6c54e5f0f26db75ce6c6ae751e5df4bf75"
+ integrity sha512-sYNE3QxcDS4ANW1k4S/wWYMXjCVcFSOX3Bg8jpuMFaXt/x8JCmp0R1Xe1ZXDX4WXnSRBf+GJ/3eGWicUuQq5cg==
emoji-regex@^8.0.0:
version "8.0.0"
@@ -2948,9 +2954,9 @@ engine.io-parser@~5.2.1:
integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==
engine.io@~6.5.2:
- version "6.5.2"
- resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.2.tgz#769348ced9d56bd47bd83d308ec1c3375e85937c"
- integrity sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA==
+ version "6.5.4"
+ resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.4.tgz#6822debf324e781add2254e912f8568508850cdc"
+ integrity sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==
dependencies:
"@types/cookie" "^0.4.1"
"@types/cors" "^2.8.12"
@@ -2981,9 +2987,9 @@ error-ex@^1.3.1:
is-arrayish "^0.2.1"
errorstacks@^2.2.0:
- version "2.4.0"
- resolved "https://registry.yarnpkg.com/errorstacks/-/errorstacks-2.4.0.tgz#2155674dd9e741aacda3f3b8b967d9c40a4a0baf"
- integrity sha512-5ecWhU5gt0a5G05nmQcgCxP5HperSMxLDzvWlT5U+ZSKkuDK0rJ3dbCQny6/vSCIXjwrhwSecXBbw1alr295hQ==
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/errorstacks/-/errorstacks-2.4.1.tgz#05adf6de1f5b04a66f2c12cc0593e1be2b18cd0f"
+ integrity sha512-jE4i0SMYevwu/xxAuzhly/KTwtj0xDhbzB6m1xPImxTkw8wcCbgarOQPfCVMi5JKVyW7in29pNJCCJrry3Ynnw==
es-dev-server@^1.57.8:
version "1.60.2"
@@ -3062,9 +3068,9 @@ es-module-lexer@^0.3.13:
integrity sha512-Va0Q/xqtrss45hWzP8CZJwzGSZJjDM5/MJRE3IXXnUCcVLElR9BRaE9F62BopysASyc4nM3uwhSW7FFB9nlWAA==
es-module-lexer@^1.0.0:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.1.tgz#c1b0dd5ada807a3b3155315911f364dc4e909db1"
- integrity sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.4.1.tgz#41ea21b43908fe6a287ffcbe4300f790555331f5"
+ integrity sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==
es-module-shims@^0.4.6:
version "0.4.7"
@@ -3072,9 +3078,9 @@ es-module-shims@^0.4.6:
integrity sha512-0LTiSQoPWwdcaTVIQXhGlaDwTneD0g9/tnH1PNs3zHFFH+xoCeJclDM3rQeqF9nurXPfMKm3l9+kfPRa5VpbKg==
es-module-shims@^1.4.1:
- version "1.8.0"
- resolved "https://registry.yarnpkg.com/es-module-shims/-/es-module-shims-1.8.0.tgz#c0c8e7f5a0d041998b9410879619ab745cb76a3a"
- integrity sha512-5l/AqgnWvYFF38qkK8VNoQ8BL3LkJ8bAJuxhOKA/JqoLC4bcaeJeLwMkhEcrDsf5IUCDdwZ6eEG40+Xuh/APcQ==
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/es-module-shims/-/es-module-shims-1.8.2.tgz#54e09c96e1a8e897e55aa9ef3b12d9a818b19b25"
+ integrity sha512-7vIYVzpOhXtpc3Yn03itB+GSgVZFW7oL4kdydA+iL+IEi7HiSLBUxM05QFw4SxTl6e++pMpGqZPo2+vdNs3TbA==
"esbuild@^0.16 || ^0.17":
version "0.17.19"
@@ -3176,9 +3182,9 @@ fast-deep-equal@^3.1.1:
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-glob@^3.2.9:
- version "3.3.1"
- resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4"
- integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
+ integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
@@ -3192,9 +3198,9 @@ fast-json-stable-stringify@^2.0.0:
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
fastq@^1.6.0:
- version "1.15.0"
- resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
- integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==
+ version "1.16.0"
+ resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.16.0.tgz#83b9a9375692db77a822df081edb6a9cf6839320"
+ integrity sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==
dependencies:
reusify "^1.0.4"
@@ -3266,9 +3272,9 @@ flatted@^3.2.7:
integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==
follow-redirects@^1.0.0:
- version "1.15.3"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a"
- integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==
+ version "1.15.4"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
+ integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
forever-agent@~0.6.1:
version "0.6.1"
@@ -3318,10 +3324,10 @@ fsevents@~2.3.2:
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
-function-bind@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
- integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+function-bind@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+ integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
@@ -3333,15 +3339,15 @@ get-caller-file@^2.0.5:
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
-get-intrinsic@^1.0.2:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82"
- integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==
+get-intrinsic@^1.0.2, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b"
+ integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==
dependencies:
- function-bind "^1.1.1"
- has "^1.0.3"
+ function-bind "^1.1.2"
has-proto "^1.0.1"
has-symbols "^1.0.3"
+ hasown "^2.0.0"
get-stream@^5.1.0:
version "5.2.0"
@@ -3410,6 +3416,13 @@ globby@^11.0.1:
merge2 "^1.4.1"
slash "^3.0.0"
+gopd@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
+ integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
+ dependencies:
+ get-intrinsic "^1.1.3"
+
graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.6:
version "4.2.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
@@ -3443,6 +3456,13 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+has-property-descriptors@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340"
+ integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==
+ dependencies:
+ get-intrinsic "^1.2.2"
+
has-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0"
@@ -3460,12 +3480,12 @@ has-tostringtag@^1.0.0:
dependencies:
has-symbols "^1.0.2"
-has@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
- integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+hasown@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c"
+ integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==
dependencies:
- function-bind "^1.1.1"
+ function-bind "^1.1.2"
he@1.2.0, he@^1.2.0:
version "1.2.0"
@@ -3574,14 +3594,14 @@ ieee754@^1.1.13:
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
ignore@^5.2.0:
- version "5.2.4"
- resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
- integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78"
+ integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==
inflation@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/inflation/-/inflation-2.0.0.tgz#8b417e47c28f925a45133d914ca1fd389107f30f"
- integrity sha512-m3xv4hJYR2oXw4o4Y5l6P5P16WYmazYof+el6Al3f+YlggGj6qT9kImBAnzDelRALnP5d3h4jGBPKzYCizjZZw==
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/inflation/-/inflation-2.1.0.tgz#9214db11a47e6f756d111c4f9df96971c60f886c"
+ integrity sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==
inflight@^1.0.4:
version "1.0.6"
@@ -3631,11 +3651,11 @@ is-builtin-module@^3.1.0:
builtin-modules "^3.3.0"
is-core-module@^2.13.0:
- version "2.13.0"
- resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db"
- integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==
+ version "2.13.1"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384"
+ integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==
dependencies:
- has "^1.0.3"
+ hasown "^2.0.0"
is-docker@^2.0.0, is-docker@^2.1.1:
version "2.2.1"
@@ -3734,9 +3754,9 @@ istanbul-lib-coverage@^2.0.5:
integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==
istanbul-lib-coverage@^3.0.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3"
- integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756"
+ integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==
istanbul-lib-instrument@^3.3.0:
version "3.3.0"
@@ -3963,15 +3983,15 @@ koa-static@^5.0.0:
koa-send "^5.0.0"
koa@^2.13.0, koa@^2.7.0:
- version "2.14.2"
- resolved "https://registry.yarnpkg.com/koa/-/koa-2.14.2.tgz#a57f925c03931c2b4d94b19d2ebf76d3244863fc"
- integrity sha512-VFI2bpJaodz6P7x2uyLiX6RLYpZmOJqNmoCst/Yyd7hQlszyPwG/I9CQJ63nOtKSxpt5M7NH67V6nJL2BwCl7g==
+ version "2.15.0"
+ resolved "https://registry.yarnpkg.com/koa/-/koa-2.15.0.tgz#d24ae1b0ff378bf12eb3df584ab4204e4c12ac2b"
+ integrity sha512-KEL/vU1knsoUvfP4MC4/GthpQrY/p6dzwaaGI6Rt4NQuFqkw3qrvsdYF5pz3wOfi7IGTvMPHC9aZIcUKYFNxsw==
dependencies:
accepts "^1.3.5"
cache-content-type "^1.0.0"
content-disposition "~0.5.2"
content-type "^1.0.4"
- cookies "~0.8.0"
+ cookies "~0.9.0"
debug "^4.3.2"
delegates "^1.0.0"
depd "^2.0.0"
@@ -3999,30 +4019,30 @@ lighthouse-logger@^1.0.0:
debug "^2.6.9"
marky "^1.2.2"
-lit-element@^3.3.0:
- version "3.3.3"
- resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.3.3.tgz#10bc19702b96ef5416cf7a70177255bfb17b3209"
- integrity sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==
+lit-element@^4.0.0:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-4.0.2.tgz#1a519896d5ab7c7be7a8729f400499e38779c093"
+ integrity sha512-/W6WQZUa5VEXwC7H9tbtDMdSs9aWil3Ou8hU6z2cOKWbsm/tXPAcsoaHVEtrDo0zcOIE5GF6QgU55tlGL2Nihg==
dependencies:
- "@lit-labs/ssr-dom-shim" "^1.1.0"
- "@lit/reactive-element" "^1.3.0"
- lit-html "^2.8.0"
+ "@lit-labs/ssr-dom-shim" "^1.1.2"
+ "@lit/reactive-element" "^2.0.0"
+ lit-html "^3.1.0"
-lit-html@^2.0.0, lit-html@^2.8.0:
- version "2.8.0"
- resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.8.0.tgz#96456a4bb4ee717b9a7d2f94562a16509d39bffa"
- integrity sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==
+"lit-html@^2.0.0 || ^3.0.0", lit-html@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-3.1.0.tgz#a7b93dd682073f2e2029656f4e9cd91e8034c196"
+ integrity sha512-FwAjq3iNsaO6SOZXEIpeROlJLUlrbyMkn4iuv4f4u1H40Jw8wkeR/OUXZUHUoiYabGk8Y4Y0F/rgq+R4MrOLmA==
dependencies:
"@types/trusted-types" "^2.0.2"
-lit@^2.0.0:
- version "2.8.0"
- resolved "https://registry.yarnpkg.com/lit/-/lit-2.8.0.tgz#4d838ae03059bf9cafa06e5c61d8acc0081e974e"
- integrity sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==
+"lit@^2.0.0 || ^3.0.0":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/lit/-/lit-3.1.0.tgz#76429b85dc1f5169fed499a0f7e89e2e619010c9"
+ integrity sha512-rzo/hmUqX8zmOdamDAeydfjsGXbbdtAFqMhmocnh2j9aDYqbu0fjXygjCa0T99Od9VQ/2itwaGrjZz/ZELVl7w==
dependencies:
- "@lit/reactive-element" "^1.6.0"
- lit-element "^3.3.0"
- lit-html "^2.8.0"
+ "@lit/reactive-element" "^2.0.0"
+ lit-element "^4.0.0"
+ lit-html "^3.1.0"
load-json-file@^4.0.0:
version "4.0.0"
@@ -4326,9 +4346,9 @@ nanoid@3.3.1:
integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==
nanoid@^3.1.25:
- version "3.3.6"
- resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
- integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
+ version "3.3.7"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
+ integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
negotiator@0.6.3:
version "0.6.3"
@@ -4336,9 +4356,9 @@ negotiator@0.6.3:
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
nise@^5.1.1:
- version "5.1.4"
- resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.4.tgz#491ce7e7307d4ec546f5a659b2efe94a18b4bbc0"
- integrity sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==
+ version "5.1.5"
+ resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.5.tgz#f2aef9536280b6c18940e32ba1fbdc770b8964ee"
+ integrity sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==
dependencies:
"@sinonjs/commons" "^2.0.0"
"@sinonjs/fake-timers" "^10.0.2"
@@ -4368,10 +4388,10 @@ node-fetch@^2.6.0:
dependencies:
whatwg-url "^5.0.0"
-node-releases@^2.0.13:
- version "2.0.13"
- resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"
- integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
+node-releases@^2.0.14:
+ version "2.0.14"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
+ integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==
normalize-package-data@^2.3.2:
version "2.5.0"
@@ -4399,9 +4419,9 @@ object-assign@^4, object-assign@^4.0.1:
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
object-inspect@^1.9.0:
- version "1.12.3"
- resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
- integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2"
+ integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==
on-finished@2.4.1, on-finished@^2.3.0:
version "2.4.1"
@@ -4627,17 +4647,17 @@ pkg-dir@4.2.0:
dependencies:
find-up "^4.0.0"
-playwright-core@1.38.0:
- version "1.38.0"
- resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.38.0.tgz#cb8e135da1c0b1918b070642372040ed9aa7009a"
- integrity sha512-f8z1y8J9zvmHoEhKgspmCvOExF2XdcxMW8jNRuX4vkQFrzV4MlZ55iwb5QeyiFQgOFCUolXiRHgpjSEnqvO48g==
+playwright-core@1.40.1:
+ version "1.40.1"
+ resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.40.1.tgz#442d15e86866a87d90d07af528e0afabe4c75c05"
+ integrity sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==
playwright@^1.22.2:
- version "1.38.0"
- resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.38.0.tgz#0ee19d38512b7b1f961c0eb44008a6fed373d206"
- integrity sha512-fJGw+HO0YY+fU/F1N57DMO+TmXHTrmr905J05zwAQE9xkuwP/QLDk63rVhmyxh03dYnEhnRbsdbH9B0UVVRB3A==
+ version "1.40.1"
+ resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.40.1.tgz#a11bf8dca15be5a194851dbbf3df235b9f53d7ae"
+ integrity sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw==
dependencies:
- playwright-core "1.38.0"
+ playwright-core "1.40.1"
optionalDependencies:
fsevents "2.3.2"
@@ -4705,9 +4725,9 @@ pump@^3.0.0:
once "^1.3.1"
punycode@^2.1.0, punycode@^2.1.1:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
- integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
+ integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
puppeteer-core@^13.1.3:
version "13.7.0"
@@ -4834,9 +4854,9 @@ regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.7:
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regenerator-runtime@^0.14.0:
- version "0.14.0"
- resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
- integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==
+ version "0.14.1"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
+ integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
regenerator-transform@^0.15.2:
version "0.15.2"
@@ -4924,9 +4944,9 @@ resolve-path@^1.4.0:
path-is-absolute "1.0.1"
resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0:
- version "1.22.6"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.6.tgz#dd209739eca3aef739c626fea1b4f3c506195362"
- integrity sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==
+ version "1.22.8"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
+ integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
dependencies:
is-core-module "^2.13.0"
path-parse "^1.0.7"
@@ -5005,6 +5025,16 @@ serialize-javascript@6.0.0:
dependencies:
randombytes "^2.1.0"
+set-function-length@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed"
+ integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==
+ dependencies:
+ define-data-property "^1.1.1"
+ get-intrinsic "^1.2.1"
+ gopd "^1.0.1"
+ has-property-descriptors "^1.0.0"
+
setprototypeof@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
@@ -5128,14 +5158,14 @@ spdx-expression-parse@^3.0.0:
spdx-license-ids "^3.0.0"
spdx-license-ids@^3.0.0:
- version "3.0.15"
- resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.15.tgz#142460aabaca062bc7cd4cc87b7d50725ed6a4ba"
- integrity sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==
+ version "3.0.16"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz#a14f64e0954f6e25cc6587bd4f392522db0d998f"
+ integrity sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==
sshpk@^1.7.0:
- version "1.17.0"
- resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5"
- integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==
+ version "1.18.0"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028"
+ integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==
dependencies:
asn1 "~0.2.3"
assert-plus "^1.0.0"
@@ -5245,9 +5275,9 @@ supports-preserve-symlinks-flag@^1.0.0:
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
systemjs@^6.3.1, systemjs@^6.8.3:
- version "6.14.2"
- resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-6.14.2.tgz#e289f959f8c8b407403bd39c6abaa16f2c13f316"
- integrity sha512-1TlOwvKWdXxAY9vba+huLu99zrQURDWA8pUTYsRIYDZYQbGyK+pyEP4h4dlySsqo7ozyJBmYD20F+iUHhAltEg==
+ version "6.14.3"
+ resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-6.14.3.tgz#c1d6e4ff5f9ff7106e5bb3d451360b1a066bde8a"
+ integrity sha512-hQv45irdhXudAOr8r6SVSpJSGtogdGZUbJBRKCE5nsIS7tsxxvnIHqT4IOPWj+P+HcSzeWzHlGCGpmhPDIKe+w==
table-layout@^1.0.2:
version "1.0.2"
@@ -5450,14 +5480,14 @@ typical@^7.1.1:
integrity sha512-T+tKVNs6Wu7IWiAce5BgMd7OZfNYUndHwc5MknN+UHOudi7sGZzuHdCadllRuqJ3fPtgFtIH9+lt9qRv6lmpfA==
ua-parser-js@^0.7.30:
- version "0.7.36"
- resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.36.tgz#382c5d6fc09141b6541be2cae446ecfcec284db2"
- integrity sha512-CPPLoCts2p7D8VbybttE3P2ylv0OBZEAy7a12DsulIEcAiMtWJy+PBgMXgWDI80D5UwqE8oQPHYnk13tm38M2Q==
+ version "0.7.37"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.37.tgz#e464e66dac2d33a7a1251d7d7a99d6157ec27832"
+ integrity sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA==
ua-parser-js@^1.0.33:
- version "1.0.36"
- resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.36.tgz#a9ab6b9bd3a8efb90bb0816674b412717b7c428c"
- integrity sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw==
+ version "1.0.37"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.37.tgz#b5dc7b163a5c1f0c510b08446aed4da92c46373f"
+ integrity sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==
unbzip2-stream@1.4.3:
version "1.4.3"
@@ -5467,6 +5497,11 @@ unbzip2-stream@1.4.3:
buffer "^5.2.1"
through "^2.3.8"
+undici-types@~5.26.4:
+ version "5.26.5"
+ resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
+ integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
+
unicode-canonical-property-names-ecmascript@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"
@@ -5500,10 +5535,10 @@ unpipe@1.0.0, unpipe@~1.0.0:
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
-update-browserslist-db@^1.0.11:
- version "1.0.12"
- resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.12.tgz#868ce670ac09b4a4d4c86b608701c0dee2dc41cd"
- integrity sha512-tE1smlR58jxbFMtrMpFNRmsrOXlpNXss965T1CrpwuZUzUAg/TBQc94SpyhDLSzrqrJS9xTRBthnZAGcE1oaxg==
+update-browserslist-db@^1.0.13:
+ version "1.0.13"
+ resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4"
+ integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==
dependencies:
escalade "^3.1.1"
picocolors "^1.0.0"
@@ -5548,13 +5583,13 @@ v8-to-istanbul@^8.0.0:
source-map "^0.7.3"
v8-to-istanbul@^9.0.1:
- version "9.1.0"
- resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz#1b83ed4e397f58c85c266a570fc2558b5feb9265"
- integrity sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==
+ version "9.2.0"
+ resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz#2ed7644a245cddd83d4e087b9b33b3e62dfd10ad"
+ integrity sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==
dependencies:
"@jridgewell/trace-mapping" "^0.3.12"
"@types/istanbul-lib-coverage" "^2.0.1"
- convert-source-map "^1.6.0"
+ convert-source-map "^2.0.0"
valid-url@^1.0.9:
version "1.0.9"
@@ -5604,9 +5639,9 @@ webidl-conversions@^7.0.0:
integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
whatwg-fetch@^3.0.0, whatwg-fetch@^3.5.0:
- version "3.6.19"
- resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.19.tgz#caefd92ae630b91c07345537e67f8354db470973"
- integrity sha512-d67JP4dHSbm2TrpFj8AbO8DnL1JXL5J9u0Kq2xW6d0TFDbCA3Muhdt8orXC22utleTVj7Prqt82baN6RBvnEgw==
+ version "3.6.20"
+ resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70"
+ integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==
whatwg-url@^11.0.0:
version "11.0.0"
diff --git a/proto/cache.proto b/proto/cache.proto
index 87ae0e49e2..09c99dfaf2 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -172,12 +172,23 @@ message ChangeNotesStateProto {
repeated int32 past_reviewer = 12;
- // Next ID: 5
+ // Next ID: 8
message ReviewerStatusUpdateProto {
// Epoch millis.
int64 timestamp_millis = 1;
int32 updated_by = 2;
+
+ // Account ID of the reviewer.
+ // Not set if a reviewer for which no Gerrit account exists is added by email.
int32 reviewer = 3;
+ bool has_reviewer = 5;
+
+ // Address of the reviewer by email (can be "Full Name <full.name@example.com>" or
+ // "full.name@example.com").
+ // Only set for reviewers that have no Gerrit account and that have been added by email.
+ string reviewer_by_email = 6;
+ bool has_reviewer_by_email = 7;
+
string state = 4;
}
repeated ReviewerStatusUpdateProto reviewer_update = 13;
@@ -344,6 +355,7 @@ message AccountProto {
bool inactive = 6;
string status = 7;
string meta_id = 8;
+ string unique_tag = 9;
}
// Serialized form of com.google.gerrit.server.account.CachedAccountDetails.Key.
diff --git a/proto/entities.proto b/proto/entities.proto
index 3412291047..be04e971ce 100644
--- a/proto/entities.proto
+++ b/proto/entities.proto
@@ -31,7 +31,7 @@ message Change_Key {
}
// Serialized form of com.google.gerrit.entities.Change.
-// Next ID: 25
+// Next ID: 26
message Change {
required Change_Id change_id = 1;
optional Change_Key change_key = 2;
@@ -50,6 +50,7 @@ message Change {
optional bool review_started = 22;
optional Change_Id revert_of = 23;
optional PatchSet_Id cherry_pick_of = 24;
+ optional string server_id = 25;
// Deleted fields, should not be reused:
reserved 3; // row_version
@@ -62,6 +63,31 @@ message Change {
reserved 101; // note_db_state
}
+// Serialized form of com.google.gerrit.extensions.common.ChangeInput.
+// Next ID: 19
+message ChangeInput {
+ optional string project = 1;
+ optional string branch = 2;
+ optional string subject = 3;
+ optional string topic = 4;
+ optional ChangeStatus status = 5;
+ optional bool is_private = 6;
+ optional bool work_in_progress = 7;
+ optional string base_change = 8;
+ optional string base_commit = 9;
+ optional bool new_branch = 10;
+ map<string, string> validation_options = 11;
+ map<string, string> custom_keyed_values = 12;
+ optional MergeInput merge = 13;
+ optional ApplyPatchInput patch = 14;
+ optional AccountInput author = 15;
+ repeated ListChangesOption response_format_options = 16;
+ optional NotifyHandling notify = 17 [default = ALL];
+ // The key is the string representation of the RecipientType enum.
+ // We use a string here because proto does not allow enum keys in maps.
+ map<string, NotifyInfo> notify_details = 18;
+}
+
// Serialized form of com.google.gerrit.enities.ChangeMessage.
// Next ID: 3
message ChangeMessage_Key {
@@ -81,6 +107,97 @@ message ChangeMessage {
optional Account_Id real_author = 7;
}
+// Serialized form of com.google.gerrit.extensions.client.ChangeStatus.
+// Next ID: 3
+enum ChangeStatus {
+ NEW = 0;
+ MERGED = 1;
+ ABANDONED = 2;
+}
+
+// Serialized form of com.google.gerrit.extensions.common.MergeInput.
+// Next ID: 5
+message MergeInput {
+ optional string source = 1;
+ optional string source_branch = 2;
+ optional string strategy = 3;
+ optional bool allow_conflicts = 4;
+}
+
+// Serialized form of com.google.gerrit.extensions.api.changes.ApplyPatchInput.
+// Next ID: 2
+message ApplyPatchInput {
+ optional string patch = 1;
+}
+
+// Serialized form of com.google.gerrit.extensions.api.accounts.AccountInput.
+// Next ID: 8
+message AccountInput {
+ optional string username = 1;
+ optional string name = 2;
+ optional string display_name = 3;
+ optional string email = 4;
+ optional string ssh_key = 5;
+ optional string http_password = 6;
+ repeated string groups = 7;
+}
+
+// Serialized form of com.google.gerrit.extensions.client.ListChangesOption.
+// Next ID: 28
+enum ListChangesOption {
+ LABELS = 0;
+ CURRENT_REVISION = 1;
+ ALL_REVISIONS = 2;
+ CURRENT_COMMIT = 3;
+ ALL_COMMITS = 4;
+ CURRENT_FILES = 5;
+ ALL_FILES = 6;
+ DETAILED_ACCOUNTS = 7;
+ DETAILED_LABELS = 8;
+ MESSAGES = 9;
+ CURRENT_ACTIONS = 10;
+ REVIEWED = 11;
+ DRAFT_COMMENTS = 12;
+ DOWNLOAD_COMMANDS = 13;
+ WEB_LINKS = 14;
+ CHECK = 15;
+ CHANGE_ACTIONS = 16;
+ COMMIT_FOOTERS = 17;
+ PUSH_CERTIFICATES = 18;
+ REVIEWER_UPDATES = 19;
+ SUBMITTABLE = 20;
+ TRACKING_IDS = 21;
+ SKIP_MERGEABLE = 22;
+ SKIP_DIFFSTAT = 23;
+ SUBMIT_REQUIREMENTS = 24;
+ CUSTOM_KEYED_VALUES = 25;
+ STAR = 26;
+ PARENTS = 27;
+}
+
+// Serialized form of com.google.gerrit.extensions.api.changes.NotifyHandling.
+// Next ID: 4
+enum NotifyHandling {
+ NONE = 0;
+ OWNER = 1;
+ OWNER_REVIEWERS = 2;
+ ALL = 3;
+}
+
+// Serialized form of com.google.gerrit.extensions.api.changes.RecipientType.
+// Next ID: 3
+enum RecipientType {
+ TO = 0;
+ CC = 1;
+ BCC = 2;
+}
+
+// Serialized form of com.google.gerrit.extensions.api.changes.NotifyInfo.
+// Next ID: 2
+message NotifyInfo {
+ repeated string accounts = 1;
+}
+
// Serialized form of com.google.gerrit.entities.PatchSet.Id.
// Next ID: 3
message PatchSet_Id {
@@ -311,3 +428,64 @@ message UserPreferences {
}
optional EditPreferencesInfo edit_preferences_info = 3;
}
+
+// Next Id: 13
+message HumanComment {
+ // Required. Note that the equivalent Java struct does not contain the change
+ // ID, so we keep the same format here.
+ optional int32 patchset_id = 1;
+ optional ObjectId dest_commit_id = 2;
+ // Required.
+ optional Account_Id account_id = 3;
+ optional Account_Id real_author = 4;
+
+ // Next Id: 5
+ message InFilePosition {
+ optional string file_path = 1;
+ enum Side {
+ // Should match the logic in
+ // http://google3/third_party/java_src/gerritcodereview/gerrit/java/com/google/gerrit/extensions/client/Side.java?rcl=579772037&l=24
+ PARENT = 0;
+ REVISION = 1;
+ }
+ // Default should match
+ // http://google3/third_party/java_src/gerritcodereview/gerrit/Documentation/rest-api-changes.txt?l=7423
+ optional Side side = 2 [default = REVISION];
+ message Range {
+ // 1-based
+ optional int32 start_line = 1;
+ // 0-based
+ optional int32 start_char = 2;
+ // 1-based
+ optional int32 end_line = 3;
+ // 0-based
+ optional int32 end_char = 4;
+ }
+ // If neither range nor line number set, the comment is on the file level. It is possible
+ // (though not required) for both values to be set. in this case, it is expected that the line
+ // number is identical to the range's end line.
+ optional Range position_range = 3;
+ // 1-based
+ optional int32 line_number = 4;
+ }
+
+ // If not set, the comment is on the patchset level.
+ optional InFilePosition in_file_position = 5;
+
+ // Required.
+ optional string comment_text = 6;
+ // Might be set by the user while creating the draft.
+ // See http://go/gerrit-rest-api-change#comment-info.
+ optional string tag = 7;
+ optional bool unresolved = 8 [default = false];
+
+ // Required.
+ optional string comment_uuid = 9;
+ // Required.
+ optional string parent_comment_uuid = 10;
+
+ // Required. Epoch millis.
+ optional fixed64 written_on_millis = 11;
+ // Required.
+ optional string server_id = 12;
+} \ No newline at end of file
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index b748ba583b..5ff1822cd9 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -57,7 +57,7 @@
{/if}
{rb};
window.PRELOADED_QUERIES = {lb}
- {if $userIsAuthenticated and $defaultDashboardHex and $dashboardQuery}
+ {if $userIsAuthenticated && $defaultDashboardHex && $dashboardQuery}
dashboardQuery: [{for $query in $dashboardQuery}{$query},{/for}],
{/if}
{rb};
@@ -103,7 +103,7 @@
<link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/drafts?enable-context=true&context-padding=3" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
{/if}
{/if}
- {if $userIsAuthenticated and $defaultDashboardHex and $dashboardQuery}
+ {if $userIsAuthenticated && $defaultDashboardHex && $dashboardQuery}
<link rel="preload" href="{$canonicalPath}/changes/?O={$defaultDashboardHex}&S=0{for $query in $dashboardQuery}&q={$query}{/for}&allow-incomplete-results=true" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
{/if}
@@ -147,7 +147,7 @@
// CC them on any changes that load content before gr-app.js.
//
// github.com/Polymer/polymer-resin/blob/master/getting-started.md#integrating
- {if $assetsPath and $assetsBundle}
+ {if $assetsPath && $assetsBundle}
<link rel="import" href="{$assetsPath}/{$assetsBundle}">{\n}
{/if}
diff --git a/resources/com/google/gerrit/server/commit-msg_test.sh b/resources/com/google/gerrit/server/commit-msg_test.sh
index c537a89b04..8434a8e6a8 100755
--- a/resources/com/google/gerrit/server/commit-msg_test.sh
+++ b/resources/com/google/gerrit/server/commit-msg_test.sh
@@ -146,9 +146,9 @@ EOF
fi
}
-function test_suppress_squash {
+function suppress_squash_like {
cat << EOF > input
-squash! bla bla
+$1! bla bla
EOF
${hook} input || fail "failed hook execution"
@@ -158,6 +158,30 @@ EOF
fi
}
+function test_suppress_squash {
+ # test for standard git prefixes
+ suppress_squash_like squash
+ suppress_squash_like fixup
+ suppress_squash_like amend
+ # test for custom prefixes
+ suppress_squash_like temp
+ suppress_squash_like nopush
+}
+
+function test_always_create {
+ cat << EOF > input
+squash! bla bla
+EOF
+
+ git config gerrit.createChangeId always
+ ${hook} input || fail "failed hook execution"
+ git config --unset gerrit.createChangeId
+ found=$(grep -c '^Change-Id' input || true)
+ if [[ "${found}" != "1" ]]; then
+ fail "got ${found} Change-Ids, want 1"
+ fi
+}
+
# gerrit.reviewUrl causes us to create Link instead of Change-Id.
function test_link {
cat << EOF > input
diff --git a/resources/com/google/gerrit/server/mail/ChangeFooter.soy b/resources/com/google/gerrit/server/mail/ChangeFooter.soy
index c5b9e4a13b..2910537e14 100644
--- a/resources/com/google/gerrit/server/mail/ChangeFooter.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeFooter.soy
@@ -34,7 +34,7 @@
visit {$email.settingsUrl}{\n}
{/if}
- {if $email.changeUrl or $email.settingsUrl}
+ {if $email.changeUrl || $email.settingsUrl}
{\n}
{/if}
{/template}
diff --git a/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy b/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
index 5a22acb99c..62358ee391 100644
--- a/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
@@ -19,13 +19,13 @@
{template ChangeFooterHtml}
{@param change: ?}
{@param email: ?}
- {if $email.changeUrl or $email.settingsUrl}
+ {if $email.changeUrl || $email.settingsUrl}
<p>
{if $email.changeUrl}
To view, visit{sp}
<a href="{$email.changeUrl}">change {$change.changeNumber}</a>.
{/if}
- {if $email.changeUrl and $email.settingsUrl}{sp}{/if}
+ {if $email.changeUrl && $email.settingsUrl}{sp}{/if}
{if $email.settingsUrl}
To unsubscribe, or for help writing mail filters,{sp}
visit <a href="{$email.settingsUrl}">settings</a>.
diff --git a/resources/com/google/gerrit/server/mail/ChangeHeader.soy b/resources/com/google/gerrit/server/mail/ChangeHeader.soy
index 12b68b6f8e..cf1eb93839 100644
--- a/resources/com/google/gerrit/server/mail/ChangeHeader.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeHeader.soy
@@ -17,8 +17,8 @@
{namespace com.google.gerrit.server.mail.template.ChangeHeader}
{template ChangeHeader kind="text"}
- {@param attentionSet: list<string>|null}
- {if $attentionSet and length($attentionSet) > 0}
+ {@param? attentionSet: list<string>|null}
+ {if $attentionSet && length($attentionSet) > 0}
Attention is currently required from:{sp}
{for $attentionSetUser, $index in $attentionSet}
{$attentionSetUser}
diff --git a/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy b/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
index e17e0215bc..4b6a1581e9 100644
--- a/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
@@ -18,8 +18,8 @@
{namespace com.google.gerrit.server.mail.template.ChangeHeaderHtml}
{template ChangeHeaderHtml}
- {@param attentionSet: list<string>|null}
- {if $attentionSet and length($attentionSet) > 0}
+ {@param? attentionSet: list<string>|null}
+ {if $attentionSet && length($attentionSet) > 0}
<p> Attention is currently required from:{sp}
{for $attentionSetUser, $index in $attentionSet}
{$attentionSetUser}
diff --git a/resources/com/google/gerrit/server/mail/ChangeSubject.soy b/resources/com/google/gerrit/server/mail/ChangeSubject.soy
index ba422a418e..26037972ba 100644
--- a/resources/com/google/gerrit/server/mail/ChangeSubject.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeSubject.soy
@@ -27,7 +27,7 @@
{@param instanceAndProjectName: ?}
{@param addInstanceNameInSubject: ?} /** boolean */
- {if not $addInstanceNameInSubject}
+ {if !$addInstanceNameInSubject}
[{$change.sizeBucket}] Change in {$shortProjectName}[{$branch.shortName}]: {$change
.shortSubject}
{else}
diff --git a/resources/com/google/gerrit/server/mail/NewChange.soy b/resources/com/google/gerrit/server/mail/NewChange.soy
index c5f34b4724..7e2af28311 100644
--- a/resources/com/google/gerrit/server/mail/NewChange.soy
+++ b/resources/com/google/gerrit/server/mail/NewChange.soy
@@ -27,7 +27,7 @@
{@param ownerName: ?}
{@param patchSet: ?}
{@param projectName: ?}
- {if $email.reviewerNames or $email.removedReviewerNames}
+ {if $email.reviewerNames || $email.removedReviewerNames}
{if $email.reviewerNames}
Hello{sp}
{for $reviewerName, $index in $email.reviewerNames}
diff --git a/resources/com/google/gerrit/server/mail/NewChangeHtml.soy b/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
index 008226fda2..fac3c3da88 100644
--- a/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
+++ b/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
@@ -26,7 +26,7 @@ import * as mailTemplate from 'com/google/gerrit/server/mail/Private.soy';
{@param patchSet: ?}
{@param projectName: ?}
<p>
- {if $email.reviewerNames or $email.removedReviewerNames}
+ {if $email.reviewerNames || $email.removedReviewerNames}
{if $email.reviewerNames}
{$fromName} would like{sp}
{for $reviewerName, $index in $email.reviewerNames}
diff --git a/resources/com/google/gerrit/server/mail/ReplacePatchSet.soy b/resources/com/google/gerrit/server/mail/ReplacePatchSet.soy
index 6ae8625a23..9b9d1ad64a 100644
--- a/resources/com/google/gerrit/server/mail/ReplacePatchSet.soy
+++ b/resources/com/google/gerrit/server/mail/ReplacePatchSet.soy
@@ -30,7 +30,7 @@
{@param unsatisfiedSubmitRequirements: ?}
{@param oldSubmitRequirements: ?}
{@param newSubmitRequirements: ?}
- {if $email.reviewerNames and $fromEmail == $change.ownerEmail}
+ {if $email.reviewerNames && $fromEmail == $change.ownerEmail}
Hello{sp}
{for $reviewerName in $email.reviewerNames}
{$reviewerName},{sp}
@@ -53,7 +53,7 @@
{/if}.
{if $email.changeUrl} ( {$email.changeUrl} ){/if}
{/if}{\n}
- {if $email.outdatedApprovals and length($email.outdatedApprovals) > 0}
+ {if $email.outdatedApprovals && length($email.outdatedApprovals) > 0}
{\n}
The following approvals got outdated and were removed:{\n}
{for $outdatedApproval, $index in $email.outdatedApprovals}
diff --git a/resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy b/resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy
index 1d99591ed3..30a5dbfcbf 100644
--- a/resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy
+++ b/resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy
@@ -44,7 +44,7 @@ import * as mailTemplate from 'com/google/gerrit/server/mail/Private.soy';
</p>
{/if}
- {if $email.outdatedApprovals and length($email.outdatedApprovals) > 0}
+ {if $email.outdatedApprovals && length($email.outdatedApprovals) > 0}
<p>
The following approvals got outdated and were removed:{\n}
{for $outdatedApproval, $index in $email.outdatedApprovals}
diff --git a/resources/com/google/gerrit/server/mime/mime-types.properties b/resources/com/google/gerrit/server/mime/mime-types.properties
index 2626059166..642ef474a5 100644
--- a/resources/com/google/gerrit/server/mime/mime-types.properties
+++ b/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -17,6 +17,7 @@ BUILD = text/x-python
bazel = text/x-python
c = text/x-csrc
cfg = text/x-ttcn-cfg
+cjs = text/javascript
cl = text/x-common-lisp
clj = text/x-clojure
cljs = text/x-clojurescript
@@ -36,6 +37,7 @@ cs = text/x-csharp
csharp = text/x-csharp
csproj = application/xml
css = text/css
+cts = application/typescript
cpp = text/x-c++src
cql = text/x-cassandra
cxx = text/x-c++src
@@ -122,6 +124,7 @@ kt = text/x-kotlin
kts = text/x-kotlin
less = text/x-less
lhs = text/x-literate-haskell
+LICENSE = text/plain
lisp = text/x-common-lisp
list = text/plain
log = text/plain
@@ -148,6 +151,7 @@ msc = text/x-mscgen
mscgen = text/x-mscgen
mscin = text/x-mscgen
msgenny = text/x-msgenny
+mts = application/typescript
nb = text/x-mathematica
nginx.conf = text/x-nginx-conf
nsh = text/x-nsis
@@ -189,6 +193,9 @@ pxd = text/x-cython
pxi = text/x-cython
PKGBUILD = text/x-sh
q = text/x-q
+qml = text/x-qml
+qmlproject = text/x-qml
+qrc = application/xml
r = text/r-src
rake = text/x-ruby
rb = text/x-ruby
diff --git a/resources/com/google/gerrit/server/tools/root/hooks/commit-msg b/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
index 5c7dffa852..13aa86c76c 100755
--- a/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+++ b/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
@@ -30,14 +30,20 @@ if test ! -f "$1" ; then
fi
# Do not create a change id if requested
-if test "false" = "$(git config --bool --get gerrit.createChangeId)" ; then
- exit 0
-fi
+case "$(git config --get gerrit.createChangeId)" in
+ false)
+ exit 0
+ ;;
+ always)
+ ;;
+ *)
+ # Do not create a change id for squash/fixup commits.
+ if head -n1 "$1" | LC_ALL=C grep -q '^[a-z][a-z]*! '; then
+ exit 0
+ fi
+ ;;
+esac
-# Do not create a change id for squash commits.
-if head -n1 "$1" | grep -q '^squash! '; then
- exit 0
-fi
if git rev-parse --verify HEAD >/dev/null 2>&1; then
refhash="$(git rev-parse HEAD)"
diff --git a/tools/BUILD b/tools/BUILD
index cb25c471f0..71ad096e55 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -18,17 +18,20 @@ default_java_toolchain(
visibility = ["//visibility:public"],
)
-default_java_toolchain(
- name = "error_prone_warnings_toolchain_java17",
+[default_java_toolchain(
+ name = "error_prone_warnings_toolchain_java" + VERSION,
configuration = dict(),
- java_runtime = "@bazel_tools//tools/jdk:remotejdk_17",
+ java_runtime = "@rules_java//toolchains:remotejdk_" + VERSION,
package_configuration = [
":error_prone",
],
- source_version = "17",
- target_version = "17",
+ source_version = VERSION,
+ target_version = VERSION,
visibility = ["//visibility:public"],
-)
+) for VERSION in [
+ "17",
+ "21",
+]]
# Error Prone errors enabled by default; see ../.bazelrc for how this is
# enabled. This warnings list is originally based on:
@@ -76,7 +79,7 @@ java_package_configuration(
"-Xep:BadImport:ERROR",
"-Xep:BadInstanceof:ERROR",
"-Xep:BadShiftAmount:ERROR",
- "-Xep:BanJNDI:OFF",
+ "-Xep:BanJNDI:WARN",
"-Xep:BanSerializableRead:ERROR",
"-Xep:BigDecimalEquals:ERROR",
"-Xep:BigDecimalLiteralDouble:ERROR",
@@ -159,7 +162,7 @@ java_package_configuration(
"-Xep:FloatingPointLiteralPrecision:ERROR",
"-Xep:FloggerArgumentToString:ERROR",
"-Xep:FloggerFormatString:ERROR",
- "-Xep:FloggerLogString:OFF",
+ "-Xep:FloggerLogString:WARN",
"-Xep:FloggerLogVarargs:ERROR",
"-Xep:FloggerSplitLogStatement:ERROR",
"-Xep:FloggerStringConcatenation:ERROR",
@@ -202,7 +205,7 @@ java_package_configuration(
"-Xep:InjectOnConstructorOfAbstractClass:ERROR",
"-Xep:InheritDoc:ERROR",
"-Xep:InlineFormatString:ERROR",
- "-Xep:InlineMeInliner:OFF",
+ "-Xep:InlineMeInliner:WARN",
"-Xep:InlineMeSuggester:ERROR",
"-Xep:InlineMeValidator:ERROR",
"-Xep:InputStreamSlowMultibyteRead:ERROR",
@@ -290,7 +293,8 @@ java_package_configuration(
"-Xep:MultipleParallelOrSequentialCalls:ERROR",
"-Xep:MultipleUnaryOperatorsInMethodCall:ERROR",
"-Xep:MustBeClosedChecker:ERROR",
- "-Xep:MutableConstantField:OFF",
+ "-Xep:MutableConstantField:WARN",
+ "-Xep:MutableGuiceModule:ERROR",
"-Xep:MutablePublicArray:ERROR",
"-Xep:NCopiesOfChar:ERROR",
"-Xep:NarrowingCompoundAssignment:ERROR",
@@ -332,6 +336,7 @@ java_package_configuration(
"-Xep:PeriodTimeMath:ERROR",
"-Xep:PreconditionsCheckNotNullRepeated:ERROR",
"-Xep:PreconditionsInvalidPlaceholder:ERROR",
+ "-Xep:PreferredInterfaceType:ERROR",
"-Xep:PrimitiveAtomicReference:ERROR",
"-Xep:PrivateSecurityContractProtoAccess:ERROR",
"-Xep:ProtectedMembersInFinalClass:ERROR",
@@ -343,7 +348,7 @@ java_package_configuration(
"-Xep:ProtoTimestampGetSecondsGetNano:ERROR",
"-Xep:ProtoTruthMixedDescriptors:ERROR",
"-Xep:ProtocolBufferOrdinal:ERROR",
- "-Xep:ProvidesMethodOutsideOfModule:OFF",
+ "-Xep:ProvidesMethodOutsideOfModule:WARN",
"-Xep:RandomCast:ERROR",
"-Xep:RandomModInteger:ERROR",
"-Xep:ReachabilityFenceUsage:ERROR",
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index 9e515e5e00..5bacec8e15 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -1,6 +1,10 @@
+"""
+Build rules for plugins.
+"""
+
+load("//:version.bzl", "GERRIT_VERSION")
load("@rules_java//java:defs.bzl", "java_binary", "java_library")
load("//tools/bzl:genrule2.bzl", "genrule2")
-load("//:version.bzl", "GERRIT_VERSION")
IN_TREE_BUILD_MODE = True
diff --git a/tools/defs.bzl b/tools/defs.bzl
new file mode 100644
index 0000000000..ff207b38a7
--- /dev/null
+++ b/tools/defs.bzl
@@ -0,0 +1,13 @@
+load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
+
+def gerrit_init():
+ """
+ Initialize the WORKSPACE for gerrit targets
+ """
+ protobuf_deps()
+
+ native.register_toolchains("//tools:error_prone_warnings_toolchain_java11_definition")
+
+ native.register_toolchains("//tools:error_prone_warnings_toolchain_java17_definition")
+
+ native.register_toolchains("//tools:error_prone_warnings_toolchain_java21_definition")
diff --git a/tools/deps.bzl b/tools/deps.bzl
index 43e8d2a576..d056483891 100644
--- a/tools/deps.bzl
+++ b/tools/deps.bzl
@@ -1,3 +1,7 @@
+"""
+This module lists the external dependencies of the Gerrit project.
+"""
+
load("//tools/bzl:maven_jar.bzl", "GERRIT", "maven_jar")
CAFFEINE_VERS = "2.9.2"
@@ -9,21 +13,24 @@ MIME4J_VERS = "0.8.1"
OW2_VERS = "9.2"
AUTO_COMMON_VERSION = "1.2.1"
AUTO_FACTORY_VERSION = "1.0.1"
-AUTO_VALUE_VERSION = "1.7.4"
+AUTO_VALUE_VERSION = "1.10.4"
AUTO_VALUE_GSON_VERSION = "1.3.1"
PROLOG_VERS = "1.4.4"
PROLOG_REPO = GERRIT
-GITILES_VERS = "1.3.0"
+GITILES_VERS = "1.4.0"
GITILES_REPO = GERRIT
# When updating Bouncy Castle, also update it in bazlets.
BC_VERS = "1.72"
-HTTPCOMP_VERS = "4.5.2"
+HTTPCOMP_VERS = "4.5.14"
JETTY_VERS = "9.4.53.v20231009"
BYTE_BUDDY_VERSION = "1.14.9"
ROARING_BITMAP_VERSION = "0.9.44"
def java_dependencies():
+ """
+ This method lists the maven jars used in the Gerrit project.
+ """
maven_jar(
name = "java-runtime",
artifact = "org.antlr:antlr-runtime:" + ANTLR_VERS,
@@ -296,13 +303,13 @@ def java_dependencies():
maven_jar(
name = "auto-value",
artifact = "com.google.auto.value:auto-value:" + AUTO_VALUE_VERSION,
- sha1 = "6b126cb218af768339e4d6e95a9b0ae41f74e73d",
+ sha1 = "90f9629eaa123f88551cc26a64bc386967ee24cc",
)
maven_jar(
name = "auto-value-annotations",
artifact = "com.google.auto.value:auto-value-annotations:" + AUTO_VALUE_VERSION,
- sha1 = "eff48ed53995db2dadf0456426cc1f8700136f86",
+ sha1 = "9679de8286eb0a151db6538ba297a8951c4a1224",
)
maven_jar(
@@ -391,14 +398,14 @@ def java_dependencies():
artifact = "com.google.gitiles:blame-cache:" + GITILES_VERS,
attach_source = False,
repository = GITILES_REPO,
- sha1 = "d0f5c98207648503b225501e84f529fa88651ebe",
+ sha1 = "005e9a8cfcfc15f232c796dbf1c8fb5499abff9c",
)
maven_jar(
name = "gitiles-servlet",
artifact = "com.google.gitiles:gitiles-servlet:" + GITILES_VERS,
repository = GITILES_REPO,
- sha1 = "b4ce5bc26e6a2674728d0d3c72c21e0b3443666d",
+ sha1 = "6fa0fe70154d09799ff7dc616727fec7342bb755",
)
maven_jar(
@@ -446,19 +453,19 @@ def java_dependencies():
maven_jar(
name = "fluent-hc",
artifact = "org.apache.httpcomponents:fluent-hc:" + HTTPCOMP_VERS,
- sha1 = "7bfdfa49de6d720ad3c8cedb6a5238eec564dfed",
+ sha1 = "81a16abc0d5acb5016d5b46d4b197b53c3d6eb93",
)
maven_jar(
name = "httpclient",
artifact = "org.apache.httpcomponents:httpclient:" + HTTPCOMP_VERS,
- sha1 = "733db77aa8d9b2d68015189df76ab06304406e50",
+ sha1 = "1194890e6f56ec29177673f2f12d0b8e627dec98",
)
maven_jar(
name = "httpcore",
- artifact = "org.apache.httpcomponents:httpcore:4.4.4",
- sha1 = "b31526a230871fbe285fbcbe2813f9c0839ae9b0",
+ artifact = "org.apache.httpcomponents:httpcore:4.4.16",
+ sha1 = "51cf043c87253c9f58b539c9f7e44c8894223850",
)
# Test-only dependencies below.
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index 9e54e7f5fe..a3f4d8fad3 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -238,8 +238,7 @@ def gen_classpath(ext):
if p.endswith('libquery_parser.jar') or \
p.endswith('libgerrit-prolog-common.jar') or \
p.endswith('external/com_google_protobuf/java/core/libcore.jar') or \
- p.endswith('external/com_google_protobuf/java/core/liblite.jar') or \
- p.endswith('lucene-core-and-backward-codecs-merged_deploy.jar'):
+ p.endswith('external/com_google_protobuf/java/core/liblite.jar'):
lib.add(p)
if proto_library.match(p) :
proto.add(p)
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index 5e7ddb97c4..04ee733fbc 100644
--- a/tools/maven/gerrit-acceptance-framework_pom.xml
+++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-acceptance-framework</artifactId>
- <version>3.9.3-SNAPSHOT</version>
+ <version>3.10.0-rc0</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Acceptance Test Framework</name>
<description>Framework for Gerrit's acceptance tests</description>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index 95a911a55b..9822666fdd 100644
--- a/tools/maven/gerrit-extension-api_pom.xml
+++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-extension-api</artifactId>
- <version>3.9.3-SNAPSHOT</version>
+ <version>3.10.0-rc0</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Extension API</name>
<description>API for Gerrit Extensions</description>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index 1823f72a80..738963c638 100644
--- a/tools/maven/gerrit-plugin-api_pom.xml
+++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-plugin-api</artifactId>
- <version>3.9.3-SNAPSHOT</version>
+ <version>3.10.0-rc0</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Plugin API</name>
<description>API for Gerrit Plugins</description>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index 499ad32b59..e3f09a954b 100644
--- a/tools/maven/gerrit-war_pom.xml
+++ b/tools/maven/gerrit-war_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-war</artifactId>
- <version>3.9.3-SNAPSHOT</version>
+ <version>3.10.0-rc0</version>
<packaging>war</packaging>
<name>Gerrit Code Review - WAR</name>
<description>Gerrit WAR</description>
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index cb200dc7c4..ac3f668c59 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -1,13 +1,56 @@
+"""
+Dependencies that are exempted from requiring a Library-Compliance approval
+from a Googler.
+"""
+
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe")
load("//tools/bzl:maven_jar.bzl", "maven_jar")
-GUAVA_VERSION = "32.1.2-jre"
+GUAVA_VERSION = "33.0.0-jre"
-GUAVA_BIN_SHA1 = "5e64ec7e056456bef3a4bc4c6fdaef71e8ab6318"
+GUAVA_BIN_SHA1 = "161ba27964a62f241533807a46b8711b13c1d94b"
-GUAVA_TESTLIB_BIN_SHA1 = "c7a8a2c91b6809ff46373b1bc06185241801f6b5"
+GUAVA_TESTLIB_BIN_SHA1 = "cf21e00fcc92786094fb5b376500f50d06878b0b"
GUAVA_DOC_URL = "https://google.github.io/guava/releases/" + GUAVA_VERSION + "/api/docs/"
+def archive_dependencies():
+ return [
+ {
+ "name": "com_google_protobuf",
+ "sha256": "9bd87b8280ef720d3240514f884e56a712f2218f0d693b48050c836028940a42",
+ "strip_prefix": "protobuf-25.1",
+ "urls": [
+ "https://github.com/protocolbuffers/protobuf/archive/v25.1.tar.gz",
+ ],
+ },
+ {
+ "name": "platforms",
+ "urls": [
+ "https://mirror.bazel.build/github.com/bazelbuild/platforms/releases/download/0.0.7/platforms-0.0.7.tar.gz",
+ "https://github.com/bazelbuild/platforms/releases/download/0.0.7/platforms-0.0.7.tar.gz",
+ ],
+ "sha256": "3a561c99e7bdbe9173aa653fd579fe849f1d8d67395780ab4770b1f381431d51",
+ },
+ {
+ "name": "rules_java",
+ "urls": [
+ "https://github.com/bazelbuild/rules_java/releases/download/7.3.1/rules_java-7.3.1.tar.gz",
+ ],
+ "sha256": "4018e97c93f97680f1650ffd2a7530245b864ac543fd24fae8c02ba447cb2864",
+ },
+ {
+ "name": "ubuntu2204_jdk17",
+ "strip_prefix": "rbe_autoconfig-5.1.0",
+ "urls": [
+ "https://gerrit-bazel.storage.googleapis.com/rbe_autoconfig/v5.1.0.tar.gz",
+ "https://github.com/davido/rbe_autoconfig/releases/download/v5.1.0/v5.1.0.tar.gz",
+ ],
+ "sha256": "8ea82b81c9707e535ff93ef5349d11e55b2a23c62bcc3b0faaec052144aed87d",
+ },
+ ]
+
def declare_nongoogle_deps():
"""loads dependencies that are not used at Google.
@@ -16,10 +59,15 @@ def declare_nongoogle_deps():
enforced by //lib:nongoogle_test.
"""
+ for dependency in archive_dependencies():
+ params = {}
+ params.update(**dependency)
+ maybe(http_archive, params.pop("name"), **params)
+
maven_jar(
name = "log4j",
- artifact = "ch.qos.reload4j:reload4j:1.2.19",
- sha1 = "4eae9978468c5e885a6fb44df7e2bbc07a20e6ce",
+ artifact = "ch.qos.reload4j:reload4j:1.2.25",
+ sha1 = "45921e383a1001c2a599fc4c6cf59af80cdd1cf1",
)
SLF4J_VERS = "1.7.36"
@@ -200,8 +248,8 @@ def declare_nongoogle_deps():
# Keep this version of Soy synchronized with the version used in Gitiles.
maven_jar(
name = "soy",
- artifact = "com.google.template:soy:2022-07-20",
- sha1 = "f64eb90da6d91beddf11653865c90f26d26710cf",
+ artifact = "com.google.template:soy:2024-01-30",
+ sha1 = "6e9ccb00926325c7a9293ed05a2eaf56ea15d60e",
)
# Test-only dependencies below.
@@ -223,62 +271,62 @@ def declare_nongoogle_deps():
sha1 = "48462eb319817c90c27d377341684b6b81372e08",
)
- TRUTH_VERS = "1.1"
+ TRUTH_VERS = "1.4.2"
maven_jar(
name = "truth",
artifact = "com.google.truth:truth:" + TRUTH_VERS,
- sha1 = "6a096a16646559c24397b03f797d0c9d75ee8720",
+ sha1 = "2322d861290bd84f84cbb178e43539725a4588fd",
)
maven_jar(
name = "truth-java8-extension",
artifact = "com.google.truth.extensions:truth-java8-extension:" + TRUTH_VERS,
- sha1 = "258db6eb8df61832c5c059ed2bc2e1c88683e92f",
+ sha1 = "bfa44a01e1bb5a1df50bc9c678d6588b4d9eb73a",
)
maven_jar(
name = "truth-liteproto-extension",
artifact = "com.google.truth.extensions:truth-liteproto-extension:" + TRUTH_VERS,
- sha1 = "bf65afa13aa03330e739bcaa5d795fe0f10fbf20",
+ sha1 = "062a2716b3b0ba9d8e72c913dad43a8139b12202",
)
maven_jar(
name = "truth-proto-extension",
artifact = "com.google.truth.extensions:truth-proto-extension:" + TRUTH_VERS,
- sha1 = "64cba89cf87c1d84cb8c81d06f0b9c482f10b4dc",
+ sha1 = "53cfc94dfa435c5dcd6f8b6844b82b423ea0a5af",
)
- LUCENE_VERS = "8.11.2"
+ LUCENE_VERS = "9.8.0"
maven_jar(
name = "lucene-core",
artifact = "org.apache.lucene:lucene-core:" + LUCENE_VERS,
- sha1 = "57438c3f31e0e440de149294890eee88e030ea6d",
+ sha1 = "5e8421c5f8573bcf22e9265fc7e19469545a775a",
)
maven_jar(
name = "lucene-analyzers-common",
- artifact = "org.apache.lucene:lucene-analyzers-common:" + LUCENE_VERS,
- sha1 = "07a74c5c2dd082b08c644a9016bc6ff66c8f27cc",
+ artifact = "org.apache.lucene:lucene-analysis-common:" + LUCENE_VERS,
+ sha1 = "36f0363325ca7bf62c180160d1ed5165c7c37795",
)
maven_jar(
- name = "backward-codecs",
+ name = "lucene-backward-codecs",
artifact = "org.apache.lucene:lucene-backward-codecs:" + LUCENE_VERS,
- sha1 = "a5d0f0db405d607cc13265819b8d2ef0c81c0819",
+ sha1 = "e98fb408028f40170e6d87c16422bfdc0bb2e392",
)
maven_jar(
name = "lucene-misc",
artifact = "org.apache.lucene:lucene-misc:" + LUCENE_VERS,
- sha1 = "9c7204f923465a96a20ac9e49cdca0cfcde64851",
+ sha1 = "9a57b049cf51a5e9c9c1909c420f645f1b6f9a54",
)
maven_jar(
name = "lucene-queryparser",
artifact = "org.apache.lucene:lucene-queryparser:" + LUCENE_VERS,
- sha1 = "1886e3a27a8d4a73eb8fad54ea93a160b099bc60",
+ sha1 = "982faf2bfa55542bf57fbadef54c19ac00f57cae",
)
# JGit's transitive dependencies
diff --git a/tools/platforms/Dockerfile b/tools/platforms/Dockerfile
deleted file mode 100644
index 157529c08a..0000000000
--- a/tools/platforms/Dockerfile
+++ /dev/null
@@ -1,21 +0,0 @@
-# Copyright (C) 2021 The Android Open Source Project
-#
-# 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.
-
-FROM gcr.io/cloud-marketplace/google/rbe-ubuntu18-04:latest
-
-# Install Git >=2.18.0
-RUN add-apt-repository ppa:git-core/ppa && \
- apt-get -y update && \
- apt-get -y install git && \
- apt-get clean
diff --git a/tools/remote-bazelrc b/tools/remote-bazelrc
index d8da574151..d2a18bc3e6 100644
--- a/tools/remote-bazelrc
+++ b/tools/remote-bazelrc
@@ -31,11 +31,11 @@ build:remote_shared --remote_download_minimal
# Set several flags related to specifying the platform, toolchain and java
# properties.
-build:remote_shared --crosstool_top=@rbe_jdk11//cc:toolchain
-build:remote_shared --extra_toolchains=@rbe_jdk11//config:cc-toolchain
-build:remote_shared --extra_execution_platforms=@rbe_jdk11//config:platform
-build:remote_shared --host_platform=@rbe_jdk11//config:platform
-build:remote_shared --platforms=@rbe_jdk11//config:platform
+build:remote_shared --crosstool_top=@ubuntu2204_jdk17//cc:toolchain
+build:remote_shared --extra_toolchains=@ubuntu2204_jdk17//config:cc-toolchain
+build:remote_shared --extra_execution_platforms=@ubuntu2204_jdk17//config:platform
+build:remote_shared --host_platform=@ubuntu2204_jdk17//config:platform
+build:remote_shared --platforms=@ubuntu2204_jdk17//config:platform
build:remote_shared --action_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1
# Set various strategies so that all actions execute remotely. Mixing remote
diff --git a/tools/rules_nodejs-5.8.4-node_versions.bzl.patch b/tools/rules_nodejs-5.8.4-node_versions.bzl.patch
new file mode 100644
index 0000000000..7df62d6b06
--- /dev/null
+++ b/tools/rules_nodejs-5.8.4-node_versions.bzl.patch
@@ -0,0 +1,17 @@
+diff --git a/nodejs/private/node_versions.bzl b/nodejs/private/node_versions.bzl
+index bbb45b26..8758b3cc 100644
+--- a/nodejs/private/node_versions.bzl
++++ b/nodejs/private/node_versions.bzl
+@@ -2311,4 +2311,12 @@ NODE_VERSIONS = {
+ "18.17.0-linux_s390x": ("node-v18.17.0-linux-s390x.tar.xz", "node-v18.17.0-linux-s390x", "876ca54c246d24e346d0c740fbb72c9fb7353369127f20492bc923ee6d0121db"),
+ "18.17.0-linux_amd64": ("node-v18.17.0-linux-x64.tar.xz", "node-v18.17.0-linux-x64", "f36facda28c4d5ce76b3a1b4344e688d29d9254943a47f2f1909b1a10acb1959"),
+ "18.17.0-windows_amd64": ("node-v18.17.0-win-x64.zip", "node-v18.17.0-win-x64", "06e30b4e70b18d794651ef132c39080e5eaaa1187f938721d57edae2824f4e96"),
++ # 20.9.0
++ "20.9.0-darwin_arm64": ("node-v20.9.0-darwin-arm64.tar.gz", "node-v20.9.0-darwin-arm64", "31d2d46ae8d8a3982f54e2ff1e60c2e4a8e80bf78a3e8b46dcaac95ac5d7ce6a"),
++ "20.9.0-darwin_amd64": ("node-v20.9.0-darwin-x64.tar.gz", "node-v20.9.0-darwin-x64", "fc5b73f2a78c17bbe926cdb1447d652f9f094c79582f1be6471b4b38a2e1ccc8"),
++ "20.9.0-linux_arm64": ("node-v20.9.0-linux-arm64.tar.xz", "node-v20.9.0-linux-arm64", "ced3ecece4b7c3a664bca3d9e34a0e3b9a31078525283a6fdb7ea2de8ca5683b"),
++ "20.9.0-linux_ppc64le": ("node-v20.9.0-linux-ppc64le.tar.xz", "node-v20.9.0-linux-ppc64le", "3c6cea5d614cfbb95d92de43fbc2f8ecd66e431502fe5efc4f3c02637897bd45"),
++ "20.9.0-linux_s390x": ("node-v20.9.0-linux-s390x.tar.xz", "node-v20.9.0-linux-s390x", "af1f4e63756ff685d452166c4d5ba93a308e816ee7c46015b5e086163d9f011b"),
++ "20.9.0-linux_amd64": ("node-v20.9.0-linux-x64.tar.xz", "node-v20.9.0-linux-x64", "9033989810bf86220ae46b1381bdcdc6c83a0294869ba2ad39e1061f1e69217a"),
++ "20.9.0-windows_amd64": ("node-v20.9.0-win-x64.zip", "node-v20.9.0-win-x64", "70d87dad2378c63216ff83d5a754c61d2886fc39d32ce0d2ea6de763a22d3780"),
+ }
diff --git a/tools/setup_gjf.sh b/tools/setup_gjf.sh
index 119f9af3d7..8e2b57b0aa 100755
--- a/tools/setup_gjf.sh
+++ b/tools/setup_gjf.sh
@@ -32,6 +32,9 @@ case "$VERSION" in
1.7)
SHA1="b6d34a51e579b08db7c624505bdf9af4397f1702"
;;
+1.22.0)
+ SHA1="693d8fd04656886a2287cfe1d7a118c4697c3a57"
+ ;;
*)
echo "unknown google-java-format version: $VERSION"
exit 1
@@ -48,7 +51,7 @@ dir="$root/tools/format"
mkdir -p "$dir"
name="google-java-format-$VERSION-all-deps.jar"
-url="https://github.com/google/google-java-format/releases/download/google-java-format-$VERSION/$name"
+url="https://github.com/google/google-java-format/releases/download/v$VERSION/$name"
"$root/tools/download_file.py" -o "$dir/$name" -u "$url" -v "$SHA1"
launcher="$dir/google-java-format-$VERSION"
diff --git a/tools/util.py b/tools/util.py
index 947e2c0a0f..aed9e91df2 100644
--- a/tools/util.py
+++ b/tools/util.py
@@ -16,8 +16,10 @@ from os import path
REPO_ROOTS = {
'ECLIPSE': 'https://repo.eclipse.org/content/groups/releases',
+ 'ECLIPSE_EGIT': 'https://repo.eclipse.org/content/repositories/egit-releases',
'GERRIT': 'https://gerrit-maven.storage.googleapis.com',
'GERRIT_API': 'https://gerrit-api.commondatastorage.googleapis.com/release',
+ 'JENKINS': 'https://repo.jenkins-ci.org/artifactory/public',
'MAVEN_CENTRAL': 'https://repo1.maven.org/maven2',
'MAVEN_LOCAL': 'file://' + path.expanduser('~/.m2/repository'),
'MAVEN_SNAPSHOT': 'https://oss.sonatype.org/content/repositories/snapshots',
diff --git a/version.bzl b/version.bzl
index c5fca8bff0..7ff6c744d7 100644
--- a/version.bzl
+++ b/version.bzl
@@ -2,4 +2,4 @@
# Used by :api_install and :api_deploy targets
# when talking to the destination repository.
#
-GERRIT_VERSION = "3.9.3-SNAPSHOT"
+GERRIT_VERSION = "3.10.0-rc0"
diff --git a/yarn.lock b/yarn.lock
index 5702d39bcd..6d62a5e6ac 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -16,11 +16,11 @@
integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.11":
- version "7.22.13"
- resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e"
- integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==
+ version "7.23.5"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244"
+ integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==
dependencies:
- "@babel/highlight" "^7.22.13"
+ "@babel/highlight" "^7.23.4"
chalk "^2.4.2"
"@babel/helper-validator-identifier@^7.22.20":
@@ -28,19 +28,19 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
-"@babel/highlight@^7.22.13":
- version "7.22.20"
- resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54"
- integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==
+"@babel/highlight@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b"
+ integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==
dependencies:
"@babel/helper-validator-identifier" "^7.22.20"
chalk "^2.4.2"
js-tokens "^4.0.0"
"@babel/runtime@^7.10.2":
- version "7.22.15"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.15.tgz#38f46494ccf6cf020bd4eed7124b425e83e523b8"
- integrity sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==
+ version "7.23.7"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.7.tgz#dd7c88deeb218a0f8bd34d5db1aa242e0f203193"
+ integrity sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==
dependencies:
regenerator-runtime "^0.14.0"
@@ -209,14 +209,14 @@
eslint-visitor-keys "^3.3.0"
"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1":
- version "4.8.1"
- resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.8.1.tgz#8c4bb756cc2aa7eaf13cfa5e69c83afb3260c20c"
- integrity sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==
+ version "4.10.0"
+ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63"
+ integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==
-"@eslint/eslintrc@^2.1.2":
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396"
- integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==
+"@eslint/eslintrc@^2.1.4":
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad"
+ integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
@@ -228,17 +228,17 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
-"@eslint/js@8.49.0":
- version "8.49.0"
- resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.49.0.tgz#86f79756004a97fa4df866835093f1df3d03c333"
- integrity sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==
+"@eslint/js@8.56.0":
+ version "8.56.0"
+ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b"
+ integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==
-"@humanwhocodes/config-array@^0.11.11":
- version "0.11.11"
- resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844"
- integrity sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==
+"@humanwhocodes/config-array@^0.11.13":
+ version "0.11.13"
+ resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297"
+ integrity sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==
dependencies:
- "@humanwhocodes/object-schema" "^1.2.1"
+ "@humanwhocodes/object-schema" "^2.0.1"
debug "^4.1.1"
minimatch "^3.0.5"
@@ -247,10 +247,10 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
-"@humanwhocodes/object-schema@^1.2.1":
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
- integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
+"@humanwhocodes/object-schema@^2.0.1":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz#e5211452df060fa8522b55c7b3c0c4d1981cb044"
+ integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==
"@koa/cors@^3.4.3":
version "3.4.3"
@@ -373,41 +373,41 @@
picomatch "^2.2.2"
"@types/accepts@*":
- version "1.3.5"
- resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575"
- integrity sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==
+ version "1.3.7"
+ resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.7.tgz#3b98b1889d2b2386604c2bbbe62e4fb51e95b265"
+ integrity sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==
dependencies:
"@types/node" "*"
"@types/body-parser@*":
- version "1.19.3"
- resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.3.tgz#fb558014374f7d9e56c8f34bab2042a3a07d25cd"
- integrity sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==
+ version "1.19.5"
+ resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4"
+ integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==
dependencies:
"@types/connect" "*"
"@types/node" "*"
"@types/command-line-args@^5.0.0":
- version "5.2.1"
- resolved "https://registry.yarnpkg.com/@types/command-line-args/-/command-line-args-5.2.1.tgz#233bd1ba687e84ecbec0388e09f9ec9ebf63c55b"
- integrity sha512-U2OcmS2tj36Yceu+mRuPyUV0ILfau/h5onStcSCzqTENsq0sBiAp2TmaXu1k8fY4McLcPKSYl9FRzn2hx5bI+w==
+ version "5.2.3"
+ resolved "https://registry.yarnpkg.com/@types/command-line-args/-/command-line-args-5.2.3.tgz#553ce2fd5acf160b448d307649b38ffc60d39639"
+ integrity sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==
"@types/connect@*":
- version "3.4.36"
- resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.36.tgz#e511558c15a39cb29bd5357eebb57bd1459cd1ab"
- integrity sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==
+ version "3.4.38"
+ resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858"
+ integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==
dependencies:
"@types/node" "*"
"@types/content-disposition@*":
- version "0.5.6"
- resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.6.tgz#0f5fa03609f308a7a1a57e0b0afe4b95f1d19740"
- integrity sha512-GmShTb4qA9+HMPPaV2+Up8tJafgi38geFi7vL4qAM7k8BwjoelgHZqEUKJZLvughUw22h6vD/wvwN4IUCaWpDA==
+ version "0.5.8"
+ resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.8.tgz#6742a5971f490dc41e59d277eee71361fea0b537"
+ integrity sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==
"@types/cookies@*":
- version "0.7.8"
- resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.8.tgz#16fccd6d58513a9833c527701a90cc96d216bc18"
- integrity sha512-y6KhF1GtsLERUpqOV+qZJrjUGzc0GE6UTa0b5Z/LZ7Nm2mKSdCXmS6Kdnl7fctPNnMSouHjxqEWI12/YqQfk5w==
+ version "0.7.10"
+ resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.10.tgz#c4881dca4dd913420c488508d192496c46eb4fd0"
+ integrity sha512-hmUCjAk2fwZVPPkkPBcI7jGLIR5mg4OVoNMBwU6aVsMm/iNPY7z9/R+x2fSwLt/ZXoGua6C5Zy2k5xOo9jUyhQ==
dependencies:
"@types/connect" "*"
"@types/express" "*"
@@ -420,9 +420,9 @@
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
"@types/express-serve-static-core@^4.17.33":
- version "4.17.36"
- resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.36.tgz#baa9022119bdc05a4adfe740ffc97b5f9360e545"
- integrity sha512-zbivROJ0ZqLAtMzgzIUC4oNqDG9iF0lSsAqpOD9kbs5xcIM3dTiyuHvBc7R8MtWBp3AAWGaovJa+wzWPjLYW7Q==
+ version "4.17.41"
+ resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz#5077defa630c2e8d28aa9ffc2c01c157c305bef6"
+ integrity sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==
dependencies:
"@types/node" "*"
"@types/qs" "*"
@@ -430,9 +430,9 @@
"@types/send" "*"
"@types/express@*":
- version "4.17.17"
- resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4"
- integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d"
+ integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==
dependencies:
"@types/body-parser" "*"
"@types/express-serve-static-core" "^4.17.33"
@@ -440,19 +440,19 @@
"@types/serve-static" "*"
"@types/http-assert@*":
- version "1.5.3"
- resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.3.tgz#ef8e3d1a8d46c387f04ab0f2e8ab8cb0c5078661"
- integrity sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA==
+ version "1.5.5"
+ resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.5.tgz#dfb1063eb7c240ee3d3fe213dac5671cfb6a8dbf"
+ integrity sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==
"@types/http-errors@*":
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.2.tgz#a86e00bbde8950364f8e7846687259ffcd96e8c2"
- integrity sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f"
+ integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==
"@types/json-schema@^7.0.9":
- version "7.0.13"
- resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85"
- integrity sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==
+ version "7.0.15"
+ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
+ integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
"@types/json5@^0.0.29":
version "0.0.29"
@@ -460,21 +460,21 @@
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/keygrip@*":
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.3.tgz#2286b16ef71d8dea74dab00902ef419a54341bfe"
- integrity sha512-tfzBBb7OV2PbUfKbG6zRE5UbmtdLVCKT/XT364Z9ny6pXNbd9GnIB6aFYpq2A5lZ6mq9bhXgK6h5MFGNwhMmuQ==
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.6.tgz#1749535181a2a9b02ac04a797550a8787345b740"
+ integrity sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==
"@types/koa-compose@*":
- version "3.2.6"
- resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.6.tgz#17a077786d0ac5eee04c37a7d6c207b3252f6de9"
- integrity sha512-PHiciWxH3NRyAaxUdEDE1NIZNfvhgtPlsdkjRPazHC6weqt90Jr0uLhIQs+SDwC8HQ/jnA7UQP6xOqGFB7ugWw==
+ version "3.2.8"
+ resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.8.tgz#dec48de1f6b3d87f87320097686a915f1e954b57"
+ integrity sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==
dependencies:
"@types/koa" "*"
"@types/koa@*", "@types/koa@^2.11.6":
- version "2.13.9"
- resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.9.tgz#8d989ac17d7f033475fbe34c4f906c9287c2041a"
- integrity sha512-tPX3cN1dGrMn+sjCDEiQqXH2AqlPoPd594S/8zxwUm/ZbPsQXKqHPUypr2gjCPhHUc+nDJLduhh5lXI/1olnGQ==
+ version "2.13.12"
+ resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.12.tgz#70d87a9061a81909e0ee11ca50168416e8d3e795"
+ integrity sha512-vAo1KuDSYWFDB4Cs80CHvfmzSQWeUb909aQib0C0aFx4sw0K9UZFz2m5jaEP+b3X1+yr904iQiruS0hXi31jbw==
dependencies:
"@types/accepts" "*"
"@types/content-disposition" "*"
@@ -491,24 +491,26 @@
integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==
"@types/mime@*":
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"
- integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.4.tgz#2198ac274de6017b44d941e00261d5bc6a0e0a45"
+ integrity sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==
"@types/mime@^1":
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
- integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690"
+ integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==
"@types/minimist@^1.2.0":
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
- integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e"
+ integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==
"@types/node@*":
- version "20.6.3"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-20.6.3.tgz#5b763b321cd3b80f6b8dde7a37e1a77ff9358dd9"
- integrity sha512-HksnYH4Ljr4VQgEy2lTStbCKv/P590tmPe5HqOnv9Gprffgv5WXAY+Y5Gqniu0GGqeTCUdBnzC3QSrzPkBkAMA==
+ version "20.10.6"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.6.tgz#a3ec84c22965802bf763da55b2394424f22bfbb5"
+ integrity sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==
+ dependencies:
+ undici-types "~5.26.4"
"@types/node@^10.1.0":
version "10.17.60"
@@ -516,14 +518,14 @@
integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==
"@types/normalize-package-data@^2.4.0":
- version "2.4.1"
- resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
- integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
+ version "2.4.4"
+ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901"
+ integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==
-"@types/page@^1.11.6":
- version "1.11.6"
- resolved "https://registry.yarnpkg.com/@types/page/-/page-1.11.6.tgz#d531dc26067ca6a52e785db54c65b9095b9d5b84"
- integrity sha512-oOJysLQSd7rY3aqnEuPui0zJGQDQbwuwclwn9FQQVVome/U4oF2XK1SDp/1kWXhVlohey0zVr2LdV17y5fdLhg==
+"@types/page@^1.11.9":
+ version "1.11.9"
+ resolved "https://registry.yarnpkg.com/@types/page/-/page-1.11.9.tgz#a596cbcbb24bbe8a4292d56ac96c23895f7d44e5"
+ integrity sha512-Ki8IZMwg63i7+tF3UpfDIl4rwBN1B1kWQjZCUzaWoohfMB0m9CYap/dExbz7W21uS2WPoA/8lvlDuwX0X/YfIQ==
"@types/parse5@^6.0.1":
version "6.0.3"
@@ -531,14 +533,14 @@
integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==
"@types/qs@*":
- version "6.9.8"
- resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.8.tgz#f2a7de3c107b89b441e071d5472e6b726b4adf45"
- integrity sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==
+ version "6.9.11"
+ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.11.tgz#208d8a30bc507bd82e03ada29e4732ea46a6bbda"
+ integrity sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==
"@types/range-parser@*":
- version "1.2.4"
- resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
- integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
+ integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
"@types/resolve@1.17.1":
version "1.17.1"
@@ -548,22 +550,22 @@
"@types/node" "*"
"@types/semver@^7.3.12":
- version "7.5.2"
- resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.2.tgz#31f6eec1ed7ec23f4f05608d3a2d381df041f564"
- integrity sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==
+ version "7.5.6"
+ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339"
+ integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==
"@types/send@*":
- version "0.17.1"
- resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.1.tgz#ed4932b8a2a805f1fe362a70f4e62d0ac994e301"
- integrity sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==
+ version "0.17.4"
+ resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a"
+ integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==
dependencies:
"@types/mime" "^1"
"@types/node" "*"
"@types/serve-static@*":
- version "1.15.2"
- resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.2.tgz#3e5419ecd1e40e7405d34093f10befb43f63381a"
- integrity sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==
+ version "1.15.5"
+ resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.5.tgz#15e67500ec40789a1e8c9defc2d32a896f05b033"
+ integrity sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==
dependencies:
"@types/http-errors" "*"
"@types/mime" "*"
@@ -660,6 +662,11 @@
"@typescript-eslint/types" "5.62.0"
eslint-visitor-keys "^3.3.0"
+"@ungap/structured-clone@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
+ integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
+
"@web/config-loader@^0.1.3":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@web/config-loader/-/config-loader-0.1.3.tgz#8325ea54f75ef2ee7166783e64e66936db25bff7"
@@ -756,9 +763,9 @@ acorn-jsx@^5.3.2:
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn@^8.9.0:
- version "8.10.0"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
- integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==
+ version "8.11.3"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
+ integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
ajv@^6.12.4:
version "6.12.6"
@@ -847,7 +854,7 @@ array-buffer-byte-length@^1.0.0:
call-bind "^1.0.2"
is-array-buffer "^3.0.1"
-array-includes@^3.1.6:
+array-includes@^3.1.7:
version "3.1.7"
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda"
integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==
@@ -868,7 +875,7 @@ array-unique@^0.3.2:
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==
-array.prototype.findlastindex@^1.2.2:
+array.prototype.findlastindex@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz#b37598438f97b579166940814e2c0493a4f50207"
integrity sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==
@@ -879,7 +886,7 @@ array.prototype.findlastindex@^1.2.2:
es-shim-unscopables "^1.0.0"
get-intrinsic "^1.2.1"
-array.prototype.flat@^1.3.1:
+array.prototype.flat@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18"
integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==
@@ -889,7 +896,7 @@ array.prototype.flat@^1.3.1:
es-abstract "^1.22.1"
es-shim-unscopables "^1.0.0"
-array.prototype.flatmap@^1.3.1:
+array.prototype.flatmap@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527"
integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==
@@ -1026,13 +1033,14 @@ cache-content-type@^1.0.0:
mime-types "^2.1.18"
ylru "^1.2.0"
-call-bind@^1.0.0, call-bind@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
- integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
+call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.4, call-bind@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513"
+ integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==
dependencies:
- function-bind "^1.1.1"
- get-intrinsic "^1.0.2"
+ function-bind "^1.1.2"
+ get-intrinsic "^1.2.1"
+ set-function-length "^1.1.1"
call-me-maybe@^1.0.1:
version "1.0.2"
@@ -1211,9 +1219,9 @@ comment-parser@1.3.1:
integrity sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==
component-emitter@^1.2.1:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
- integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17"
+ integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==
concat-map@0.0.1:
version "0.0.1"
@@ -1232,10 +1240,10 @@ content-type@^1.0.4:
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
-cookies@~0.8.0:
- version "0.8.0"
- resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90"
- integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==
+cookies@~0.9.0:
+ version "0.9.1"
+ resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.9.1.tgz#3ffed6f60bb4fb5f146feeedba50acc418af67e3"
+ integrity sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==
dependencies:
depd "~2.0.0"
keygrip "~1.1.0"
@@ -1324,10 +1332,10 @@ deepmerge@^4.2.2:
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
-define-data-property@^1.0.1:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.0.tgz#0db13540704e1d8d479a0656cf781267531b9451"
- integrity sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==
+define-data-property@^1.0.1, define-data-property@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3"
+ integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==
dependencies:
get-intrinsic "^1.2.1"
gopd "^1.0.1"
@@ -1338,7 +1346,7 @@ define-lazy-prop@^2.0.0:
resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==
-define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0:
+define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c"
integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==
@@ -1477,25 +1485,25 @@ error-ex@^1.3.1:
is-arrayish "^0.2.1"
es-abstract@^1.22.1:
- version "1.22.2"
- resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.2.tgz#90f7282d91d0ad577f505e423e52d4c1d93c1b8a"
- integrity sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==
+ version "1.22.3"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.3.tgz#48e79f5573198de6dee3589195727f4f74bc4f32"
+ integrity sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==
dependencies:
array-buffer-byte-length "^1.0.0"
arraybuffer.prototype.slice "^1.0.2"
available-typed-arrays "^1.0.5"
- call-bind "^1.0.2"
+ call-bind "^1.0.5"
es-set-tostringtag "^2.0.1"
es-to-primitive "^1.2.1"
function.prototype.name "^1.1.6"
- get-intrinsic "^1.2.1"
+ get-intrinsic "^1.2.2"
get-symbol-description "^1.0.0"
globalthis "^1.0.3"
gopd "^1.0.1"
- has "^1.0.3"
has-property-descriptors "^1.0.0"
has-proto "^1.0.1"
has-symbols "^1.0.3"
+ hasown "^2.0.0"
internal-slot "^1.0.5"
is-array-buffer "^3.0.2"
is-callable "^1.2.7"
@@ -1505,7 +1513,7 @@ es-abstract@^1.22.1:
is-string "^1.0.7"
is-typed-array "^1.1.12"
is-weakref "^1.0.2"
- object-inspect "^1.12.3"
+ object-inspect "^1.13.1"
object-keys "^1.1.1"
object.assign "^4.1.4"
regexp.prototype.flags "^1.5.1"
@@ -1519,28 +1527,28 @@ es-abstract@^1.22.1:
typed-array-byte-offset "^1.0.0"
typed-array-length "^1.0.4"
unbox-primitive "^1.0.2"
- which-typed-array "^1.1.11"
+ which-typed-array "^1.1.13"
es-module-lexer@^1.0.0:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.1.tgz#c1b0dd5ada807a3b3155315911f364dc4e909db1"
- integrity sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.4.1.tgz#41ea21b43908fe6a287ffcbe4300f790555331f5"
+ integrity sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==
es-set-tostringtag@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8"
- integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz#11f7cc9f63376930a5f20be4915834f4bc74f9c9"
+ integrity sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==
dependencies:
- get-intrinsic "^1.1.3"
- has "^1.0.3"
+ get-intrinsic "^1.2.2"
has-tostringtag "^1.0.0"
+ hasown "^2.0.0"
es-shim-unscopables@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241"
- integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763"
+ integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==
dependencies:
- has "^1.0.3"
+ hasown "^2.0.0"
es-to-primitive@^1.2.1:
version "1.2.1"
@@ -1604,7 +1612,7 @@ eslint-config-prettier@^7.0.0:
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz#f4a4bd2832e810e8cc7c1411ec85b3e85c0c53f9"
integrity sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg==
-eslint-import-resolver-node@^0.3.7:
+eslint-import-resolver-node@^0.3.9:
version "0.3.9"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac"
integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==
@@ -1635,28 +1643,28 @@ eslint-plugin-html@^7.1.0:
dependencies:
htmlparser2 "^8.0.1"
-eslint-plugin-import@^2.28.1:
- version "2.28.1"
- resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz#63b8b5b3c409bfc75ebaf8fb206b07ab435482c4"
- integrity sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==
+eslint-plugin-import@^2.29.1:
+ version "2.29.1"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643"
+ integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==
dependencies:
- array-includes "^3.1.6"
- array.prototype.findlastindex "^1.2.2"
- array.prototype.flat "^1.3.1"
- array.prototype.flatmap "^1.3.1"
+ array-includes "^3.1.7"
+ array.prototype.findlastindex "^1.2.3"
+ array.prototype.flat "^1.3.2"
+ array.prototype.flatmap "^1.3.2"
debug "^3.2.7"
doctrine "^2.1.0"
- eslint-import-resolver-node "^0.3.7"
+ eslint-import-resolver-node "^0.3.9"
eslint-module-utils "^2.8.0"
- has "^1.0.3"
- is-core-module "^2.13.0"
+ hasown "^2.0.0"
+ is-core-module "^2.13.1"
is-glob "^4.0.3"
minimatch "^3.1.2"
- object.fromentries "^2.0.6"
- object.groupby "^1.0.0"
- object.values "^1.1.6"
+ object.fromentries "^2.0.7"
+ object.groupby "^1.0.1"
+ object.values "^1.1.7"
semver "^6.3.1"
- tsconfig-paths "^3.14.2"
+ tsconfig-paths "^3.15.0"
eslint-plugin-jsdoc@^44.2.7:
version "44.2.7"
@@ -1672,10 +1680,10 @@ eslint-plugin-jsdoc@^44.2.7:
semver "^7.5.1"
spdx-expression-parse "^3.0.1"
-eslint-plugin-lit@^1.9.1:
- version "1.9.1"
- resolved "https://registry.yarnpkg.com/eslint-plugin-lit/-/eslint-plugin-lit-1.9.1.tgz#40cdd1f8d1b565eb5e913eab159c88f6f947bb19"
- integrity sha512-XFFVufVxYJwqRB9sLkDXB7SvV1xi9hrC4HRFEdX1h9+iZ3dm4x9uS7EuT9uaXs6zR3DEgcojia1F7pmvWbc4Gg==
+eslint-plugin-lit@^1.11.0:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-lit/-/eslint-plugin-lit-1.11.0.tgz#32fc1c58b476e5b9aa1c7b6ba9de295641bd4e9b"
+ integrity sha512-jVqy2juQTAtOzj1ILf+ZW5GpDobXlSw0kvpP2zu2r8ZbW7KISt7ikj1Gw9DhNeirEU1UlSJR0VIWpdr4lzjayw==
dependencies:
parse5 "^6.0.1"
parse5-htmlparser2-tree-adapter "^6.0.1"
@@ -1745,18 +1753,19 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
-eslint@^7.10.0, eslint@^8.49.0:
- version "8.49.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.49.0.tgz#09d80a89bdb4edee2efcf6964623af1054bf6d42"
- integrity sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==
+eslint@^7.10.0, eslint@^8.49.0, eslint@^8.56.0:
+ version "8.56.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.56.0.tgz#4957ce8da409dc0809f99ab07a1b94832ab74b15"
+ integrity sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.6.1"
- "@eslint/eslintrc" "^2.1.2"
- "@eslint/js" "8.49.0"
- "@humanwhocodes/config-array" "^0.11.11"
+ "@eslint/eslintrc" "^2.1.4"
+ "@eslint/js" "8.56.0"
+ "@humanwhocodes/config-array" "^0.11.13"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"
+ "@ungap/structured-clone" "^1.2.0"
ajv "^6.12.4"
chalk "^4.0.0"
cross-spawn "^7.0.2"
@@ -1925,9 +1934,9 @@ fast-glob@^2.2.6:
micromatch "^3.1.10"
fast-glob@^3.2.2, fast-glob@^3.2.9:
- version "3.3.1"
- resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4"
- integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
+ integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
@@ -1946,9 +1955,9 @@ fast-levenshtein@^2.0.6:
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
fastq@^1.6.0:
- version "1.15.0"
- resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
- integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==
+ version "1.16.0"
+ resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.16.0.tgz#83b9a9375692db77a822df081edb6a9cf6839320"
+ integrity sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==
dependencies:
reusify "^1.0.4"
@@ -2007,15 +2016,15 @@ find-up@^5.0.0:
path-exists "^4.0.0"
flat-cache@^3.0.4:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.1.0.tgz#0e54ab4a1a60fe87e2946b6b00657f1c99e1af3f"
- integrity sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee"
+ integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==
dependencies:
- flatted "^3.2.7"
+ flatted "^3.2.9"
keyv "^4.5.3"
rimraf "^3.0.2"
-flatted@^3.2.7:
+flatted@^3.2.9:
version "3.2.9"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf"
integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==
@@ -2054,10 +2063,10 @@ fsevents@~2.3.2:
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
-function-bind@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
- integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+function-bind@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+ integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
function.prototype.name@^1.1.6:
version "1.1.6"
@@ -2079,15 +2088,15 @@ get-caller-file@^2.0.1:
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
-get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82"
- integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==
+get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b"
+ integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==
dependencies:
- function-bind "^1.1.1"
- has "^1.0.3"
+ function-bind "^1.1.2"
has-proto "^1.0.1"
has-symbols "^1.0.3"
+ hasown "^2.0.0"
get-stream@^6.0.0:
version "6.0.1"
@@ -2147,9 +2156,9 @@ glob@^7.1.3:
path-is-absolute "^1.0.0"
globals@^13.19.0:
- version "13.22.0"
- resolved "https://registry.yarnpkg.com/globals/-/globals-13.22.0.tgz#0c9fcb9c48a2494fbb5edbfee644285543eba9d8"
- integrity sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==
+ version "13.24.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171"
+ integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==
dependencies:
type-fest "^0.20.2"
@@ -2236,11 +2245,11 @@ has-flag@^4.0.0:
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
has-property-descriptors@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861"
- integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340"
+ integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==
dependencies:
- get-intrinsic "^1.1.1"
+ get-intrinsic "^1.2.2"
has-proto@^1.0.1:
version "1.0.1"
@@ -2290,12 +2299,12 @@ has-values@^1.0.0:
is-number "^3.0.0"
kind-of "^4.0.0"
-has@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
- integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+hasown@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c"
+ integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==
dependencies:
- function-bind "^1.1.1"
+ function-bind "^1.1.2"
hosted-git-info@^2.1.4:
version "2.8.9"
@@ -2361,9 +2370,9 @@ iconv-lite@^0.4.24:
safer-buffer ">= 2.1.2 < 3"
ignore@^5.1.1, ignore@^5.2.0:
- version "5.2.4"
- resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
- integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78"
+ integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==
import-fresh@^3.2.1:
version "3.3.0"
@@ -2421,12 +2430,12 @@ inquirer@^7.3.3:
through "^2.3.6"
internal-slot@^1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986"
- integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.6.tgz#37e756098c4911c5e912b8edbf71ed3aa116f930"
+ integrity sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==
dependencies:
- get-intrinsic "^1.2.0"
- has "^1.0.3"
+ get-intrinsic "^1.2.2"
+ hasown "^2.0.0"
side-channel "^1.0.4"
ip@^1.1.5:
@@ -2434,19 +2443,12 @@ ip@^1.1.5:
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48"
integrity sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==
-is-accessor-descriptor@^0.1.6:
- version "0.1.6"
- resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
- integrity sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==
- dependencies:
- kind-of "^3.0.2"
-
-is-accessor-descriptor@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
- integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==
+is-accessor-descriptor@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz#3223b10628354644b86260db29b3e693f5ceedd4"
+ integrity sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==
dependencies:
- kind-of "^6.0.0"
+ hasown "^2.0.0"
is-array-buffer@^3.0.1, is-array-buffer@^3.0.2:
version "3.0.2"
@@ -2501,26 +2503,19 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7:
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
-is-core-module@^2.13.0, is-core-module@^2.5.0:
- version "2.13.0"
- resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db"
- integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==
- dependencies:
- has "^1.0.3"
-
-is-data-descriptor@^0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
- integrity sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==
+is-core-module@^2.13.0, is-core-module@^2.13.1, is-core-module@^2.5.0:
+ version "2.13.1"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384"
+ integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==
dependencies:
- kind-of "^3.0.2"
+ hasown "^2.0.0"
-is-data-descriptor@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
- integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==
+is-data-descriptor@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz#2109164426166d32ea38c405c1e0945d9e6a4eeb"
+ integrity sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==
dependencies:
- kind-of "^6.0.0"
+ hasown "^2.0.0"
is-date-object@^1.0.1:
version "1.0.5"
@@ -2530,22 +2525,20 @@ is-date-object@^1.0.1:
has-tostringtag "^1.0.0"
is-descriptor@^0.1.0:
- version "0.1.6"
- resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
- integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.7.tgz#2727eb61fd789dcd5bdf0ed4569f551d2fe3be33"
+ integrity sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==
dependencies:
- is-accessor-descriptor "^0.1.6"
- is-data-descriptor "^0.1.4"
- kind-of "^5.0.0"
+ is-accessor-descriptor "^1.0.1"
+ is-data-descriptor "^1.0.1"
is-descriptor@^1.0.0, is-descriptor@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
- integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.3.tgz#92d27cb3cd311c4977a4db47df457234a13cb306"
+ integrity sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==
dependencies:
- is-accessor-descriptor "^1.0.0"
- is-data-descriptor "^1.0.0"
- kind-of "^6.0.2"
+ is-accessor-descriptor "^1.0.1"
+ is-data-descriptor "^1.0.1"
is-docker@^2.0.0, is-docker@^2.1.1:
version "2.2.1"
@@ -2800,9 +2793,9 @@ keygrip@~1.1.0:
tsscmp "1.0.6"
keyv@^4.5.3:
- version "4.5.3"
- resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.3.tgz#00873d2b046df737963157bd04f294ca818c9c25"
- integrity sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==
+ version "4.5.4"
+ resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
+ integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
dependencies:
json-buffer "3.0.1"
@@ -2820,12 +2813,7 @@ kind-of@^4.0.0:
dependencies:
is-buffer "^1.1.5"
-kind-of@^5.0.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
- integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
-
-kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3:
+kind-of@^6.0.2, kind-of@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
@@ -2868,15 +2856,15 @@ koa-static@^5.0.0:
koa-send "^5.0.0"
koa@^2.13.0:
- version "2.14.2"
- resolved "https://registry.yarnpkg.com/koa/-/koa-2.14.2.tgz#a57f925c03931c2b4d94b19d2ebf76d3244863fc"
- integrity sha512-VFI2bpJaodz6P7x2uyLiX6RLYpZmOJqNmoCst/Yyd7hQlszyPwG/I9CQJ63nOtKSxpt5M7NH67V6nJL2BwCl7g==
+ version "2.15.0"
+ resolved "https://registry.yarnpkg.com/koa/-/koa-2.15.0.tgz#d24ae1b0ff378bf12eb3df584ab4204e4c12ac2b"
+ integrity sha512-KEL/vU1knsoUvfP4MC4/GthpQrY/p6dzwaaGI6Rt4NQuFqkw3qrvsdYF5pz3wOfi7IGTvMPHC9aZIcUKYFNxsw==
dependencies:
accepts "^1.3.5"
cache-content-type "^1.0.0"
content-disposition "~0.5.2"
content-type "^1.0.4"
- cookies "~0.8.0"
+ cookies "~0.9.0"
debug "^4.3.2"
delegates "^1.0.0"
depd "^2.0.0"
@@ -3257,10 +3245,10 @@ object-copy@^0.1.0:
define-property "^0.2.5"
kind-of "^3.0.3"
-object-inspect@^1.12.3, object-inspect@^1.9.0:
- version "1.12.3"
- resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
- integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
+object-inspect@^1.13.1, object-inspect@^1.9.0:
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2"
+ integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==
object-keys@^1.1.1:
version "1.1.1"
@@ -3275,16 +3263,16 @@ object-visit@^1.0.0:
isobject "^3.0.0"
object.assign@^4.1.4:
- version "4.1.4"
- resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f"
- integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==
+ version "4.1.5"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0"
+ integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==
dependencies:
- call-bind "^1.0.2"
- define-properties "^1.1.4"
+ call-bind "^1.0.5"
+ define-properties "^1.2.1"
has-symbols "^1.0.3"
object-keys "^1.1.1"
-object.fromentries@^2.0.6:
+object.fromentries@^2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.7.tgz#71e95f441e9a0ea6baf682ecaaf37fa2a8d7e616"
integrity sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==
@@ -3293,7 +3281,7 @@ object.fromentries@^2.0.6:
define-properties "^1.2.0"
es-abstract "^1.22.1"
-object.groupby@^1.0.0:
+object.groupby@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.1.tgz#d41d9f3c8d6c778d9cbac86b4ee9f5af103152ee"
integrity sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==
@@ -3310,7 +3298,7 @@ object.pick@^1.3.0:
dependencies:
isobject "^3.0.1"
-object.values@^1.1.6:
+object.values@^1.1.7:
version "1.1.7"
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.7.tgz#617ed13272e7e1071b43973aa1655d9291b8442a"
integrity sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==
@@ -3564,9 +3552,9 @@ protobufjs@6.8.8:
long "^4.0.0"
punycode@^2.1.0, punycode@^2.1.1:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
- integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
+ integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
queue-microtask@^1.2.2:
version "1.2.3"
@@ -3622,9 +3610,9 @@ redent@^3.0.0:
strip-indent "^3.0.0"
regenerator-runtime@^0.14.0:
- version "0.14.0"
- resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
- integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==
+ version "0.14.1"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
+ integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
regex-not@^1.0.0, regex-not@^1.0.2:
version "1.0.2"
@@ -3692,9 +3680,9 @@ resolve-url@^0.2.1:
integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==
resolve@^1.10.0, resolve@^1.10.1, resolve@^1.19.0, resolve@^1.22.4:
- version "1.22.6"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.6.tgz#dd209739eca3aef739c626fea1b4f3c506195362"
- integrity sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==
+ version "1.22.8"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
+ integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
dependencies:
is-core-module "^2.13.0"
path-parse "^1.0.7"
@@ -3814,6 +3802,16 @@ set-blocking@^2.0.0:
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
+set-function-length@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed"
+ integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==
+ dependencies:
+ define-data-property "^1.1.1"
+ get-intrinsic "^1.2.1"
+ gopd "^1.0.1"
+ has-property-descriptors "^1.0.0"
+
set-function-name@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a"
@@ -3990,9 +3988,9 @@ spdx-expression-parse@^3.0.0, spdx-expression-parse@^3.0.1:
spdx-license-ids "^3.0.0"
spdx-license-ids@^3.0.0:
- version "3.0.15"
- resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.15.tgz#142460aabaca062bc7cd4cc87b7d50725ed6a4ba"
- integrity sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==
+ version "3.0.16"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz#a14f64e0954f6e25cc6587bd4f392522db0d998f"
+ integrity sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==
split-string@^3.0.1, split-string@^3.0.2:
version "3.1.0"
@@ -4212,10 +4210,10 @@ ts-simple-type@~1.0.5:
resolved "https://registry.yarnpkg.com/ts-simple-type/-/ts-simple-type-1.0.7.tgz#03930af557528dd40eaa121913c7035a0baaacf8"
integrity sha512-zKmsCQs4dZaeSKjEA7pLFDv7FHHqAFLPd0Mr//OIJvu8M+4p4bgSFJwZSEBEg3ec9W7RzRz1vi8giiX0+mheBQ==
-tsconfig-paths@^3.14.2:
- version "3.14.2"
- resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088"
- integrity sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==
+tsconfig-paths@^3.15.0:
+ version "3.15.0"
+ resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4"
+ integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==
dependencies:
"@types/json5" "^0.0.29"
json5 "^1.0.2"
@@ -4346,9 +4344,9 @@ typical@^7.1.1:
integrity sha512-T+tKVNs6Wu7IWiAce5BgMd7OZfNYUndHwc5MknN+UHOudi7sGZzuHdCadllRuqJ3fPtgFtIH9+lt9qRv6lmpfA==
ua-parser-js@^1.0.33:
- version "1.0.36"
- resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.36.tgz#a9ab6b9bd3a8efb90bb0816674b412717b7c428c"
- integrity sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw==
+ version "1.0.37"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.37.tgz#b5dc7b163a5c1f0c510b08446aed4da92c46373f"
+ integrity sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==
unbox-primitive@^1.0.2:
version "1.0.2"
@@ -4360,6 +4358,11 @@ unbox-primitive@^1.0.2:
has-symbols "^1.0.3"
which-boxed-primitive "^1.0.2"
+undici-types@~5.26.4:
+ version "5.26.5"
+ resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
+ integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
+
union-value@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
@@ -4429,9 +4432,9 @@ vscode-html-languageservice@3.1.0:
vscode-uri "^2.1.2"
vscode-languageserver-textdocument@^1.0.1:
- version "1.0.8"
- resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz#9eae94509cbd945ea44bca8dcfe4bb0c15bb3ac0"
- integrity sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q==
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf"
+ integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==
vscode-languageserver-types@3.16.0-next.2:
version "3.16.0-next.2"
@@ -4487,13 +4490,13 @@ which-module@^2.0.0:
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409"
integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==
-which-typed-array@^1.1.11:
- version "1.1.11"
- resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a"
- integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==
+which-typed-array@^1.1.11, which-typed-array@^1.1.13:
+ version "1.1.13"
+ resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.13.tgz#870cd5be06ddb616f504e7b039c4c24898184d36"
+ integrity sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==
dependencies:
available-typed-arrays "^1.0.5"
- call-bind "^1.0.2"
+ call-bind "^1.0.4"
for-each "^0.3.3"
gopd "^1.0.1"
has-tostringtag "^1.0.0"